wp-note

Sviluppo Siti Web

Script Python per pulire Navidrome in base al rating

Navidrome è un popolare music server open source che ti permette catalogare e ascoltare la tua collezione musicale. Una delle sue funzionalità più utili è la possibilità di valutare le canzoni, con un sistema da una a cinque stelle. Ma cosa succede quando vuoi fare pulizia e liberare spazio, eliminando magari tutte le canzoni che hai valutato con una sola stella? Farlo manualmente può essere un processo lungo e noioso.

Il piccolo script python che ho scritto, identifica i file musicali che hanno solo 1 stella e li cancella. Vediamo come funziona.

Lo script: cosa fa e perché è utile

Lo script svolge il suo compito in tre fasi principali: interrogazione del database, elaborazione dei file e reportistica. È uno strumento creato su misura per risolvere un’esigenza specifica: tradurre una valutazione all’interno di un’applicazione (una stella in Navidrome) in un’azione concreta sul file system (la cancellazione del file).

1. Dialogare con il database di Navidrome

Il cuore di Navidrome è un database SQLite, un file leggero e autonomo (solitamente navidrome.db) che contiene tutte le informazioni sulla tua libreria: artisti, album, playlist e, soprattutto, i metadati come le valutazioni.

Lo script utilizza la libreria standard di Python sqlite3 per connettersi a questo database. La sua prima azione è eseguire una query SQL:

  • SELECT: Seleziona le informazioni cruciali: titolo della canzone, artista, percorso relativo del file e percorso base della libreria a cui appartiene.
  • JOIN: Collega diverse tabelle (annotation, media_file, artist, library) per mettere insieme tutte queste informazioni. Ad esempio, la valutazione di una canzone si trova nella tabella annotation, ma il suo percorso fisico è in media_file.
  • WHERE an.rating = 1: Questa è la condizione fondamentale che filtra i risultati, restituendo solo ed esclusivamente le canzoni a cui è stata assegnata una valutazione di una stella. Ovvamente potete cambiare questa condizione

Questo approccio è incredibilmente efficiente. Invece di analizzare manualmente migliaia di file sul disco, lo script interroga un indice già pronto, ottenendo una lista precisa dei file da eliminare in una frazione di secondo.

2. Operazioni sul File System

Una volta ottenuta la lista dei brani, inizia la fase operativa. Per ogni canzone, lo script:

  • Verifica e Cancella: Prima di cancellare, controlla che il file esista davvero con os.path.exists(). Successivamente, tenta di eliminarlo con os.remove(). L’uso di un blocco try...except garantisce che lo script non si blocchi in caso di problemi (ad esempio, permessi di file insufficienti), ma segnali l’errore e prosegua.
  • Ricostruisce il Percorso Assoluto: Navidrome, specialmente se eseguito in Docker, memorizza percorsi interni (es. /music/brano.mp3). Lo script prevede di combinare questi percorsi con il percorso base reale del sistema host (es. /mnt/media nel mio caso).

3. Report finale

Al termine di tutte le operazioni, lo script fornisce un riepilogo che riassume quante canzoni sono state cancellate con successo e quante operazioni sono fallite.

4. Il codice Python per eliminare file con 1 stella

import sqlite3
import os
import time
import sys

# Classe per gestire i codici colore ANSI in modo pulito
class Colors:
    GREEN = '\033[92m'
    RED = '\033[91m'
    YELLOW = '\033[93m'
    BLUE = '\033[94m'
    RESET = '\033[0m' # Resetta il colore al default

def cancella_canzoni_una_stella(db_path, percorso_host):
    """
    Identifica, cancella e riporta lo stato delle canzoni con 1 stella,
    con un output colorato e dinamico per il terminale.
    """
    canzoni_trovate_db = []
    
    # --- 1. Connessione e Lettura dal Database ---
    try:
        conn = sqlite3.connect(db_path)
        conn.row_factory = sqlite3.Row
        cursor = conn.cursor()
        
        query = """
            SELECT
                mf.title AS titolo,
                ar.name AS artista,
                lib.path AS percorso_libreria,
                mf.path AS percorso_relativo
            FROM
                annotation an
            JOIN
                media_file mf ON an.item_id = mf.id
            JOIN
                artist ar ON mf.artist_id = ar.id
            JOIN
                library lib ON mf.library_id = lib.id
            WHERE
                an.rating = 1;
        """
        cursor.execute(query)
        canzoni_trovate_db = cursor.fetchall()
        print(f"{Colors.BLUE}INFO:{Colors.RESET} Connessione al database riuscita. Trovate {len(canzoni_trovate_db)} canzoni con 1 stella.")

    except sqlite3.Error as e:
        print(f"{Colors.RED}ERRORE CRITICO:{Colors.RESET} Impossibile interrogare il database - {e}")
        return
    finally:
        if conn:
            conn.close()

    if not canzoni_trovate_db:
        print(f"{Colors.YELLOW}INFO:{Colors.RESET} Nessuna canzone da processare.")
        return

    # --- 2. Processo di Cancellazione e Report ---
    print(f"\n{Colors.BLUE}--- Inizio processo di cancellazione file ---{Colors.RESET}\n")
    successi = 0
    fallimenti = 0
    spinner_chars = ['-', '\\', '|', '/']
    total_files = len(canzoni_trovate_db)

    for i, canzone in enumerate(canzoni_trovate_db):
        # --- Effetto Spinner ---
        spinner_index = i % len(spinner_chars)
        spinner = spinner_chars[spinner_index]
        status_line = f" {spinner} Processando file {i+1} di {total_files}... "
        # Stampa la riga di stato senza andare a capo, sovrascrivendo la precedente
        sys.stdout.write(status_line + '\r')
        sys.stdout.flush()
        time.sleep(0.1) # Rende l'effetto visibile
        
        # --- Logica di cancellazione ---
        percorso_navidrome = os.path.join(canzone['percorso_libreria'], canzone['percorso_relativo'])
        percorso_reale_file = os.path.join(percorso_host, percorso_navidrome.lstrip('/'))
        
        # Sovrascrive la riga dello spinner con il risultato finale dell'operazione
        # ' ' * len(status_line) pulisce eventuali caratteri rimasti dalla riga precedente
        print(' ' * len(status_line) + '\r', end="") 
        
        if not os.path.exists(percorso_reale_file):
            print(f"[{Colors.YELLOW}FALLITO{Colors.RESET}] {percorso_reale_file}")
            print(f"  └─ Motivo: File non trovato sul disco.\n")
            fallimenti += 1
            continue

        try:
            os.remove(percorso_reale_file)
            print(f"[{Colors.GREEN}CANCELLATO{Colors.RESET}] {percorso_reale_file}\n")
            successi += 1
        except OSError as e:
            print(f"[{Colors.RED}ERRORE{Colors.RESET}] Non è stato possibile cancellare {percorso_reale_file}")
            print(f"  └─ Motivo: {e}\n")
            fallimenti += 1
            
    # --- 3. Report Finale ---
    print(f"{Colors.BLUE}--- Processo di cancellazione terminato ---{Colors.RESET}")
    print("Report finale:")
    print(f"  - {Colors.GREEN}File cancellati con successo: {successi}{Colors.RESET}")
    print(f"  - {Colors.YELLOW if fallimenti > 0 else ''}Operazioni fallite: {fallimenti}{Colors.RESET if fallimenti > 0 else ''}")
    print(f"{Colors.BLUE}------------------------------------------{Colors.RESET}")

