iTunes library import

Coding Critique is the place to post source code for peer review by other members of DevNetwork. Any kind of code can be posted. Code posted does not have to be limited to PHP. All members are invited to contribute constructive criticism with the goal of improving the code. Posted code should include some background information about it and what areas you specifically would like help with.

Popular code excerpts may be moved to "Code Snippets" by the moderators.

Moderator: General Moderators

Post Reply
User avatar
pickle
Briney Mod
Posts: 6445
Joined: Mon Jan 19, 2004 6:11 pm
Location: 53.01N x 112.48W
Contact:

iTunes library import

Post by pickle »

I wrote this class as the first part of a suite I'm building to compare 2 iTunes libraries. This class will open an iTunes library/playlist export file (File->Export, select 'XML') & transfer it into a PHP array. I was going to use PHP5 & SimpleXML, but SimpleXML doesn't duplicate the structure of the file & does so in a pretty much random fashion. So, I'm back to the old way.

Let me know if any of the comments aren't clear/to many comments/to few comments. I'm trying to improve my documentation. Heck, if you can find any optimization, that won't hurt either.

Update: I've just discovered that having an ampersand (&) in the data somewhere will bugger up the xml parser. The string that gets sent to xml_character_data() arrives with the only the text after the ampersand (ie: '(Above & Beyond Mix)' in the XML file gets turned into ' Beyond Mix)'). I've discovered the xml_parse_into_struct() function which I can use to keep that ampersand & drastically cut down on the code. However, it does take about 33% longer to run, so I'd like a fix using this method if possible.

Any ideas anyone? If I don't get any in a couple days, I'll add another post below with the new code.

Code: Select all

<?PHP
/*******************
 * iTunesImport
 * 
 * A class for importing your iTunes library into an array
 * After running, your iTunes tracklist will be stored in $this->tracks
 * 
 * Usage:
 *    $iTunesImport = new iTunesImport('/path/to/iTunes/generated/xml');
 *    $iTunesImport->parse();
 *    $my_XML_as_an_array = $iTunesImport->tracks();
 *  
 * Author: Dylan Anderson
 * Date: February 25, 2007
 * License: http://www.gnu.org/licenses/lgpl.txt Lesser GNU Public License
 * Website: http://nderson.ca 
 * 
 * Copyright: copyright 2007 Dylan Anderson - All Rights Reserved.
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Lesser General Public
 *   License as published by the Free Software Foundation; either
 *   version 2.1 of the License, or any later version.
 *
 *   This library is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *   Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public
 *   License along with this library; if not, write to
 *
 *   The Free Software Foundation, Inc.,
 *   51 Franklin Street,
 *   Fifth Floor,
 *   Boston,
 *   MA  02110-1301  USA
 */       
class iTunesImport
{
  //The file to import: full file-system path
  //ie: /usr/local/xml/library.xml
  var $file = FALSE;
  
  //The array of tracks.  This will be populated after
  //calling $this->parse()
  var $tracks = array();
	
  //The XML parser resource
  var $parser = FALSE;
	
  //Used to keep track of what song is currently being parsed
  //set by xml_start_element()
  //read by xml_character_data()
  //initialized to -1 so that the array is 0-based.
  var $track_index = -1;
  
  //Used to keep track of the parsers location in the file
  //set by xml_start_element(),xml_end_element()
  //read by xml_start_element(),xml_end_element(),xml_character_data()
  var $depth = 0;
  
  //Used to tell xml_character_data what to do with the data it receives
  //set by xml_start_element(),xml_end_element()
  //read by xml_character_data()
  var $data_is_key = FALSE;
  var $data_is_value = FALSE;
  
  //Used by xml_character_data() to match up key-value pairs when inserting
  //data into the array
  //set & read by xml_character_data()
  var $curr_key = FALSE;
  
  /**************************
   * Function: iTunesImport()
   * Purpose: Constructor of iTunesImport class
   * Parameters: @file: a full file system path to the xml file to parse
   *                    ie: /usr/local/xml/library.xml
   */                             	
  function iTunesImport($file)
  {
    if(file_exists($file))
      $this->file = $file;
  }
	
  /**************************
   * Function: parse()
   * Purpose: To parse through $this->file & insert the data found
   *          into the $this->tracks array
   * Parameters: None
   * Returns: TRUE if the file was read in correctly,
   *          FALSE if not
   *          FALSE if no file was found   	 
   * References: Nothing directly, but sets up $this->xml_start_element(),
   *             $this->xml_end_element() & $this->xml_character_data() as
   *             xml parsing functions
   */                  	
  function parse()
  {
    if($this->file)  
    {
      $this->parser = xml_parser_create();
      xml_set_object($this->parser,&$this);	
      xml_set_element_handler($this->parser,'xml_start_element','xml_end_element');
      xml_set_character_data_handler($this->parser,'xml_character_data');
      
      $success = xml_parse($this->parser,file_get_contents($this->file));
      return ($success == 1) ? TRUE : FALSE;
    }
    else
      return FALSE;   	
  }
	
