Created
November 21, 2025 07:52
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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