Etiqueta: linux

  • Automatizando la conversión de una biblioteca musical con Bash y ffmpeg

    Para aquellos melómanos / audiófilos que tienen amplias colecciones por clasificar y/o reducir una biblioteca musical completa de 320kbps o cualquier tasa de bits a 128kbps para ahorrar espacio en disco, sobretodo este año donde el precio del almacenamiento está en los cielos, tareas de limpiar los nombres de archivo, y hacerlo sobre un share de red (SMB), procesando cientos de álbumes automáticamente sin intervención manual álbum por álbum. Este es el proceso completo, incluyendo los problemas reales que fueron apareciendo y cómo se resolvieron.

    Objetivo

    Convertir MP3 en 320kbps a 128kbps, eliminando además el prefijo numérico típico de pista en el nombre de archivo (01. Artista - Canción.mp3 → Artista - Canción.mp3).

    Comando base con ffmpeg

    for f in *.mp3; do
      ffmpeg -i "$f" -codec:a libmp3lame -b:a 128k "converted/$f"
    done

    -codec:a libmp3lame especifica el códec de audio (el encoder MP3 estándar en ffmpeg), y -b:a 128k fija el bitrate de audio objetivo. El resultado se guarda en una subcarpeta converted/ para no sobrescribir los originales durante las pruebas.

    Eliminando el prefijo numérico con regex

    for f in *.mp3; do
      if [[ "$f" =~ ^[0-9]+\.[[:space:]]*(.+)$ ]]; then
        newname="${BASH_REMATCH[1]}"
      else
        newname="$f"
      fi
      ffmpeg -i "$f" -codec:a libmp3lame -b:a 128k "converted/$newname"
    done

    El regex ^[0-9]+\.[[:space:]]*(.+)$ se lee así: uno o más dígitos al inicio (^[0-9]+), seguido de un punto literal (\.), seguido de cero o más espacios ([[:space:]]*), y captura todo el resto del nombre en un grupo ((.+)$). Funciona igual para prefijos de una, dos o tres cifras — 1., 01., 078. — sin necesitar reglas distintas para cada longitud. ${BASH_REMATCH[1]} es la variable de Bash que guarda el contenido del primer grupo capturado por el regex.

    Dry run: verificar antes de convertir

    Antes de correr la conversión real (que toma tiempo y no es instantánea de revertir), conviene simular el renombrado con un echo para confirmar visualmente que el patrón detecta los nombres correctamente:

    for f in *.mp3; do
      if [[ "$f" =~ ^[0-9]+\.[[:space:]]*(.+)$ ]]; then
        echo "'$f' -> '${BASH_REMATCH[1]}'"
      fi
    done

    Script final: procesar toda la biblioteca de forma recursiva

    Con el patrón ya validado, el script completo recorre todas las subcarpetas del directorio base, maneja álbumes con un nivel extra de subdirectorio (algunos venían con una carpeta duplicada dentro), y evita reprocesar carpetas ya convertidas:

    #!/bin/bash
    BASE="/mnt/hd1/biblio/musica_por_clasificar"
    
    for dir in "$BASE"/*/; do
      dir="${dir%/}"  # quitar slash final
      dirname=$(basename "$dir")
    
      # Carpetas ya procesadas o a saltar
      [[ "$dirname" == "converted" ]] && continue
      [[ "$dirname" == *"Nombre carpeta ya procesada 1"* ]] && continue
      [[ "$dirname" == *"Nombre carpeta ya procesada 2"* ]] && continue
    
      echo "=== Procesando: $dirname ==="
      mkdir -p "$dir/converted"
    
      for f in "$dir"/*.mp3 "$dir"/*/*.mp3; do
        [[ -f "$f" ]] || continue
    
        # Saltar archivos que ya estén dentro de converted/
        [[ "$f" == */converted/* ]] && continue
    
        fname=$(basename "$f")
        if [[ "$fname" =~ ^[0-9]+\.[[:space:]]*(.+)$ ]]; then
          newname="${BASH_REMATCH[1]}"
        else
          newname="$fname"
        fi
        echo "  -> $newname"
        ffmpeg -i "$f" -codec:a libmp3lame -b:a 128k "$dir/converted/$newname" -y -loglevel error
      done
    done
    
    echo "=== LISTO ==="

    Problemas encontrados y sus soluciones

    ProblemaCausaSolución
    El script no hacía nadaMount de gvfs lento o colgadoMontar el share con mount.cifs en vez de gvfs
    «Permiso denegado» al crear converted/Mount CIFS sin permisos de escrituraRemontar especificando uid, gid, dir_mode
    Doble slash //converted/$dir del glob */ ya termina en /dir="${dir%/}" para quitar el slash final antes de concatenar
    ffmpeg reportaba «same as Input #0»El glob */*.mp3 volvía a capturar archivos ya dentro de converted/Agregar la condición [[ "$f" == */converted/* ]] && continue
    Archivo corrupto («No stream»)Un MP3 inválido dentro de la colecciónffmpeg reporta el error y continúa — no bloquea el resto del loop
    Algunos álbumes quedaban sin convertirLos MP3 estaban dentro de una subcarpeta extra anidadaAgregar el glob adicional "$dir"/*/*.mp3 al loop

    Montar un share SMB correctamente para scripting

    Un detalle clave que causó varios de los problemas anteriores: gvfs (el montaje automático que hacen los gestores de archivos gráficos, visible en rutas como /run/user/1000/gvfs/smb-share:...) es inestable para scripts en lote — se cuelga o responde lento bajo cargas de I/O sostenidas. La alternativa confiable es montar el share manualmente con mount.cifs:

    # Crear el punto de montaje
    sudo mkdir -p /mnt/hd1
    
    # Montar con usuario y contraseña
    sudo mount -t cifs //192.168.100.50/hd1 /mnt/hd1 \
      -o username=TU_USUARIO,password=TU_PASSWORD,uid=1000,gid=1000,file_mode=0755,dir_mode=0755
    
    # Montar sin contraseña (share abierto/guest)
    sudo mount -t cifs //192.168.100.50/hd1 /mnt/hd1 \
      -o guest,uid=1000,gid=1000,file_mode=0755,dir_mode=0755
    
    # Verificar que el montaje permite escritura
    touch /mnt/hd1/test.txt && echo "OK escritura" && rm /mnt/hd1/test.txt

    Los parámetros uid y gid son importantes: sin ellos, los archivos montados vía CIFS suelen aparecer con el propietario root por defecto, lo que puede bloquear la escritura para un usuario normal — especificarlos fuerza a que el montaje asigne la propiedad al usuario local correcto.

    Notas técnicas adicionales

    • El regex ^[0-9]+\.[[:space:]]*(.+)$ funciona igual para prefijos de cualquier longitud: 1., 01., 078., etc.
    • -loglevel error en ffmpeg suprime toda la salida de metadata habitual, mostrando solo errores reales — indispensable al procesar miles de archivos, para no inundar la terminal.
    • -y sobrescribe el archivo destino sin preguntar si ya existe, necesario para que el loop corra sin intervención manual.
    • Convertir un MP3 de 128kbps a FLAC o de vuelta a 320kbps no mejora la calidad — la información descartada durante la compresión con pérdida original no se puede recuperar. Reconvertir un archivo ya comprimido a un bitrate mayor solo aumenta el tamaño del archivo, no la calidad de audio real.
  • Diagnóstico de bloqueos de apt: locks, debconf y deadlocks de systemd

    Un apt upgrade que se queda colgado rara vez tiene una sola causa — suele ser una cadena de bloqueos que se van destapando uno tras otro. Este es un caso real donde tres problemas distintos se encadenaron: un lock de dpkg, un bloqueo de debconf, y finalmente un deadlock real entre dos servicios de systemd provocado por needrestart.

    Cómo se encadenó el problema

    1. apt.systemd.daily tenía tomado el lock inicialmente — el timer automático de actualizaciones de Ubuntu/Debian corriendo en segundo plano, compitiendo por el mismo recurso que el apt upgrade manual.
    2. Al liberarse ese lock, apareció un bloqueo real en debconf, con el mensaje /var/cache/debconf/config.dat is locked by another process.
    3. Al actualizar libc6 (una librería base que usan prácticamente todos los procesos del sistema), needrestart intentó reiniciar automáticamente los servicios que dependían de ella — y quedó colgado a mitad de ese proceso.
    4. Esto generó un deadlock real entre Zabbix y MySQL: zabbix-server.service estaba en estado restart (running), mientras mysql.service quedó en start (waiting) — cada uno esperando indefinidamente al otro, sin que ninguno pudiera avanzar por sí solo.
    5. Cancelar manualmente los jobs colgados de systemd liberó el bloqueo, y apt pudo terminar correctamente.
    6. El sistema quedó estable, solo pendiente de un reinicio para cargar el nuevo kernel instalado durante la actualización.

    Por qué pasa esto: el rol de needrestart

    needrestart es una herramienta que, tras actualizar paquetes, detecta qué servicios en ejecución siguen usando versiones antiguas de librerías ya reemplazadas (típicamente detectando referencias a archivos .so borrados en memoria), y ofrece reiniciarlos automáticamente para que carguen las versiones nuevas. Es útil en general, pero cuando el paquete actualizado es algo tan fundamental como libc6, la lista de servicios a reiniciar puede ser larga — y si dos de esos servicios tienen una dependencia circular entre sí a nivel de arranque (como Zabbix, que necesita a MySQL corriendo, mientras MySQL en este caso quedó esperando por un recurso que Zabbix tenía tomado), el reinicio automático masivo puede desencadenar exactamente el tipo de deadlock que ocurrió aquí.

    Diagnóstico: ver quién tiene el lock

    sudo lsof /var/lib/dpkg/lock-frontend
    sudo lsof /var/lib/dpkg/lock
    sudo lsof /var/cache/debconf/config.dat

    lsof (list open files) muestra qué proceso tiene abierto un archivo específico — en este caso, los archivos de lock que usan dpkg y debconf para evitar que dos instancias se ejecuten simultáneamente y corrompan el estado de paquetes instalados. Si el comando devuelve un PID, ese es el proceso responsable del bloqueo.

    Detener los procesos automáticos de APT

    sudo systemctl stop apt-daily.service
    sudo systemctl stop apt-daily-upgrade.service
    sudo systemctl stop apt-daily.timer
    sudo systemctl stop apt-daily-upgrade.timer

    Ubuntu y Debian ejecutan actualizaciones automáticas de índices de paquetes (y opcionalmente de seguridad) en segundo plano mediante estos timers. Si uno de ellos está corriendo justo cuando se intenta un apt upgrade manual, ambos procesos compiten por el mismo lock — deteniendo estos servicios temporalmente se elimina esa competencia.

    Limpiar procesos colgados

    sudo kill -9 <PID>

    Último recurso cuando un proceso relacionado con apt/dpkg queda genuinamente colgado (no avanza ni responde) — kill -9 lo termina de forma forzosa, sin darle oportunidad de limpiar sus propios recursos. Debe usarse con cuidado, solo tras confirmar con lsof o ps que el proceso realmente está detenido y no simplemente trabajando lento.

    Reparar el estado de dpkg

    sudo dpkg --configure -a
    sudo apt -f install

    dpkg --configure -a retoma y completa la configuración de cualquier paquete que haya quedado a medias (por una interrupción, un lock forzado a matar, etc.). apt -f install («fix broken») resuelve dependencias rotas que hayan quedado como resultado.

    Ver y cancelar jobs pendientes en systemd

    # Listar jobs de systemd en curso o en espera
    systemctl list-jobs
    
    # Cancelar jobs específicos por su número de job (no PID de proceso)
    sudo systemctl cancel 2421271 2421892

    Esto fue lo que realmente destrabó el problema en este caso. Un «job» de systemd es una operación de cambio de estado en curso sobre una unidad (arrancar, detener, reiniciar) — cuando dos jobs quedan esperándose mutuamente (como el restart de Zabbix esperando el start de MySQL, y viceversa por otra dependencia), systemctl list-jobs los muestra con su estado (running o waiting), y cancel con el número de job los elimina de la cola sin necesidad de matar procesos a nivel de sistema operativo.

    Reiniciar servicios manualmente si hace falta

    sudo systemctl restart mysql
    sudo systemctl restart zabbix-server

    Tras romper el deadlock cancelando los jobs colgados, conviene reiniciar los servicios afectados manualmente, en el orden correcto de dependencia (la base de datos primero, el servicio que depende de ella después) para asegurar que ambos queden arriba de forma limpia.

    Validar el estado final

    sudo dpkg --configure -a
    sudo apt -f install
    sudo apt upgrade

    Repetir estos comandos confirma que ya no quedan paquetes a medio configurar ni dependencias rotas, y permite completar la actualización que originalmente se había colgado.

    Revisar servicios fallidos

    systemctl --failed

    Lista únicamente las unidades que quedaron en estado failed tras todo el proceso — un chequeo final rápido para confirmar que ningún otro servicio quedó afectado colateralmente por el deadlock o por los reinicios forzados.

    Reiniciar para cargar el kernel nuevo

    sudo reboot

    Cuando la actualización incluye un kernel nuevo (común en actualizaciones de libc6 y paquetes base), el sistema sigue corriendo con el kernel anterior en memoria hasta el próximo reinicio. No es urgente hacerlo de inmediato si el sistema está estable — se puede programar para una ventana de mantenimiento sin prisa.

    # Confirmar el kernel cargado después del reinicio
    uname -r

    Resumen del flujo de diagnóstico

    1. Si apt no avanza: verificar locks con lsof sobre los archivos de dpkg y debconf.
    2. Detener los timers automáticos de apt-daily si están compitiendo por el lock.
    3. Si un proceso está genuinamente colgado (no solo lento), terminarlo con kill -9 tras confirmarlo.
    4. Reparar con dpkg --configure -a y apt -f install.
    5. Si el problema persiste tras esto, revisar systemctl list-jobs — puede haber un deadlock real entre servicios provocado por needrestart, especialmente tras actualizar librerías base como libc6.
    6. Cancelar los jobs colgados con systemctl cancel, reiniciar manualmente los servicios afectados en el orden correcto de dependencia, y validar con systemctl --failed.