  /**************************
   * Function: xml_start_element()
   * Purpose: This function is called whenever the XML parser comes across an
   *          opening tag.  Depending on the tag, this function updates 
   *          different object variables.  See comments in the switch() for more 
   *          info.	 
   * Parameters: @parser_obj: the parser resource
   *             @element_name: the name of the XML node currently being parsed
   *             @attributes: an array of any attributes the current element has
   *                          indexed by attribute name
   *             Note: This function by definition must accept these three
   *                   arguments even though $parser_obj & $attributes aren't 
   *                   referenced.
   * Returns: Nothing
   * References: $this->depth,
   *             $this->track_index,
   *             $this->data_is_key,
   *             $this->data_is_value
   *
   * Note: By default, 'CASE FOLDING' is turned on.  This uppercases all tags.	 
   */                                   	
  function xml_start_element($parser_obj,$element_name,$attributes)
  {
    switch($element_name)
    {
      //PLIST is the opening tag
      case 'PLIST':
        $this->depth = 1;
        break;
      //DICT is found in a number of levels.  The 'KEY' case acts differently
      //accordingly to which level is currently being iterated through, so this
      //needs to be updated
      case 'DICT':
        $this->depth++;
        break;
      //The KEY tag has multiple uses:
      //  If inside the <DICT> tag that denotes the file list (depth == 3), 
      //  then it means the next song has been reached.
      //  If inside the <DICT> tag that denotes the track properties list
      //  (depth == 4), then it means the next data retrieved will be a key
      //  value to use in $this->tracks 
      case 'KEY':
        if($this->depth == 3)
          $this->track_index++;
        else if($this->depth == 4)
          $this->data_is_key = TRUE;
        break;
      
      //If tag is any of these & inside the <DICT> tag that denotes the track 
      //properties list, then the next data retrieved will be a value to insert
      //into $this->tracks
      case 'STRING':
      case 'DATE':
      case 'INTEGER':
        if($this->depth == 4)
          $this->data_is_value = TRUE;
        break;
    }
  }
  
  /**************************
   * Function: xml_end_element()
   * Purpose: This function is called whenever the XML parser comes across a 
   *          closing XML tag. Depending on the tag, this function updates 
   *          different object variables.  See comments in the switch() for more 
   *          info.  	 
   * Parameters: @parser_obj: the parser resource
   *             @element_name: the name of the XML node currently being parsed
   *             Note: This function by definition must accept these two
   *                   arguments even though $parser_obj isn't referenced.
   * Returns: Nothing
   * References: $this->depth,
   *             $this->track_index,
   *             $this->data_is_key,
   *             $this->data_is_value
   */ 
  function xml_end_element($parser_obj,$element_name)
  {
    switch($element_name)
    {
      //a closing PLIST tag means we're done with the file
      case 'PLIST':
        $this->depth = 0;
        break;
      //DICT occurs on many levels, so we need to update $this->depth to keep
      //track of where we are
      case 'DICT':
        $this->depth--;
        break;
      //a closing KEY tag means the next data retrieved cannot be key data
      case 'KEY':
        $this->data_is_key = FALSE;
        break;
      //these closing tags mean the next data retrieved is not necessarily
      //a value
      case 'STRING':
      case 'DATE':
      case 'INTEGER':
        $this->data_is_value = FALSE;
        break;
    }
  }
  
  /**************************
   * Function: xml_character_data()
   * Purpose: This function is called whenever data between an opening and
   *          closing tag is found & when that data isn't other opening & closing
   *          tags.
   *          This function takes the data that is found & either stores it in
   *          $this->curr_key for use when the relevant data is found, or stores
   *          the said relevant data in $this->tracks               	 
   * Parameters: @parser_obj: the parser resource
   *             @data: the data (usually text) that was found
   *             Note: This function by definition must accept these two
   *                   arguments even though $parser_obj isn't referenced.
   * Returns: Nothing
   * References: $this->depth,
   *             $this->track_index,
   *             $this->data_is_key,
   *             $this->data_is_value
   */ 
  function xml_character_data($parser_obj,$data)
  {
    //if this data is key data, store it for when we next come across value data
    if($this->data_is_key)
      $this->curr_key = $data;
    //if this data is value data, use the current key to store it in 
    //$this->tracks
    else if($this->data_is_value)
      $this->tracks[$this->track_index][$this->curr_key] = $data;
  }
}
?>

Usage is in the comments at the head of the code.
Last edited by pickle on Mon Feb 26, 2007 1:58 am, edited 1 time in total.
Real programmers don't comment their code. If it was hard to write, it should be hard to understand.
User avatar
Kieran Huggins
DevNet Master
Posts: 3635
Joined: Wed Dec 06, 2006 4:14 pm
Location: Toronto, Canada
Contact:

Post by Kieran Huggins »

Have you tried the (php5) DOM XML functions instead of SimpleXML?

If you like, you can import the XML with the DOM functions then translate to simpleXML with the built-in functions.
User avatar
pickle
Briney Mod
Posts: 6445
Joined: Mon Jan 19, 2004 6:11 pm
Location: 53.01N x 112.48W
Contact:

Post by pickle »

Nope, I haven't ever seen those. Now that the class is written, I doubt I'll rewrite it. I'll keep that DOM stuff in mind though if I need to do any XML work in the future. Thanks for the heads-up.
Real programmers don't comment their code. If it was hard to write, it should be hard to understand.
Post Reply