Backstory (if your not interested then skip to the meat):
A very bad tech karma week ended in the death of my Windows XP desktop. Everything was backed up, but I was not looking forward to hours installing xp, office, iTunes, etc…. Like many I have recently purchased a shiny new MacBook, and like many I now use it for almost all of my desktop duties. So instead of rebuilding XP, I’ve decided to migrate my desktop life to the Mac.
A number of things need to be migrated, some easy, some much more difficult (I’ll probably have future posts chronicling some of migration tasks). My priorities being what they are, the first and most critical migration is iTunes. The problem is that after a couple hours of googling I could not find a solution that made me happy. Wht would make me happy?
Requirements For iTunes Migration:
Apart from migrating the music itself there is a lot of metadata that I wanted to preserve, specifically I wanted the last played date, play count, skip count, last skipped date, date added and rating. In addition, I like the folder structure of my music files, so if possible I would like to maintain it.
What I Found:
A little googling turned up some helpful links, but nothing that completely satisfied my needs. I found applescript snippets that showed how to programmatically update much of the metadata that I wanted to preserve (with one very significant exception). I also found details on importing a music library using iTunes (unfortunately it does not import all of the metadata).
What I ended up doing (i.e. the meat):
Ultimately I decided to cobble together my own solution. So here’s what I did.
- Get the old library XML file
- Place your music files where they need to be
- Update the old XML library file to point to the new XML files
- Import the music
- Import the metadata
Please,… before you go any further keep the following in mind. The following steps assume basic programming skills (specifically applescript and PHP). Review the code and backup your music and system before you do anything.
1. Get the old XML library file
There are two data files that iTunes uses to manage its data, a binary library file and an XML library file. The XML file is what you want. Under XP this file is typically located in c:\Documents and Settings\username\My Documents\My Music\iTunes\iTunes Music Library.xml .
2. Place your music files where they need to be
Place your music files where you want them. Figure out if you want your music on an internal drive or an external drive. Decide if you want iTunes to manage your file locations or if you want to manage your music folder manually. I decided to place my music on an external drive. Also, I am not yet a complete fan of letting iTunes move my music files around, so I turn that option off and make iTunes leave my music files where I put them.
3. Update the file paths in the XML file
The next step was to modify the paths in the XML library file so that they point to the new music path. This should be pretty straight forward. Open up the XML file in whatever text editor you like, do a simple search/replace which replaces the old path with the new location.
4. Import the music
This is one thing that iTunes will do for you (assuming that step 3 worked out). Open iTunes on your Mac. Go to File >> Import. Then in the file selection dialog select the updated version of the old XML library file. If the paths are correct iTunes should import your old music info. If you told iTunes to handle where your files are placed, it should also move your music files to the music location specified in the iTunes preferences.
5. Import the MetaData
If you don’t need to import metadata not included in the import performed in step 4 then you are done. However, as stated previously there are some pieces of data that I wanted preserved namely skipped date, skip count, last played, play count and date added. I wrote two scripts to do the job. The first is a PHP script that extracts the metadata from the XML Library file. The PHP script then generates an intermediate shell script that calls an applescript which tells iTunes to update its library data.
There are a couple important caveats/limitations. First, the applescript must search for the tracks that will be updated. But since there is no known unique identifier guaranteed to be consistent between the old XML library file and the new iTunes library data an alternative is needed. So, these scripts use the following fields to uniquely identify a track; Track Name, Track Artist, Track Album Name, Track Number, Track Size.
These fields in concert should be unique. However, in the off chance that there are still duplicates, the applescript wil output a message saying that a unique match could not be found. A second limitation is that at the time of this blog entry Apple has not provided a way to update the Date Added property for tracks in iTunes. So for now the original add date information will be appended to the track description property. This is obviously not what I really want, but at least the metadata is there, and if/when Apple allows this property to be update or when there is a reasonable work around the data will be there.
So, with no more adieu, here are the two scripts.
<?php
// parse the command line and get the input filename
if ($argc != 2) {
die("Please specify the input file and nothing else!");
}
$filename = $argv[1];
// get the XML object
$xmlData = simplexml_load_file($filename);
// iterate through the children of dict until we find the key with the value tracks
$i = -1;
$indexOfTracksKey = -1;
$topDictElements = $xmlData->dict->children();
foreach ($topDictElements as $ele) {
$i++;
if ($ele->getName() != "key") {
continue;
} else if ((string) $ele == "Tracks") {
// get the next element should be a dict
$indexOfTracksKey = $i;
break;
}
}
// the element that contains all the tracks elements
$xmlTracksDictEle = $topDictElements[$indexOfTracksKey + 1];
echo "# The number of tracks in the library is : " . count($xmlTracksDictEle->children()) / 2 . "\n";
// iterate over every track
$i = 0;
foreach ($xmlTracksDictEle->xpath("dict") as $track) {
// get the track info
$trackinfo = GetTrackInfo($track);
// if there is something to update then add the call to the applescript to the output
if ($trackinfo['Play Count'] != 0 || $trackinfo['Skip Count'] != 0) {
printf('osascript iTunesUpdateMeta.applescript %s %s %s "%s" "%s" "%s" "%s" "%s" "%s" "%s"' .
"\n", CleanParam($trackinfo['Name']), CleanParam($trackinfo['Artist']),
CleanParam($trackinfo['Album']), $trackinfo['Track Number'], $trackinfo['Size'],
$trackinfo['Play Count'], $trackinfo['Play Date UTC'], $trackinfo['Date Added'],
$trackinfo['Skip Count'], $trackinfo['Skip Date']);
} else {
// nothing to update,... then report that we are skipping this track
printf('# Skipping (%s)"%s"' . "\n", $trackinfo['Track ID'], $trackinfo['Name']);
}
}
// retrieve all track info form a track element
function GetTrackInfo($trackEle) {
// get all child elements
$childrenEles = $trackEle->children();
$i = 0;
$ret = array();
// iterate through all track element children
while ($i < count($childrenEles)) {
// get the property key and value
$key = (string) $childrenEles[$i++];
$value = (string) $childrenEles[$i];
// if there is no value part to the next element then use the element name,...
// it is usually the case for true/false values
if (strlen($value) == 0) {
$value = $childrenEles[$i] ->getName();
} else if (($key == "Play Date UTC") || ($key == "Date Added") || ($key == "Skip Date")) {
// if the value is a key then reformat the data to something that is applescript friendly
$value = date_create($value)->format("m-d-Y H:i:s");
}
// add to hashtable
$ret[$key] = $value;
$i++;
}
// Fill out the information by adding any missing values that we need
if (! array_key_exists("Skip Count", $ret)) {
$ret['Skip Count'] = 0;
}
if (! array_key_exists("Skip Date", $ret)) {
$ret['Skip Date'] = "";
}
if (! array_key_exists("Play Count", $ret)) {
$ret['Play Count'] = 0;
}
if (! array_key_exists("Play Date UTC", $ret)) {
$ret['Play Date UTC'] = "";
}
if (!array_key_exists("Album", $ret)) {
$ret['Album'] = "";
}
if (!array_key_exists("Artist", $ret)) {
$ret['Artist'] = "";
}
if (!array_key_exists("Track Number", $ret)) {
$ret['Track Number'] = "";
}
return $ret;
}
// make sure that special characters are escaped
function CleanParam($param) {
$ret = $param;
// if the parameter does not contain a single quote,... then just wrap it in single quotes
if (stripos($ret, "'") === false) {
$ret = "'" . $param . "'";
} else {
// escape special chars
$ret = str_replace('\\', '\\\\', $param);
$ret = str_replace('$', '\\$', $param);
$ret = str_replace("`", "\`", $param);
$ret = str_replace('"', '\\"', $param);
$ret = str_replace('`', '\\`', $param);
$ret = '"' . $ret . '"';
}
return $ret;
}
?>
|
iTunesUpdateMeta.applescript
(*
This applescript is meant to be run on the commandline using osascript.
It is used to update the metadata for a single track in an iTunes
library. The parametes include search values used to identify the
track and metadata which will be updated.
The input parameters are in order
name - The name of the iTunes track (search)
artist - The track artist (search)
album - The album on which the track appears (search)
track number - the track number on the track's album (search)
track size - The size of the audio file for the track, in bytes (search)
played count - The number of times that the track has been
played (for update)
last_played_date - The last date that the track was played (for update)
date_added - The date on which the track was added to the
library (for update)
skipped_count - The number of times the track has been
skipped (for update)
last_skipped_date - The date of the last time the track
was skipped (for update)
All parameters are required. An empty parameter is acceptable.
Notes:
For now,... date added is not updatable via the iTunes object model.
So the desired date added value is appended to the end of the
description property of the track. The information will be of the
form...
<true_add_date>Saturday, December 23, 2006 7:17:17 AM</true_add_date>
If no track matching the search criteria is found, or if multiple
tracks are found, then no update will be done and a
message displaying the search problem will be output to stdout.
The skipped count and play count currently stored in the library
will be incremented by the amount specified in the command line.
The assumption is that the data being used to update the iTunes
Library comes from a previously used and no longer in use library.
Therefore if the skipped count or played count is greater than 0,
it is assumed that the last played or skipped date for the track
will be more recent than it was in the previous library and no
update to these fields will be performed.
*)
on run argv
-- make sure there are exactly 10 args
if (count of argv) is not 10 then
return (count of argv) & " arguments given!" & Usage()
end if
-- PARSE THE COMMANDLINE PARAMETERS
set _trackname to item 1 of argv
set _artist to item 2 of argv
set _album to item 3 of argv
set _tracknum to item 4 of argv
set _tracksize to item 5 of argv as number
set _playcount to item 6 of argv as number
if _playcount is greater than 0 then
set _lastPlayed to date (item 7 of argv)
else
set _lastPlayed to ""
end if
set _dateadded to date (item 8 of argv)
set _skipcount to item 9 of argv as number
if _skipcount is greater than 0 then
set _skipdate to date (item 10 of argv)
else
set _skipdate to ""
end if
-- generate a string for reporting the parameters
set parameters to _trackname & ":" & _artist & ":" & _album & ":" & ¬
_tracknum & ":" & _tracksize & " -> " & _playcount & ":" & ¬
_lastPlayed & ":" & _dateadded & ":" & _skipcount & ":" & _skipdate
tell application "iTunes"
-- get the iTunes Library Playlist
set iTunesLibrary to library playlist 1
-- use the search method to quickly find tracks with the search name
set tracks_matched_by_name to (search iTunesLibrary for _trackname only songs)
-- iterate through the returned tracks to find the track that matches the rest of the criteria
set successfullyFound to false
repeat with maybet in tracks_matched_by_name
-- match the track number,... since some of my tracks don't have
-- track numbers, if no track number exists in the search or
-- matched track, then handle that as well
set tracknummatch to (length of _tracknum > 0 and ¬
_tracknum as number is equal to track number of maybet) or ¬
(length of _tracknum is equal to 0 and track number of maybet is missing value)
-- now compare criteria
if (artist of maybet is _artist) and (album of maybet is _album) and tracknummatch then
-- if this is the second successful match then we may have
-- a problem,... lets try and resolve with track size
if successfullyFound is true then
-- use the track that matches the size,...
-- if the current track matches the tracksize and the previously found
-- track does not then use the current track if neither track matches the
-- tracksize, or if both do then we have a problem,... otherwise we are
-- good because the only other possibility is that the previously found track
-- matches and the new one does not,... so use the previously found track
if (size of maybet = _tracksize) and not (_tracksize = size of tracktofix) then
set tracktofix to maybet
else if (not (size of maybet = _tracksize) and not (_tracksize = size of tracktofix)) or ¬
((size of maybet = _tracksize) and (_tracksize = size of tracktofix)) then
return "Multiple possible files found for ::" & parameters
end if
else
-- looks like ths is the first successful math, so set some variables
set successfullyFound to true
set tracktofix to maybet
end if
end if
end repeat
-- if we found a unique match then perform the updates
if successfullyFound is true then
-- Handle played count updates
if _playcount is greater than 0 then
if (played count of tracktofix is greater than 0) then
-- increment the played count if there is already a value
set played count of tracktofix to (_playcount + (played count of tracktofix))
else
-- set the played count and last played date
set played date of tracktofix to _lastPlayed
set played count of tracktofix to _playcount
end if
end if
-- Handle skipped count,.. everything here is similar to how play count updates are handled
if _skipcount is greater than 0 then
if (skipped count of tracktofix is greater than 0) then
set skipped count of tracktofix to (_skipcount + (skipped count of tracktofix))
else
set skipped count of tracktofix to _skipcount
set skipped date of tracktofix to _skipdate
end if
end if
-- lastly take care of the add date by appending to the description property
set description of tracktofix to (description of tracktofix) & "
" & ¬
_dateadded & ""
return "UPDATED (" & database ID of tracktofix & ")::" & parameters
else
return "NOT FOUND ::" & parameters
end if
end tell
end run
-- Command line Usage
on Usage()
return "
Usage is: <trackname> <artist> <album> <track_number> <track_size> " & ¬
"<played_count> <last_played_date> <date_added> " & ¬
"<skipped_count> <last_skipped_date>"
end Usage
|