nidobin

My digital music collection cleanup & organization strategy

Tags: tech, tech.music, tech.picard

This guide outlines a two-part workflow to clean up a messy music directory and organize it into a strict, minimalist library structure on macOS.

For reference, this is how I generally organize my tunes:

Music
| Music-General
| Music-Holidays
| cleanup.sh
| backup.sh

Part 1: The Cleanup Script

Goal: Remove all non-audio files (images, logs, .DS_Store, etc.) and delete empty folders, while preserving the actual music files.

1. Create the Script

  1. Open the Terminal app on your Mac.
  2. Type nano cleanup.sh and press Enter.
  3. Copy and paste the code below into the window:
#!/bin/zsh

# --- Configuration ---
# Audio extensions to KEEP. 
KEEP_EXTENSIONS=("flac" "mp3" "m4a" "wav" "aiff" "ogg" "wma" "ape" "alac" "aac")

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BLUE='\033[1;34m'
NC='\033[0m' 

if [ -z "$1" ]; then
    echo -e "${RED}Error: No directory provided.${NC}"
    echo "Usage: ./cleanup.sh /path/to/music"
    exit 1
fi

TARGET_DIR="$1"

if [ ! -d "$TARGET_DIR" ]; then
    echo -e "${RED}Error: Directory not found:${NC} $TARGET_DIR"
    exit 1
fi

# Get absolute path of this script to prevent self-deletion
SCRIPT_PATH=${0:A}

# ==========================================
# STAGE 1: SCAN AND GROUP NON-AUDIO FILES
# ==========================================

echo -e "${CYAN}=== STAGE 1: Scanning for non-audio files ===${NC}"

# Master list for deletion
FILES_TO_DELETE=()

# Associative array for grouping files by extension for the preview
typeset -A PREVIEW_GROUPS

while IFS= read -r file; do
    # 1. Self-Preservation Check
    if [[ "${file:A}" == "$SCRIPT_PATH" ]]; then
        continue
    fi

    # 2. Extract Extension
    filename=$(basename "$file")
    if [[ "$filename" == *.* ]]; then
        ext="${filename##*.}"
        ext_lower="${ext:l}"
    else
        ext="No Extension"
        ext_lower="no_extension"
    fi
    
    # 3. Handle .DS_Store explicitly
    if [[ "$filename" == ".DS_Store" ]]; then
        FILES_TO_DELETE+=("$file")
        PREVIEW_GROUPS[SYSTEM_FILES]+="$file"$'\n'
        continue
    fi

    # 4. Check if Audio
    is_audio=false
    for valid_ext in $KEEP_EXTENSIONS; do
        if [[ "$ext_lower" == "$valid_ext" ]]; then
            is_audio=true
            break
        fi
    done

    # 5. If Not Audio, Add to Lists
    if [ "$is_audio" = false ]; then
        FILES_TO_DELETE+=("$file")
        # Add to the group string (using uppercase ext as key)
        group_key="${ext:u}"
        PREVIEW_GROUPS[$group_key]+="$file"$'\n'
    fi

done < <(find "$TARGET_DIR" -type f)

# ==========================================
# PREVIEW PHASE
# ==========================================

