green frog iphone case beside black samsung android smartphone

Sync Your iTunes/Music Playlists to Android with a Simple Bash Script

Do you have an iPhone or Mac but an Android phone? If you’re like me, you might have all your carefully curated playlists in iTunes/Music, but struggle to get them onto your Android device. After years of manually recreating playlists, I finally created a solution: a bash script that automatically exports and syncs your playlists from macOS to Android.

Note: This script deletes all files in your Music folder by design as it syncs the Music folder. Take this into account. So make a backup before running. Use it at your own risk.

What This Script Does

This script:

  1. Extracts playlists directly from your Mac’s Music app (iTunes)
  2. Exports them as standard M3U playlist files
  3. Optionally syncs both the playlists and actual music files to your Android device
  4. Remembers your preferences for next time
  5. Only transfers files that aren’t already on your device (to save time)

🤓😎 More and more people are getting our Geek, Privacy, Dev & Lifestyle Tips

Want to receive the latest Geek, Privacy, Dev & Lifestyle blogs? Subscribe to our newsletter.

Installation Instructions

Disclaimer: This script is best suited for developers or tech-savvy users who are comfortable with Terminal commands. Use at your own risk. While the script has been tested, I can’t guarantee it will work perfectly with all setups. As mentioned above, it deletes all your music on your Android phone.

Prerequisites:

Installation Steps:

  1. Install ADB if you don’t have it already. The easiest way is to use Homebrew:
    brew install android-platform-tools
  2. Enable USB Debugging on your Android device:
    • Go to Settings > About Phone
    • Tap “Build Number” 7 times to enable Developer Options
    • Go back to Settings > System > Developer Options
    • Enable “USB Debugging”
  3. Create the script file:
    • Open Terminal
    • Create a new file using your preferred text editor (I’ll use nano):
      nano ~/music_playlist_sync.sh
    • Copy and paste the entire script (the code at the bottom of this blog)
    • Save and exit (in nano, press Ctrl+O, then Enter, then Ctrl+X)
  4. Make the script executable:
  5. chmod +x ~/music_playlist_sync.sh

Using the Script

To run the script:

~/music_playlist_sync.sh

The first time you run it, the script will:

  1. Display all available playlists from your Music app
  2. Ask which playlist(s) you want to export (you can select multiple with comma-separated numbers)
  3. Ask where you want to save the exported playlists
  4. Ask if you want to sync to an Android device
  5. If yes, it will look for connected devices and help locate the music folder

All these preferences are saved, so next time you run the script it will remember your choices.

Troubleshooting Tips

Music Not Showing Up After Sync

Sometimes Android doesn’t immediately recognize newly added music files. To force a media rescan:

  1. Install a media rescanning app from Google Play Store, like “Rescan Media” or “SD Rescan”
  2. Run the app to trigger a media library rescan
  3. Sometimes a phone restart does the trick

Samsung Phones

On Samsung devices, you may need to manually import the playlists:

  1. Open the Samsung Music app
  2. Go to Settings > Playlists > Import Playlists
  3. Your new playlists should now appear

ADB Connection Issues

If the script can’t find your device:

  • Make sure USB debugging is enabled
  • Try a different USB cable (some cables only support charging)
  • Unlock your phone and accept any USB debugging prompts
  • Try running adb devices in Terminal to check if your device is recognized

How It Works

The script uses AppleScript to directly communicate with the Music app and extract playlist information. It then converts these into standard M3U format playlists that can be read by most music players. If you choose to sync to Android, it uses ADB to transfer both the playlist files and the actual music files to your device.

For efficiency, it keeps track of which files have already been transferred and only syncs new or changed files.

Limitations

  • The script only works on macOS with the Music app
  • Your Android device needs to be connected via USB with debugging enabled
  • It doesn’t sync metadata like play counts or ratings
  • It may take a long time on first sync if you have a large music library

Final Thoughts

This script has saved me hours of tedious work recreating my playlists on Android. It’s a simple solution to a problem that many people with mixed Apple/Android ecosystems face. Feel free to modify it to suit your specific needs – the code is well-commented to make customization easier.

If you’re tired of maintaining separate playlists across devices, give this script a try and enjoy your carefully curated music collections on your Android device!

The script

Call it something like music_playlist_sync.sh

#!/bin/bash

# Config file path in home directory
CONFIG_FILE="$HOME/.music_playlist_export.conf"