# --- ESECUZIONE ---
if __name__ == "__main__":

    #ATTENZIONE: cambia questi 2 parametri in base alla tua configurazione
    db_file = '/home/pi/navidrome/data/navidrome.db'
    host_music_path = '/mnt/media'
    
    cancella_canzoni_una_stella(db_file, host_music_path)

5. Pulizia del database e aggiornamento della libreria

Finora ci siamo concentrati sui file: individuare quelli indesiderati e cancellarli dal disco. Ma come forse hai già notato, Navidrome non si accorge subito che quei file non ci sono più: nella sua interfaccia può continuare a mostrarti brani “fantasma”, che non esistono più fisicamente.

La soluzione? Fare una pulizia del database e avviare una nuova scansione della libreria.
Questa operazione equivale a cliccare su “Remove All” quando Navidrome ti segnala file mancanti, e subito dopo forzare una scansione. Con poche righe di Python possiamo automatizzare anche questo passaggio, così il database rimane sempre allineato allo stato reale dei tuoi file.

Ecco il codice:

# Configurazione iniziale: credenziali e parametri Subsonic API
NAVIDROME_URL = "http://192.168.xxx.xxx:4533"
USERNAME = "il_tuo_username"
PASSWORD = "la_tua_password"
API_VERSION = "1.16.1"
CLIENT = "script"

def get_auth_params():
    """Genera i parametri di autenticazione per le API Subsonic"""
    salt = "mysalt"
    token = hashlib.md5((PASSWORD + salt).encode()).hexdigest()
    return {
        "u": USERNAME,
        "t": token,
        "s": salt,
        "v": API_VERSION,
        "c": CLIENT,
        "f": "json",
    }
    #se vuoi una scansione completa aggiungere il parametro:
    #"fullScan": "true"

def start_scan():
    """Avvia una nuova scansione della libreria"""
    params = get_auth_params()
    r = requests.get(f"{NAVIDROME_URL}/rest/startScan.view", params=params)
    r.raise_for_status()
    print("Scan avviato ✅")
    return True

def wait_until_done(poll_interval=1):
    """Attende la fine della scansione interrogando periodicamente lo stato"""
    params = get_auth_params()
    while True:
        r = requests.get(f"{NAVIDROME_URL}/rest/getScanStatus.view", params=params)
        r.raise_for_status()
        data = r.json()["subsonic-response"]["scanStatus"]
        if not data["scanning"]:
            print("✅ Scan completato")
            break
        print(f"⏳ In corso: {data['count']} cartelle scansionate...")
        time.sleep(poll_interval)

def get_token():
    """Recupera un token JWT valido per le API interne di Navidrome"""
    r = requests.post(f"{NAVIDROME_URL}/auth/login", json={
        "username": USERNAME,
        "password": PASSWORD
    })
    r.raise_for_status()
    return r.json()["token"]

def remove_missing_files():
    """Elimina dal database tutti i riferimenti ai file mancanti"""
    token = get_token()
    headers = {
        "accept": "application/json",
        "content-type": "application/json",
        "x-nd-authorization": f"Bearer {token}",
        "x-nd-client-unique-id": "python-script"
    }
    r = requests.delete(f"{NAVIDROME_URL}/api/missing", headers=headers)
    if r.status_code == 200:
        print("🗑️ File mancanti rimossi dal database")
    else:
        print("❌ Errore nella pulizia:", r.status_code, r.text)

if __name__ == "__main__":
    # 1. Rimuovi i file mancanti dal database
    remove_missing_files()
    # 2. Avvia una nuova scansione
    start_scan()
    # 3. Aspetta la fine della scansione
    wait_until_done()

Pubblicato

in

, ,

da

Tag:

Commenti

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *