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
- Open the Terminal app on your Mac.
- Type
nano cleanup.shand press Enter. - 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
-
Save the file in nano: Press Ctrl + O, then Enter.
-
Exit nano: Press Ctrl + X.
-
Make it executable:
chmod +x cleanup.sh -
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
- Open MusicBrainz Picard.
- Go to Settings (Cmd + ,) > File Naming.
- Check "Move files when saving".
- Set Destination Directory to your final, clean music folder.
- Check "Rename files when saving".
- 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)
- Single Disc:
2. Tag & Artwork Settings
- Go to Tags in the settings sidebar.
- Check "Clear existing tags" (removes junk metadata).
- Check "Remove ID3v2.3 tags" (forces current standards).
- 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
- Drag your cleaned folder into Picard's left panel.
- Click Cluster (groups files by existing tags).
- Select the clusters and click Scan (listens to audio to identify tracks).
- Review matches in the right panel.
- Green/Gold icons: Good match.
- Red icons: Needs manual review.
- Select the matched albums and click Save.
- Picard will move the files to your new library location.
- Cleanup: Run the strict_clean.sh script on your source folder again to remove the empty folders left behind by the move.