/ Sérendipité / Code / Ranger sa collection de...

Ranger sa collection de MP3s avec Python et ffprobe

Avec un Python 3.x:

#!/usr/bin/env python
# Petit script quick and dirty utilisant la lib standard et l'utilitaire ffprobe,
# rangeant les MP3s dans des dossiers de la façon suivante:
# a/artiste/album/01-titre.mp3

import subprocess
import os
import sys
import fnmatch
import re
import unicodedata
import logging
import shutil

logger = logging.Logger(__name__)

def slugify(value, allow_unicode=False):
    """
    (from Django)
    Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
    dashes to single dashes. Remove characters that aren't alphanumerics,
    underscores, or hyphens. Convert to lowercase. Also strip leading and
    trailing whitespace, dashes, and underscores.
    """
    value = str(value)
    if allow_unicode:
        value = unicodedata.normalize("NFKC", value)
    else:
        value = (
            unicodedata.normalize("NFKD", value)
            .encode("ascii", "ignore")
            .decode("ascii")
        )
    value = re.sub(r"[^\w\s-]", "", value.lower())
    return re.sub(r"[-\s]+", "-", value).strip("-_")

def make_path_from_tags(tags: dict) -> str:
    return f"{slugify(tags.get('artist'))[0]}/" \
           f"{slugify(tags.get('artist'))}/" \
           f"{slugify(tags.get('album'))}/" \
           f"{str(tags.get('track')).zfill(2)}-" \
           f"{slugify(tags.get('title'))}.mp3"

def data_from_tags(path: str) -> dict:
    data = {
        "artist": None,
        "title": None,
        "album": "unknown",
        "track": 0,
    }
    proc = subprocess.Popen(
        [
            "ffprobe",
            "-loglevel",
            "error",
            "-show_entries",
            "format_tags=artist,album,title,track",
            "-of",
            "default=noprint_wrappers=1",
            path,
        ],
        stdout=subprocess.PIPE,
    )
    output = proc.stdout.read()
    output = output.decode("utf8", errors="ignore")
    for line in output.split("\n"):
        if line.strip():
            prop, val = line.split("=")
            prop = prop.strip().lower()
            val = val.strip()
            if prop in ["tag:artist", "tag:track", "tag:title", "tag:album"]:
                if prop == "tag:track":
                    try:
                        val = val.split("/")[0]
                        val = int(val)
                        data["track"] = val
                    except ValueError:
                        pass
                if prop == "tag:title" and val:
                    data["title"] = val
                if prop == "tag:album" and val:
                    data["album"] = val
                if prop == "tag:artist" and val:
                    data["artist"] = val
    return data

if __name__ == "__main__":
    if len(sys.argv) < 3:
        sys.exit(f"\nMove mp3s from one dir to another and arrange dirs according to tags.\n\nUsage: {sys.argv[0]} /source/dir/ /dest/dir\n")
    if not shutil.which("ffprobe"):
        sys.exit("Command-line tool ffprobe is needed but couldn't be found")
    for root, _, files in os.walk(sys.argv[1]):
        for item in fnmatch.filter(files, "*.mp3"):
            og_path = os.path.join(root, item)
            tags = data_from_tags(og_path)
            if not tags.get("artist") and not tags.get("title"):
                logger.warning(f"Not enough tag data, won't move {og_path}")
                continue
            dest = os.path.join(sys.argv[2], make_path_from_tags(tags))
            if os.path.exists(dest):
                logger.warning(f"File already exists, not gonna overwrite it: {dest}")
            else:
                logger.info(f"Moving file to {dest}")
                os.makedirs(os.path.dirname(dest), exist_ok=True)
                new_dest = shutil.copy2(og_path, dest)
                if os.path.exists(dest):
                    os.remove(og_path)