Skip to content

Instantly share code, notes, and snippets.

@zyphlar
Created November 21, 2025 07:52
Show Gist options
  • Select an option

  • Save zyphlar/c7646aa9abc53f947b279f65a3e8106d to your computer and use it in GitHub Desktop.

Select an option

Save zyphlar/c7646aa9abc53f947b279f65a3e8106d to your computer and use it in GitHub Desktop.
Detect and write MP3 metadata (ID3 tags) based on filenames (## Artist - Title.mp3)
#!/bin/bash
# Script to set MP3 artist and title metadata from filename
# Expected format: "artist - title.mp3"
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check if id3v2 is installed
if ! command -v id3v2 &> /dev/null; then
echo -e "${RED}Error: id3v2 is not installed.${NC}"
echo "Please install it with: sudo apt-get install id3v2"
exit 1
fi
# Function to process a single MP3 file
process_mp3() {
local file="$1"
local filename=$(basename "$file" .mp3)
# Check if file exists and is an MP3
if [[ ! -f "$file" ]]; then
echo -e "${RED}Error: File '$file' not found${NC}"
return 1
fi
if [[ ! "$file" =~ \.mp3$ ]]; then
echo -e "${YELLOW}Warning: '$file' is not an MP3 file, skipping${NC}"
return 1
fi
# Check if filename contains a separator (- or -- with optional spaces)
if [[ ! "$filename" =~ [[:space:]]*-{1,2}[[:space:]]* ]]; then
echo -e "${YELLOW}Warning: '$file' doesn't contain a valid separator (- or --), skipping${NC}"
return 1
fi
# Determine format and extract metadata
local track=""
local artist=""
local title=""
# Check if filename starts with a number (track number)
if [[ "$filename" =~ ^[0-9]+ ]]; then
# Format: "track title - artist.mp3"
# Extract track number
track="${filename%% *}"
# Remove track number from filename
local remaining="${filename#* }"
# Split at the first occurrence of separator (- or -- with optional spaces)
# Use sed to replace only the first separator with unique delimiter
local split_result=$(echo "$remaining" | sed -E 's/[[:space:]]*-{1,2}[[:space:]]*/__SEPARATOR__/1')
if [[ "$split_result" == *"__SEPARATOR__"* ]]; then
title="${split_result%%__SEPARATOR__*}"
artist="${split_result#*__SEPARATOR__}"
else
echo -e "${YELLOW}Warning: '$file' couldn't be split properly, skipping${NC}"
return 1
fi
else
# Format: "artist - title.mp3"
# Split at the first occurrence of separator (- or -- with optional spaces)
local split_result=$(echo "$filename" | sed -E 's/[[:space:]]*-{1,2}[[:space:]]*/__SEPARATOR__/1')
if [[ "$split_result" == *"__SEPARATOR__"* ]]; then
artist="${split_result%%__SEPARATOR__*}"
title="${split_result#*__SEPARATOR__}"
else
echo -e "${YELLOW}Warning: '$file' couldn't be split properly, skipping${NC}"
return 1
fi
# Check if title starts with a track number
if [[ "$title" =~ ^[0-9]+ ]]; then
track="${title%% *}"
title="${title#* }"
fi
fi
# Trim whitespace
artist="$(echo -e "${artist}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
title="$(echo -e "${title}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
track="$(echo -e "${track}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
echo -e "${GREEN}Processing:${NC} $file"
echo " Artist: $artist"
echo " Title: $title"
if [[ -n "$track" ]]; then
echo " Track: $track"
fi
# Build id3v2 command
local id3_cmd="id3v2 --artist \"$artist\" --song \"$title\""
# Add track number if present
if [[ -n "$track" ]]; then
id3_cmd="$id3_cmd --track \"$track\""
fi
# Add the file
id3_cmd="$id3_cmd \"$file\""
# Execute the command
eval $id3_cmd
if [ $? -eq 0 ]; then
echo -e "${GREEN} ✓ Metadata updated successfully${NC}"
else
echo -e "${RED} ✗ Failed to update metadata${NC}"
return 1
fi
return 0
}
# Function to show usage
show_usage() {
echo "Usage: $0 [OPTIONS] <mp3_files_or_directory>"
echo ""
echo "Options:"
echo " -r, --recursive Process directories recursively"
echo " -d, --dry-run Show what would be done without making changes"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 'Artist Name - Song Title.mp3'"
echo " $0 'Artist Name--Song Title.mp3'"
echo " $0 'Artist Name-Song Title.mp3'"
echo " $0 '01 Song Title - Artist Name.mp3'"
echo " $0 *.mp3"
echo " $0 -r /path/to/music/directory"
echo ""
echo "Supported filename formats:"
echo " 'Artist - Title.mp3' (sets artist and title)"
echo " 'Artist - 01 Title.mp3' (sets artist, title, and track number)"
echo " '01 Title - Artist.mp3' (sets track number, title, and artist)"
echo ""
echo "Separator patterns:"
echo " The script accepts these separators between artist and title:"
echo " ' - ' (space-dash-space)"
echo " ' -- ' (space-double-dash-space)"
echo " '-' (single dash without spaces)"
echo " '--' (double dash without spaces)"
echo " Only the FIRST separator is used to split artist from title."
}
# Parse command line arguments
RECURSIVE=false
DRY_RUN=false
FILES=()
while [[ $# -gt 0 ]]; do
case $1 in
-r|--recursive)
RECURSIVE=true
shift
;;
-d|--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
show_usage
exit 0
;;
*)
FILES+=("$1")
shift
;;
esac
done
# Check if any files were provided
if [ ${#FILES[@]} -eq 0 ]; then
echo -e "${RED}Error: No files or directories specified${NC}"
show_usage
exit 1
fi
# If dry run mode, modify the process_mp3 function
if $DRY_RUN; then
echo -e "${YELLOW}DRY RUN MODE - No changes will be made${NC}"
echo ""
# Override the process_mp3 function for dry run
process_mp3() {
local file="$1"
local filename=$(basename "$file" .mp3)
if [[ ! -f "$file" ]]; then
echo -e "${RED}Error: File '$file' not found${NC}"
return 1
fi
if [[ ! "$file" =~ \.mp3$ ]]; then
echo -e "${YELLOW}Warning: '$file' is not an MP3 file, skipping${NC}"
return 1
fi
# Check if filename contains a separator (- or -- with optional spaces)
if [[ ! "$filename" =~ [[:space:]]*-{1,2}[[:space:]]* ]]; then
echo -e "${YELLOW}Warning: '$file' doesn't contain a valid separator (- or --), skipping${NC}"
return 1
fi
# Determine format and extract metadata
local track=""
local artist=""
local title=""
# Check if filename starts with a number (track number)
if [[ "$filename" =~ ^[0-9]+ ]]; then
# Format: "track title - artist.mp3"
track="${filename%% *}"
local remaining="${filename#* }"
# Split at the first occurrence of separator
local split_result=$(echo "$remaining" | sed -E 's/[[:space:]]*-{1,2}[[:space:]]*/__SEPARATOR__/1')
if [[ "$split_result" == *"__SEPARATOR__"* ]]; then
title="${split_result%%__SEPARATOR__*}"
artist="${split_result#*__SEPARATOR__}"
else
echo -e "${YELLOW}Warning: '$file' couldn't be split properly, skipping${NC}"
return 1
fi
else
# Format: "artist - title.mp3"
local split_result=$(echo "$filename" | sed -E 's/[[:space:]]*-{1,2}[[:space:]]*/__SEPARATOR__/1')
if [[ "$split_result" == *"__SEPARATOR__"* ]]; then
artist="${split_result%%__SEPARATOR__*}"
title="${split_result#*__SEPARATOR__}"
else
echo -e "${YELLOW}Warning: '$file' couldn't be split properly, skipping${NC}"
return 1
fi
# Check if title starts with a track number
if [[ "$title" =~ ^[0-9]+ ]]; then
track="${title%% *}"
title="${title#* }"
fi
fi
artist="$(echo -e "${artist}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
title="$(echo -e "${title}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
track="$(echo -e "${track}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
echo -e "${GREEN}Would process:${NC} $file"
echo " Artist: $artist"
echo " Title: $title"
if [[ -n "$track" ]]; then
echo " Track: $track"
fi
return 0
}
fi
# Process files
SUCCESS_COUNT=0
SKIP_COUNT=0
TOTAL_COUNT=0
for item in "${FILES[@]}"; do
if [ -d "$item" ]; then
# It's a directory
echo -e "${GREEN}Processing directory: $item${NC}"
if $RECURSIVE; then
# Find all MP3 files recursively
while IFS= read -r -d '' mp3_file; do
((TOTAL_COUNT++))
if process_mp3 "$mp3_file"; then
((SUCCESS_COUNT++))
else
((SKIP_COUNT++))
fi
echo ""
done < <(find "$item" -type f -name "*.mp3" -print0)
else
# Process only MP3 files in the directory (not recursive)
for mp3_file in "$item"/*.mp3; do
if [ -f "$mp3_file" ]; then
((TOTAL_COUNT++))
if process_mp3 "$mp3_file"; then
((SUCCESS_COUNT++))
else
((SKIP_COUNT++))
fi
echo ""
fi
done
fi
elif [ -f "$item" ]; then
# It's a file
((TOTAL_COUNT++))
if process_mp3 "$item"; then
((SUCCESS_COUNT++))
else
((SKIP_COUNT++))
fi
echo ""
else
echo -e "${RED}Error: '$item' is neither a file nor a directory${NC}"
((SKIP_COUNT++))
((TOTAL_COUNT++))
fi
done
# Show summary
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${GREEN}Summary:${NC}"
echo " Total files: $TOTAL_COUNT"
echo " Successfully processed: $SUCCESS_COUNT"
echo " Skipped: $SKIP_COUNT"
if $DRY_RUN; then
echo ""
echo -e "${YELLOW}This was a dry run. No files were modified.${NC}"
echo "Remove the -d/--dry-run flag to apply changes."
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment