if its too loud, turn it down

Monday, June 29, 2009

Use perl and applescript to keep playlists synced with iPhone/iPod

I am always adding to and editing iTunes playlists, particularly the more oft-listened ones like my "punk", "ambient", and "programs & podcasts" playlists. Since I listen to alot of stuff on my iPhone, I naturally want these playlists always available and up-to-date on my device. I can't tell you how many times I plugged in my iPhone to my car stereo and found, to my disappointment, that I forgot to drag-and-drop the most recent version of a playlist to the device.

Why not just set the iPhone to automatically update? Because I want the finite control that manually managing affords me. (UPDATE: to all you haters out there wondering why I don't just use auto-sync...because it "locks" the iPod so you can't manually manage any playlists. I want to manually manage most things and just have selected playlists update automatically. If you don't like this script don't use it!)

So I created a script that updates a specified list of playlists to all attached iPods/iPhones. It can run on a schedule (with cron), so I never have to worry that my iPhone won't have the most recent versions of my playlists!

Features
  • Adds specified playlists to all iPods attached to your mac
  • Will skip the playlist if there's not enough space on an attached device
  • Retains the order in which the tracks appear on the library playlist
  • Outputs useful debug info
Usage

First, the script requires the iPod/iPhone be set to "Manually manage music and videos". I named the script "updateipod.pl" and placed in my user's "bin" directory. It's recommended you specify the playlists that you want to sync in the @playlists array at the top of the script:
my @playlists = (

 "jamz",
 "0pods",
 "punk best",
 "stoner best"
 
 );
After chmod-ing the script to 755 just invoke it as you would any script:
/Users/user/bin/updateipod.pl
Optionally, you can specify the names of the playlists as arguments on the command line. Useful for one-off updating. Note that if playlists are passed as command-line arguments they will be processed instead of whatever is specified in the @playlists array at the top of the script. Make sure to enclose playlist names in quotes if the playlists have spaces or special characters. For example, here's how I'd send three playlists to my connected devices on the command line:
/User/user/bin/updateipod.pl "michael best" 0pods "80's by rory"
I schedule it to run hourly with cron:
0 * * * * /Users/user/bin/updateipod.pl > /dev/null 2>&1
But you can do this if you want more output:
0 * * * * /Users/user/bin/updateipod.pl >> /Users/user/Desktop/ipod_update_log.txt 2>&1
That's it! Here's the script. Click "expand source" below to view:
#!/usr/bin/perl

# 2009-07-05
# Auto-sync specified playlists to all attached iPods/iPhones
# Specify playlist names in the @playlist array OR enter them
# as arguments (in single quotes) on the command line like this:
#
# $ ./updateipod.pl "michael best" "0pods" "80's by rory"
#
# I run this hourly with the cronjob:
#
# 0 * * * * /Users/user/bin/updateipod.pl > /dev/null 2>&1
# 
# use something like this if you want to see the output for each run:
# 
# 0 * * * * /Users/user/bin/updateipod.pl >> /Users/user/Desktop/ipod_update_log.txt 2>&1
#
# some applescript in this script was borrowed from "Selected Playlists To iPod v2.1" at the very excellent http://www.dougscripts.com/

use strict;
use POSIX qw(strftime);

my @playlists = (

 "jamz",
 "0pods",
 "punk best",
 "stoner best"
 
 );
 
# if there are playlists entered as arguments on the cmd line
# those are gonna trump whatever is specified above
if ($ARGV[0]) {
 @playlists = @ARGV;
}

my $time = strftime( "%Y-%m-%d %H:%M:%S",localtime(time));
print "syncing of playlists started at $time\n";

# check if itunes is running, start if not
checkIfRunning();

# get the sources of any attached ipods.  exit if there are none attached.
my ($id_ref,$name_ref) = getiPodSources();
my @ipod_ids = @$id_ref;
my @ipod_names = @$name_ref;
my $devices = @ipod_ids;

# exit if we have no attached ipods
if (!$ipod_ids[0]) {
 print "no manually managed iPods detected, exiting...\n";
 exit;
}

# make sure the playlists exist in the itunes lib.
my @confirmed_playlists = ();
foreach my $playlist (@playlists) {
 my $exists = checkPlaylistExists($playlist);
 if($exists) {
  push(@confirmed_playlists,$playlist);
 } else {
  print "\"$playlist\" is not a valid playlist in the iTunes library, skipping...\n";
 }
}

# make sure we have at least one valid playlist
if (!$confirmed_playlists[0]) {
 print "no valid playlists entered, exiting...\n";
 exit;
}

# loop thru our ipods first, then within that,
# loop thru our playlists array and sync them to each ipod
for (my $i=0;$i<$devices;$i++) {
 foreach my $playlist (@confirmed_playlists) {
  print "telling itunes to sync playlist \"$playlist\" to $ipod_names[$i]\n";
  my $output = syncPlaylist($ipod_ids[$i],$ipod_names[$i],$playlist);
  if ($output ne '1') {
   print "${output}, skipping...\n";
   next;
  }
 } # end loop thru playlists
} # end for thru ipods

print "done.\n\n";
exit;

#################################################################################################
###################                      SUBROUTINES                   #####################
#################################################################################################

sub stripNonAlph {
 # strips non-alphanumeric characters from strings
 
 my($text) = shift;
 $text =~ s/([^0-9a-zA-z ])//g;
 return ($text);

} # end sub strip non alph

sub checkIfRunning {
 # checks if iTunes is running
 # starts it if not
 
 my $start=`/usr/bin/osascript <<END
 --start the program if necessary
 set isRunning to 0
 tell application "System Events"
  set isRunning to ((application processes whose (name is equal to "iTunes")) count)
 end tell
 if isRunning is 0 then
  tell application "iTunes"
   activate
  end tell
 end if
 `;
 
} # end sub check if running

sub getiPodSources {
 # gets the system source IDs of any attached ipods
 # (assumes there's more than 1 in case there actually are)
 # returns in semicolon-delimited list
 
 my $source=`/usr/bin/osascript <<END
 tell application "iTunes"
  set validiPods to {}
  set myPods to ""
  set myPodsNames to ""
  set myPodReturn to ""
  set ipodSources to (get every source whose kind is iPod)
  if ipodSources is {} then
   return 0
  end if
  
  repeat with s in ipodSources
   try -- can't make a playlist on an iPod set to sync automatically, so this will fail.  
    set tp to make new user playlist at s with properties {name:("p)" & my get_temp_name() & "-^")}
    delete tp
    set end of validiPods to s
   end try
  end repeat
  
  if validiPods is {} then
   return 0
  end if
  
  repeat with i from 1 to (length of validiPods)
   set myPods to myPods & ";" & id of item i of ipodSources
   set myPodsNames to myPodsNames & ";" & name of item i of ipodSources
  end repeat
  
  set myPodsReturn to myPods & "|" & myPodsNames
  return myPodsReturn
  
 end tell
 to get_temp_name()
  return (get time of (get current date)) as text
 end get_temp_name
 END`;
 
 chomp($source);
 $source =~ /^;(.*?)\|;(.*?)$/;
 my ($id,$name) = ($1,$2);
 my @ids = split(/;/,$id);
 my @names = split(/;/,$name);
 return (\@ids,\@names);
 
} # end sub getiPodSources

sub checkPlaylistExists {
 # checks if the itunes playlist exists
 
 my $playlist = shift;
 my $exists=`/usr/bin/osascript <<END
 tell application "iTunes"
  if (exists playlist "$playlist") then
   return 1
  else
   return 0
  end if
 end tell
 END`;
 $exists = &stripNonAlph($exists);
 return ($exists);
 
} # end check if playlist exist

sub syncPlaylist {
 # takes the id of the source (ipod) and the name of the playlist to sync as arguments
 
 my ($ipod_id,$ipod_name,$playlist) = @_;
 my $sync = `/usr/bin/osascript <<END

 tell application "iTunes"
  set playlistName to "$playlist"
  set main_lib to library playlist 1
  set ipod_src to item 1 of (get every source whose id is $ipod_id)
  set ipod_lib to library playlist 1 of ipod_src
  set playlistSize to 0
  set iPodFreeSpace to free space of ipod_src
  set copySuccess to 0
  set playlistSuccess to 0
  
  set myPlaylists to (get every user playlist)
  repeat with thisPlaylist in myPlaylists
   if name of thisPlaylist is playlistName then
    
    --loop thru the playlist to determine if there is enough
    --free space on the ipod for any songs not already on it
    set eop to index of last track of thisPlaylist
    repeat with i from 1 to eop
     --determine if the song is already on ipod lib
     set i_track to track i of thisPlaylist
     if (get class of i_track) is file track then
      tell i_track to set {nom, art, alb, siz, kin, tim, pid} to {get name, get artist, get album, get size, get kind, get time, get persistent ID}
      try
       -- is track already in iPod library?  setting this will fail if not
       set new_i_track to (some track of ipod_lib whose name is nom and artist is art and album is alb and size is siz and kind is kin and time is tim)
      on error -- if not, add size of track to running total of tracks not on ipod
       set playlistSize to playlistSize + siz
      end try
     end if
    end repeat --thru selected playlist
    
    if free space of ipod_src is greater than playlistSize then
     -- establish iPod playlist
     try
      --does the iPod playlist already exist?  if so we delete it.
      --why?  because if we add additional tracks to the end of it, the track list will no be in the 
      --same order as they are on our iTunes playlist
      set new_ipodplaylist to user playlist playlistName of ipod_src
      delete new_ipodplaylist
      set new_ipodplaylist to (make new user playlist at ipod_src with properties {name:playlistName})
     on error
      -- if not, create the new iPod playlist
      set new_ipodplaylist to (make new user playlist at ipod_src with properties {name:playlistName})
     end try
     
     --I'm not checking to see if the tracks already exist on the ipod before copying
     --because it *appears* itunes won't duplicate them.  maybe this is a new feature
     try
      duplicate (every track of thisPlaylist) to ipod_lib
      set copySuccess to 1
     on error
      return "error copying tracks to iPod $ipod_name"
      set copySuccess to 0
     end try
     try
      duplicate (every track of thisPlaylist) to new_ipodplaylist
      set playlistSuccess to 1
     on error
      return "error duplicating track listing to playlist $playlist on iPod $ipod_name"
      set playlistSuccess to 0
     end try
     
     if copySuccess is 1 and playlistSuccess is 1 
      return 1
     end if
     
    else
     return "not enough free space on iPod $ipod_name to copy all tracks on $playlist"
    end if
    
   end if --current playlist = selected playlist
  end repeat
  
 end tell
 `;
 $sync = &stripNonAlph($sync);
 return ($sync);
 
} # end sub syncPlaylist

__END__
Some of the applescript was borrowed from this script at the very awesome Doug's Applescripts for iTunes.

No comments:

Post a Comment