# Common Android music paths to try
ANDROID_PATHS=(
    "/storage/emulated/0/Music"
    "/sdcard/Music"
    "/storage/sdcard0/Music"
    "/storage/sdcard1/Music"
    "/storage/self/primary/Music"
)

# Function to sanitize filenames
sanitize_filename() {
  echo "$1" | sed 's/[^a-zA-Z0-9._-]/_/g'
}

# Check if adb is installed
if ! command -v adb &> /dev/null; then
    echo "Error: ADB is not installed or not in your PATH"
    echo "Please install Android Debug Bridge (adb) to use the sync functionality"
    exit 1
fi

# Check device connection
check_device() {
    echo "Checking for connected Android devices..."
    devices=$(adb devices | grep -v "List" | grep "device$")
    if [ -z "$devices" ]; then
        echo "No Android device found. Please connect your device with USB debugging enabled."
        exit 1
    fi
    echo "Device connected."
}

# Auto-detect Android music path
detect_android_music_path() {
    echo "Detecting Android music path..."
    for path in "${ANDROID_PATHS[@]}"; do
        exists=$(adb shell "[ -d \"$path\" ] && echo 'yes' || echo 'no'")
        if [ "$exists" = "yes" ]; then
            echo "Found music directory: $path"
            ANDROID_MUSIC_PATH="$path"
            return 0
        fi
    done
    echo "Could not auto-detect music path. Will use default."
    return 1
}

# Get playlists directly using AppleScript
echo "Getting playlist information from Music app..."
playlist_output=$(osascript <<EOF
tell application "Music"
    set output to ""
    set playlists_list to user playlists
    repeat with i from 1 to count of playlists_list
        set pl to item i of playlists_list
        set pl_name to name of pl
        set output to output & pl_name & "|"
    end repeat
    return output
end tell
EOF
)

# Convert to array (macOS compatible)
IFS='|' read -r -a playlist_array <<< "$playlist_output"

