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:
- Extracts playlists directly from your Mac’s Music app (iTunes)
- Exports them as standard M3U playlist files
- Optionally syncs both the playlists and actual music files to your Android device
- Remembers your preferences for next time
- Only transfers files that aren’t already on your device (to save time)
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:
- A Mac with the Music app (formerly iTunes)
- An Android device
- Android Debug Bridge (ADB) installed
Installation Steps:
- Install ADB if you don’t have it already. The easiest way is to use Homebrew:
brew install android-platform-tools - 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”
- 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)
- Make the script executable:
chmod +x ~/music_playlist_sync.sh
Using the Script
To run the script:
~/music_playlist_sync.shThe first time you run it, the script will:
- Display all available playlists from your Music app
- Ask which playlist(s) you want to export (you can select multiple with comma-separated numbers)
- Ask where you want to save the exported playlists
- Ask if you want to sync to an Android device
- 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:
- Install a media rescanning app from Google Play Store, like “Rescan Media” or “SD Rescan”
- Run the app to trigger a media library rescan
- Sometimes a phone restart does the trick
Samsung Phones
On Samsung devices, you may need to manually import the playlists:
- Open the Samsung Music app
- Go to Settings > Playlists > Import Playlists
- 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 devicesin 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