if [ ${#FILES_TO_DELETE[@]} -eq 0 ]; then
    echo -e "${GREEN}No non-audio files found.${NC}"
else
    echo -e "${YELLOW}The following non-audio files have been found:${NC}"
    echo ""

    # Iterate over the group keys (File Types)
    for key in ${(k)PREVIEW_GROUPS}; do
        # Count lines in this group variable
        # We trim the trailing newline to get accurate count
        content="${PREVIEW_GROUPS[$key]}"
        file_list=("${(@f)content}")
        count=${#file_list[@]}
        
        echo -e "${BLUE}=== $key ($count files) ===${NC}"
        # Print the files in this group
        echo -n "${PREVIEW_GROUPS[$key]}" | while read -r line; do
             # Indent files slightly for readability
             if [[ -n "$line" ]]; then
                echo "  $line"
             fi
        done
        echo ""
    done

    echo -e "${YELLOW}Total files to delete:${NC} ${#FILES_TO_DELETE[@]}"
    
    echo "---------------------------------------------------"
    read -q "response?Confirm delete ALL above files? (y/n) "
    echo ""
    
    if [[ "$response" == "y" ]]; then
        echo -e "${CYAN}Deleting...${NC}"
        for f in "${FILES_TO_DELETE[@]}"; do
            rm "$f"
        done
        echo -e "${GREEN}Files deleted.${NC}"
    else
        echo -e "${RED}Skipping file deletion.${NC}"
    fi
fi

echo "---------------------------------------------------"

# ==========================================
# STAGE 2: DELETE EMPTY DIRECTORIES
# ==========================================

echo -e "${CYAN}=== STAGE 2: Scanning for empty directories ===${NC}"

EMPTY_DIRS=()
while IFS= read -r dir; do
    # Protect root target dir
    if [[ "${dir:A}" == "${TARGET_DIR:A}" ]]; then
        continue
    fi
    EMPTY_DIRS+=("$dir")
done < <(find "$TARGET_DIR" -type d -empty)

if [ ${#EMPTY_DIRS[@]} -eq 0 ]; then
    echo -e "${GREEN}No empty directories found.${NC}"
    exit 0
fi

echo -e "${YELLOW}The following EMPTY directories will be REMOVED:${NC}"
for d in "${EMPTY_DIRS[@]}"; do
    echo -e "${RED}[REMOVE]${NC} $d"
done
echo -e "${YELLOW}Total empty folders:${NC} ${#EMPTY_DIRS[@]}"

echo ""
read -q "response?Confirm remove empty folders? (y/n) "
echo ""

if [[ "$response" == "y" ]]; then
    count=0
    for d in "${EMPTY_DIRS[@]}"; do
        rmdir "$d" 2>/dev/null
        if [ $? -eq 0 ]; then
            ((count++))
        fi
    done
    echo -e "${GREEN}Success! Removed $count empty folders.${NC}"
else
    echo -e "${GREEN}Operation cancelled. Empty folders remain.${NC}"
fi

2. Save and Run

  1. Save the file in nano: Press Ctrl + O, then Enter.

  2. Exit nano: Press Ctrl + X.

  3. Make it executable:

    chmod +x cleanup.sh
  4. Run the script on your target folder:

    ./cleanup.sh "/Users/yourname/Downloads/MusicToClean"

Part 2: MusicBrainz Picard Setup

Goal: Rename files, fix metadata, and organize them into a Artist / Album (Year) / Track structure automatically.

1. File Naming Settings

  1. Open MusicBrainz Picard.
  2. Go to Settings (Cmd + ,) > File Naming.
  3. Check "Move files when saving".
  4. Set Destination Directory to your final, clean music folder.
  5. Check "Rename files when saving".
  6. Paste the following code into the "Name files like this" box:
$if2(%albumartist%,%artist%)/  
$if(%album%,%album%$if(%date%, \\($left(%date%,4)\\)))/  
$if($gt(%totaldiscs%,1),$num(%discnumber%,2)-,)$num(%tracknumber%,2) \- %title%

Naming Logic:

  • Root Folder: Artist Name.
  • Album Folder: "Album Title (Year)". (Handles missing years gracefully).
  • File Name:
    • Single Disc: 01 - Title.mp3
    • Multi-Disc: 01-01 - Title.mp3 (Disc 01, Track 01)

2. Tag & Artwork Settings

  1. Go to Tags in the settings sidebar.
    • Check "Clear existing tags" (removes junk metadata).
    • Check "Remove ID3v2.3 tags" (forces current standards).
  2. Go to Cover Art.
    • Uncheck "Save cover images as separate files" (Keep folders clean).
    • Check "Embed cover images into tags" (Save art inside the audio file).
    • Check “Cover Art Archive: Release Group” (Fallback to release group images, when release image is not available)

3. Execution Workflow

  1. Drag your cleaned folder into Picard's left panel.
  2. Click Cluster (groups files by existing tags).
  3. Select the clusters and click Scan (listens to audio to identify tracks).
  4. Review matches in the right panel.
    • Green/Gold icons: Good match.
    • Red icons: Needs manual review.
  5. Select the matched albums and click Save.
    • Picard will move the files to your new library location.
  6. Cleanup: Run the strict_clean.sh script on your source folder again to remove the empty folders left behind by the move.
visitors
000294