# Check if we got any playlists
if [ ${#playlist_array[@]} -eq 0 ]; then
    echo "Error: No playlists found in Music app. Try using 'all playlists' instead of 'user playlists'."
    
    # Try again with all playlists
    echo "Trying with all playlists instead..."
    playlist_output=$(osascript <<EOF
tell application "Music"
    set output to ""
    set playlists_list to playlists
    repeat with i from 1 to count of playlists_list
        set pl to item i of playlists_list
        set pl_name to name of pl
        set output to output & pl_name & "|"
    end repeat
    return output
end tell
EOF
)
    
    # Convert to array (macOS compatible)
    IFS='|' read -r -a playlist_array <<< "$playlist_output"
    
    if [ ${#playlist_array[@]} -eq 0 ]; then
        echo "Error: Still no playlists found. Please check your Music app."
        exit 1
    fi
fi

# Display with numbers
echo "Available playlists:"
for i in "${!playlist_array[@]}"; do
    # Skip empty entries
    if [ -n "${playlist_array[$i]}" ]; then
        echo "$((i+1)): ${playlist_array[$i]}"
    fi
done

# Check if we have a previous selection
previous_selection=""
previous_dir=""
previous_android_path=""
if [ -f "$CONFIG_FILE" ]; then
    previous_selection=$(grep "^PLAYLIST_SELECTION=" "$CONFIG_FILE" | cut -d= -f2 2>/dev/null)
    previous_dir=$(grep "^EXPORT_DIR=" "$CONFIG_FILE" | cut -d= -f2 2>/dev/null)
    previous_android_path=$(grep "^ANDROID_MUSIC_PATH=" "$CONFIG_FILE" | cut -d= -f2 2>/dev/null)
fi

# Get user selection with support for comma-separated values
if [ -n "$previous_selection" ]; then
    read -p "Enter playlist number(s) to export (comma-separated for multiple) [$previous_selection]: " selection_input
    selection_input="${selection_input:-$previous_selection}"
else
    read -p "Enter playlist number(s) to export (comma-separated for multiple): " selection_input
fi

# Default export directory
default_dir="${previous_dir:-$HOME/exported-playlists}"
read -p "Enter export directory [$default_dir]: " user_dir
export_dir="${user_dir:-$default_dir}"
export_dir="${export_dir/#\~/$HOME}"

# Ask about Android sync
read -p "Sync to Android device? (y/n): " android_sync
if [[ "$android_sync" =~ ^[Yy]$ ]]; then
    check_device
    
    # Try to detect Android music path
    ANDROID_MUSIC_PATH="${previous_android_path:-${ANDROID_PATHS[0]}}"
    detect_android_music_path
    
    read -p "Enter Android music path [$ANDROID_MUSIC_PATH]: " android_path
    ANDROID_MUSIC_PATH="${android_path:-$ANDROID_MUSIC_PATH}"
    
    # Create playlists directory on device
    adb shell "mkdir -p \"$ANDROID_MUSIC_PATH/Playlists\""
fi

# Save the selections to config file
mkdir -p "$(dirname "$CONFIG_FILE")"
echo "PLAYLIST_SELECTION=$selection_input" > "$CONFIG_FILE"
echo "EXPORT_DIR=$export_dir" >> "$CONFIG_FILE"
echo "LAST_RUN=$(date)" >> "$CONFIG_FILE"
if [[ "$android_sync" =~ ^[Yy]$ ]]; then
    echo "ANDROID_MUSIC_PATH=$ANDROID_MUSIC_PATH" >> "$CONFIG_FILE"
fi

# Create the directory if it doesn't exist
mkdir -p "$export_dir"

# All files to sync
ALL_MUSIC_FILES="$export_dir/.all_music_files.txt"
> "$ALL_MUSIC_FILES"

# Process comma-separated selections
IFS=',' read -r -a selections <<< "$selection_input"
for selection in "${selections[@]}"; do
    # Trim any spaces
    selection=$(echo "$selection" | xargs)
    
    if [[ "$selection" =~ ^[0-9]+$ ]]; then
        index=$((selection-1))
        if [ "$index" -ge 0 ] && [ "$index" -lt "${#playlist_array[@]}" ]; then
            selected_playlist="${playlist_array[$index]}"
            
            # Skip empty playlist names
            if [ -z "$selected_playlist" ]; then
                echo "Skipping empty playlist at index $selection"
                continue
            fi
            
            safe_name=$(sanitize_filename "$selected_playlist")
            output_path="$export_dir/${safe_name}.m3u"
            
            echo "Exporting playlist: $selected_playlist"
            
            # Create a temporary file for track paths
            playlist_tracks="$export_dir/.playlist_tracks_${safe_name}.txt"
            
            # Get tracks directly from Music app
            osascript > "$playlist_tracks" <<EOF
tell application "Music"
    set thePlaylist to (first playlist whose name is "$selected_playlist")
    set theOutput to ""
    repeat with aTrack in tracks of thePlaylist
        try
            if class of aTrack is file track then
                set trackLoc to location of aTrack
                if trackLoc is not missing value then
                    set trackPath to POSIX path of trackLoc
                    set theOutput to theOutput & trackPath & linefeed
                    log "Found track: " & trackPath
                end if
            end if
        on error errMsg
            log "Error processing track: " & errMsg
        end try
    end repeat
    return theOutput
end tell
EOF

            # Clean up the output file to ensure proper line breaks
            tr -d '\r' < "$playlist_tracks" | grep -v '^$' | tr '\n' '\n' > "$playlist_tracks.tmp" && mv "$playlist_tracks.tmp" "$playlist_tracks"

            # Count the tracks and show debug info
            track_count=$(grep -v "^$" "$playlist_tracks" | wc -l | tr -d ' ')
            echo "Debug: Found $track_count tracks in playlist"
            echo "Debug: First few tracks:"
            head -n 3 "$playlist_tracks"
            
            # Create the M3U file
            echo "#EXTM3U" > "$output_path"
            cat "$playlist_tracks" >> "$output_path"
            
            echo "Success: exported $track_count tracks to $output_path"
            
            # Add to the list of files to sync
            cat "$playlist_tracks" >> "$ALL_MUSIC_FILES"
            
            # Create Android version if needed
            if [[ "$android_sync" =~ ^[Yy]$ ]] && [ "$track_count" -gt 0 ]; then
                android_playlist="$ANDROID_MUSIC_PATH/Playlists/${safe_name}.m3u"
                android_m3u="$export_dir/android_${safe_name}.m3u"
                
                echo "#EXTM3U" > "$android_m3u"
                
                # Process each track path with better error handling
                while IFS= read -r track_path; do
                    if [ -n "$track_path" ]; then
                        if [ -f "$track_path" ]; then
                            filename=$(basename "$track_path")
                            echo "$ANDROID_MUSIC_PATH/$filename" >> "$android_m3u"
                            echo "Debug: Added track to Android playlist: $filename"
                        else
                            echo "Debug: Track file not found: $track_path"
                        fi
                    fi
                done < "$playlist_tracks"
                
                # Check if the Android playlist has content
                android_count=$(grep -v "^#" "$android_m3u" | grep -v "^$" | wc -l | tr -d ' ')
                
                if [ "$android_count" -gt 0 ]; then
                    echo "Pushing Android playlist with $android_count tracks..."
                    adb push "$android_m3u" "$android_playlist"
                    echo "Android playlist created successfully"
                else
                    echo "No valid tracks for Android playlist"
                fi
            fi
        else
            echo "Invalid selection: $selection (out of range)"
        fi
    else
        echo "Invalid selection: $selection (not a number)"
    fi
done

# Sync tracks if requested
if [[ "$android_sync" =~ ^[Yy]$ ]]; then
    echo "Syncing music files to Android device..."
    
    # Create music directory on device
    adb shell "mkdir -p \"$ANDROID_MUSIC_PATH\""
    
    # Get unique tracks and make sure they exist
    sort "$ALL_MUSIC_FILES" | uniq > "$ALL_MUSIC_FILES.unique"
    > "$ALL_MUSIC_FILES.valid"
    
    # Populate valid files list
    echo "Checking valid music files..."
    while IFS= read -r track_path; do
        if [ -n "$track_path" ] && [ -f "$track_path" ]; then
            echo "$track_path" >> "$ALL_MUSIC_FILES.valid"
        fi
    done < "$ALL_MUSIC_FILES.unique"
    
    # Count the total tracks
    total_tracks=$(grep -v "^$" "$ALL_MUSIC_FILES.valid" | wc -l | tr -d ' ')
    echo "Found $total_tracks valid tracks out of $(wc -l < "$ALL_MUSIC_FILES.unique" | tr -d ' ') unique tracks"
    
    if [ "$total_tracks" -eq 0 ]; then
        echo "No valid tracks to sync."
    else
        echo "Found $total_tracks valid tracks to sync"
        echo "Debug: First 3 tracks to sync:"
        head -n 3 "$ALL_MUSIC_FILES.valid"
        
        echo "Starting sync of $total_tracks files..."
        current=0
        
        # Process each track using a different approach
        # Save track paths to an array first
        track_paths=()
        while IFS= read -r line; do
            track_paths+=("$line")
        done < "$ALL_MUSIC_FILES.valid"
        
        echo "Debug: Loaded ${#track_paths[@]} tracks into memory"
        
        # Now process each track in the array
        for track_path in "${track_paths[@]}"; do
            if [ -n "$track_path" ]; then
                current=$((current + 1))
                filename=$(basename "$track_path")
                echo "[$current/$total_tracks] Syncing: $filename"
                
                # More thorough check if file exists on device
                file_exists=$(adb shell "if [ -f \"$ANDROID_MUSIC_PATH/$filename\" ]; then ls -l \"$ANDROID_MUSIC_PATH/$filename\" | awk '{print \$5}'; else echo '0'; fi")
                local_size=$(stat -f%z "$track_path")
                
                echo "  Local size: $local_size bytes"
                echo "  Remote size: $file_exists bytes"
                
                if [ "$file_exists" != "0" ]; then
                    if [ "$file_exists" -eq "$local_size" ]; then
                        echo "  File size matches, skipping..."
                    else
                        echo "  File size mismatch, re-syncing..."
                        adb push "$track_path" "$ANDROID_MUSIC_PATH/$filename"
                    fi
                else
                    echo "  File not found on device, transferring..."
                    if ! adb push "$track_path" "$ANDROID_MUSIC_PATH/$filename"; then
                        echo "  Error transferring file, continuing with next..."
                        continue
                    fi
                fi
                
                # Debug output
                if [ "$current" -eq 1 ]; then
                    echo "Processed first file, continuing..."
                elif [ "$current" -eq 2 ]; then
                    echo "Processed second file, continuing..."
                elif [ "$current" -eq 3 ]; then
                    echo "Processed third file, continuing..."
                elif [ "$current" -eq 4 ]; then
                    echo "Processed fourth file, continuing..."
                elif [ "$current" -eq 5 ]; then
                    echo "Processed fifth file, continuing..."
                fi
            fi
        done
    fi
    
    # Clean up temporary files
    rm -f "$ALL_MUSIC_FILES" "$ALL_MUSIC_FILES.unique" "$ALL_MUSIC_FILES.valid"
fi

echo "All exports complete to $export_dir"
echo "Your selections have been saved. Run this script again to use the same settings."

Last Updated on 28 March 2025

Leave a Comment

Your email address will not be published. Required fields are marked *

en_USEnglish
Scroll to Top