Version 8 of itunes music player

Updated 2005-10-30 16:40:57

Itunes is the music management software from Apple [L1 ]. It can be used to manage music files, podcasts, and now videos. On windows, there is also a COM interface. The documentation for this is available via the itunes sdk [L2 ], as well as samples in javascript.


<REMARK-JK:Oct2005 > On Mac OS X, you can control iTunes via AppleScript using the Tclapplescript extension, or you can even send raw apple events using the TclAE extension. You can pretty much control everything in this way, playing specific songs, managing playlists, and so on.

For an example of this, implementing an iTunes controller inside Alpha, see http://mat.uab.es/~kock/alpha-tcl/iTunesController.tcl.gz (Recall that Alpha is an extremely powerful editor, written mostly in Tcl -- it's really paradise for Tcl scripters!)

The Windows example below (the initial content of this page, by an anonymous author) implements a global hotkey. To do this in OSX, you should probably have a look at the hotkey example provided by DAS using ffidl. <END-OF-REMARK-BY-JK>


The examples below really only make sense on Windows.


The setup: with geoshell [L3 ], hotkeys can be (fairly) easily created to do things like control the volume of the machine, mute, etc. These hotkeys are global hotkeys since they do not depend on any particular application to have focus. itunes for windows has some hotkeys to handle volume, pause, etc, but they are not global. I prefer to keep itunes minimised and peeking out from the system tray, so giving focus to itunes can be problematic at times. So, I wrote a series of tcl scripts to control itunes via COM.

(for those without geoshell, a short cut of these tcl scripts can be used to create a hotkey. twapi [L4 ] can be used to create shortcuts as well. An example of this here [L5 ].)

First, pause/play. When run, this script will pause and unpause itunes. This is very sensitive to context, though: whatever item the cursor is on is what gets played.

  package require tcom

  set iTunes [::tcom::ref createobject "iTunes.Application"]

  $iTunes PlayPause

  exit

Simple enough. Next, I create a hotkey for geoshell. When pressing control+window+p, the following command gets executed

  "c:\tcl\bin\wish.exe" "C:\Documents and Settings\wl\batch\itunes_pause_play.tcl"

and all seems good.

To expand on this, I next create a pair of tcl scripts to control the volume

  package require tcom

  set interval -2
  set iTunes [::tcom::ref createobject "iTunes.Application"]

  set vol [$iTunes SoundVolume]

  if {[incr vol $interval] <= 0} {
      set vol 0
  }

  $iTunes SoundVolume $vol

  exit

and

  package require tcom

  set interval 2
  set iTunes [::tcom::ref createobject "iTunes.Application"]

  set vol [$iTunes SoundVolume]

  if {[incr vol $interval] >= 100} {
      set vol 100
  }

  $iTunes SoundVolume $vol

  exit

I create hotkeys for control+window+down to call the first script, and control+window+up to call the second script.


After playing around with the COM and itunes a bit more, I wanted an easier way testing these ideas out. So, I created a little server to control itunes. A person telnets to port 9630 and issues a series of commands to control itunes. The interface is a bit clunky, though. The point of this isn't really to create a networked version of itunes, although adding something like Skype [L6 ] could be really fun to play with.... I had some weird crashes when multiple people were using this, so I'm enforcing a one user at a time rule.

Is this longer than appropriate for a wiki addition?

First, a sample session of this:

  > blah
  you got nothing. Try:
  info album | artist | bpm | coment | kind | name | time
  pause
  playlist kind | list | name | tracks [ n ]
  volume [ n ]
  quit | exit
  quit | exit

  > info name
  sleepy sunday show #20
  > info kind
  MPEG audio file
  > playlist tracks
  1 sleepy sunday show #20
  2 tartanpodcast #62
  3 tartanpodcast #63
  4 tp - finniston live!
  5 tp64 - tartanpodcast #64; the marathon
  6 tpsss21 - sleepy sunday show #21
  > playlist track 5
  Playing track 5 -- tp64 - tartanpodcast #64; the marathon
  > playlist list
  9 playlists
          1 Library
                  923 tracks
          2 Party Shuffle
                  21 tracks
          3 all_songs
                  916 tracks
          4 Recently Added
                  4 tracks
          5 Recently Played
                  213 tracks
          6 spods
                  6 tracks
          7 super_shuffle
                  150 tracks
          8 Podcasts
                  6 tracks
          9 stream
                  8 tracks

  > playlist list 2
  Playing first track of playlist 2 (Party Shuffle)
  > info album
  Trailer Park
  > info name
  Tangent
  > info artist
  Beth Orton
  >

... and the actual code below. I didn't do a lot of error checking. One thing I know doesn't work is if nothing is playing, the track commands will usually (always?) fail. And of course, there's a ton of things I'd like to add to this ... over time, of course.

  package require tcom

  package require textutil
  catch { namespace import textutil::* }

  namespace eval iTunesServer {

      namespace eval V {
          # lock -- only one access
          variable lock FALSE

          # port to attach to
          variable port 9630

          # socket to listen on
          variable sid

          # the client sid (singleton for now)
          variable cid ""

          # the com object
          variable itunes NOTDEF

          # path to itunes
          variable iTunes_app [file join $::env(PROGRAMFILES) iTunes iTunes.exe]

          # list of available functions (register here)
          # dictionary of function and help message
          variable function_list
          array set function_list [list]

          # enumeration: types of source
          variable source_kind  [list "unknown" "library" "ipod" \
            "audio cd" "mp3 cd" "device" "radio tuner" "shared library"]
      }
  }

  proc iTunesServer::lock {} {
      if {! $V::lock} {
          set V::lock TRUE
      } else {
          error "already locked"
      }
  }

  proc iTunesServer::unlock {} {
      if {$V::lock} {
          set V::lock FALSE
      }
  }

  proc iTunesServer::getItunes {} {
      # this may start up itunes...
      if {! [valid]}  {

          set V::itunes [::tcom::ref createobject "iTunes.Application"]

          if {! [valid]} {
              error "could not attach to itunes"
          }
      }
  }

  proc iTunesServer::start {} {
      getItunes
      listen
  }

  proc iTunesServer::valid {} {
      set ret false
      if {! [string match "NOTDEF" $V::itunes]} {
          set ret true
      }
      return $ret
  }

  proc iTunesServer::register {func msg} {
      # registers a function by taking the name
      # and the help message
      if {! [info exists V::function_list($func)]} {
          set V::function_list($func) $msg
      } else {
          set ret "function \"$func\" defined twice. First usage:\n [set V::function_list($func)]\n"
          append ret "Second usage:\n$msg"
          error $ret
      }
  }

  proc iTunesServer::listen {} {
      # start the listener
      set V::sid [socket -server [namespace current]::accept $V::port]
  }

  proc iTunesServer::output {str} {
      if {[string match "" $V::cid]} {
          catch {puts -nonewline $str}
      } else {
          catch {puts -nonewline $V::cid $str}
      }
  }

  proc iTunesServer::accept {cid ip port} {

      if {[catch {lock}]} {
          catch {
              puts $cid "Sorry, only one user at a time."
              close $cid
          }
          return
      }

      set V::cid $cid
      fconfigure $cid -buffering line -blocking 0

      output "> "
      flush $cid
      fileevent $cid readable [namespace current]::command
  }

  proc iTunesServer::command {} {
      if {[eof $V::cid] || [catch {set line [gets $V::cid]}]} {

          catch {
              close $V::cid
              set V::cid ""
              unlock
          }

      } elseif {[fblocked $V::cid]} {

          return

      } else {

          if {[catch {command_parse $line} ret]} {
              output "An error: $ret\n"
          } else {
              if {! [string match "" $ret]} {
                  output "$ret\n"
              }
          }
          output "> "
          catch {flush $V::cid}
      }
  }

  proc iTunesServer::command_parse line {
      set cmd [splitx $line]
      set len [llength $line]
      set indx 0
      set ret {}

      if {$len > 0 } {
          switch -- [lindex $cmd 0] {
              pause {
                  set ret [PlayPause]
                  ## return the status...
              }

              volume {
                  set ret [Volume [lindex $cmd 1]]
              }

              info {
                  set ret [Info [lindex $cmd 1]]
              }

              playlist {
                  set ret [Playlist [lrange $cmd 1 end]]
              }

              quit  -
              exit {
                  client_quit
              }
              default {
                  set ret [command_usage]
              }
          }
      } else {
          set ret [command_usage]
      }

      return $ret
  }

  iTunesServer::register quit "quit | exit"
  iTunesServer::register exit "quit | exit"
  proc iTunesServer::client_quit {} {
      catch {
          close $V::cid
          set V::cid ""
          unlock
      }
  }

  proc iTunesServer::command_usage {} {

      set ret "you got nothing. Try:\n"
      foreach f [lsort [array names V::function_list]] {
          append ret "[set V::function_list($f)]\n"
      }
      return $ret
  }

  iTunesServer::register "Playlist" "playlist kind | list | name | tracks \[ n \]"
  proc iTunesServer::Playlist {val} {

      if {! [valid]} return

      set ret ""
      set len [llength $val]
      set playlist [$V::itunes CurrentPlaylist]

      set cmd [lindex $val 0]
      switch -- $cmd  {
          kind {
              set ret [$playlist Kind]
          }
          list {
              switch -- $len {
                  1 {
                      set sources [$V::itunes Sources]

                      # XXX find the library source
                      set playlists [[$sources Item 1] Playlists]

                      # go through the playlists
                      set playlists_count [$playlists Count]
                      append ret "$playlists_count playlists\n"

                      for {set i 1} {$i <= $playlists_count} {incr i} {

                          # for each playlist
                          set pl [$playlists Item $i]
                          append ret "\t$i [$pl Name]\n"
                          set tracks [$pl Tracks]
                          append ret "\t\t[$tracks Count] tracks\n"
                      }
                  }
                  2 {
                      # only accept numbers for playlists
                      set targ_playlist [lindex $val 1]

                      set sources [$V::itunes Sources]
                      set playlists [[$sources Item 1] Playlists]

                      set playlists_count [$playlists Count]

                      if {[string is integer $targ_playlist]} {
                          if {$targ_playlist >= 1 && $targ_playlist <= $playlists_count} {
                              set pl [$playlists Item $targ_playlist]
                              $pl PlayFirstTrack
                              set ret "Playing first track of playlist $targ_playlist ([$pl Name])"
                          } else {
                              error "Playlist number must be in range"
                          }
                      } else {
                          error "use numbers for playlists"
                      }
                  }
              }
          }

          name {
              if {! [string match "" $playlist]} {
                  set ret [$playlist Name]
              } else {
                  error "no current playlist"
              }
          }
          track -
          tracks {
              switch -- $len {
                  1 {

                      # list tracks for current playlist
                      set tracks [$playlist Tracks]
                      set cnt [$tracks Count]
                      set track_list [list]
                      for {set i 1} {$i <= $cnt} {incr i} {
                          set track [$tracks Item $i]
                          lappend track_list "[$track Index] [$track Name]"
                      }
                      set ret [join $track_list "\n"]

                  }
                  2 {

                      # play a numbered track

                      set track_num [lindex $ar 1]
                      set tracks [$playlist Tracks]

                      if {[string is integer $track_num]} {
                          if {$track_num > 0 && $track_num <= [$tracks Count]} {
                              set track [$tracks Item $track_num]
                              $track Play
                              set ret "Playing track [$track Index] -- [$track Name]"
                          } else {
                              error "Cannot play track \"$track_num\"
                          }
                      } else {
                          error "Cannot play track \"$track_num\"
                      }
                  }
                  default {
                      error "unknown subcommand \"$val\""
                  }
              }
          }
          default {
              error "unknown subcommand \"$val\""
          }
      }
      return $ret
  }

  iTunesServer::register "Info" "info album | artist | bpm | coment | kind | name | time"
  proc iTunesServer::Info {val} {
      if {! [valid]} return

      set ret ""
      set track [$V::itunes CurrentTrack]
      switch -- $val {
          album {
              set ret [$track Album]
          }
          artist {
              set ret [$track Artist]
          }
          bpm {
              set ret [$track BPM]
          }
          comment {
              set ret [$track Comment]
          }
          kind {
              set ret [$track KindAsString]
          }
          name {
              set ret [$track Name]
          }
          time {
              set ret [$track Time]
          }
          default {
              error "Unkown subcommand $val"
          }
      }
  }

  iTunesServer::register "PlayPause" "pause"
  proc iTunesServer::PlayPause {} {
      # play or pause a current itunes
      if {! [valid]} return

      $V::itunes PlayPause
  }

  iTunesServer::register Volume "volume \[ n \]"
  proc iTunesServer::Volume {{val ""}} {
      # returns the current volume if singleton
      # sets the volume to the given value if given
      # a number
      # out of range values are ignored
      # returns nothing, or the current value

      if {! [valid]} return

      set ret {}

      if {[string match "" $val]} {

          set ret "Current volume is [$V::itunes SoundVolume]"

      } elseif {[string is digit $val]} {

          # make sure the range is 0 and 100
          if {$val <= 100 && $val >= 0} {
              $V::itunes SoundVolume $val
              set ret "Setting volume to $val"
          }
      } 

      return $ret
  }

  if {$::tcl_interactive} {
      interp alias {} command_parse {} iTunesServer::command_parse
      interp alias {} getItunes {} iTunesServer::getItunes
  } else {
      iTunesServer::start
      vwait ::forever
  }

The original author of this page and the snippets is WL


Category Application Category Music