Skip to content

Instantly share code, notes, and snippets.

@lamberta
Created April 16, 2010 02:46
Show Gist options
  • Save lamberta/367939 to your computer and use it in GitHub Desktop.
Save lamberta/367939 to your computer and use it in GitHub Desktop.
beejay.py
#!/usr/bin/env python
##
## -- 'beejay' is Billy's Jukebox --
## -- Copyright 2008 Billy Lamberta --
##
## Read and sort an iTunes generated playlist to use with a
## command-line audio player like 'mplayer' or 'mpg123'.
##
## Requires the 'Plistlib' library, which is available here:
## http://svn.red-bean.com/bob/plistlib/trunk/plistlib.py
##
## Please make sure the 'PLAYER' and 'ITUNESLIBRARY' variables
## below have the proper location paths assigned to them.
##
## basic usage: beejay -p playlist.xml
## beejay --help
##
## For more information and examples, please go to
## http://lamberta.posterous.com/beejay-is-billys-jukebox
##
## This code was written by William Lamberta and is
## distributed under the GNU General Public License Version 3.
## For more details see http://www.gnu.org/licenses/gpl-3.0.txt
#Must include the complete path to player if not in system path.
#Be careful! Opening a bunch of mp3's with notepad is not very fun.
PLAYER = "mplayer -really-quiet"
#Use your entire iTunes library as the default playlist, option '-d'
ITUNESLIBARY = "$HOME\My Music\iTunes\iTunes Music Library.xml"
version = "beejay v0.1c"
usage = "usage: %prog -p playlist [-options] args"
class World():
def __init__(self):
#parse command-line & setup environment
self.iTunesPlist = {}
self.filePath = None
self.player = PLAYER
self.shuffle = False
self.silent = False
self.counter = True
self.search = False
self.searchType = None
self.printOut = False
self.printM3U = False
self.m3uFile = None
self.pattern = None #user search pattern
self.parseCommandLine()
def parseCommandLine(self):
from optparse import OptionParser, OptionGroup
parser = OptionParser(usage=usage)
parser.add_option("-v", dest="version", default=False,
action="store_true",
help="version and program information")
parser.add_option("-p", dest="infile", default=None, metavar="file",
action="store",
help="open an iTunes formatted playlist")
parser.add_option("-r", dest="shuffle", default=False,
action="store_true",
help="random mode (shuffle)")
parser.add_option("-t", dest="counter", default=True,
action="store_false",
help="turn off track numbers")
parser.add_option("-d", dest="defaultLib", default=False,
action="store_true",
help="use the default library")
parser.add_option("-o", dest="outfile", default=None, metavar="file",
action="store",
help="output a M3U formatted playlist (use with --print)")
parser.add_option("--print", dest="printOut", default=False,
action="store_true",
help="print playlist to screen without playing")
parser.add_option("--no-see", dest="silent", default=False,
action="store_true",
help="don't print song info to screen")
#not implemented
# parser.add_option("--player", dest="player", default=None, metavar="program",
# action="store", help="use a different media player")
group = OptionGroup(parser, "Search playlist options",
"Each option is considered in addition to, not a subset of. So --artist --album will search both playlist fields, not just the artist's albums.")
group.add_option("-s","--search",dest="patternAll",default=None,metavar="pattern",
type="string",action="store",help="search all fields")
group.add_option("--artist",dest="patternArtist",default=None,type="string",
metavar="pattern",action="store", help="search by artist")
group.add_option("--name",dest="patternTitle",default=None,type="string",
metavar="pattern",action="store", help="search by song name")
group.add_option("--album",dest="patternAlbum",default=None,type="string",
metavar="pattern",action="store",help="search by album")
parser.add_option_group(group)
(options, args) = parser.parse_args()
if options.version is True:
print "\
%s\nFor information and examples visit http://www.lamberta.org/blog/beejay\
\n%s was written by Billy Lamberta and\
\ndistributed under the GNU General Public License, Version 3.\
\nSee http://www.gnu.org/licenses/gpl-3.0.txt for details." %(version,version)
if options.shuffle is True:
self.shuffle = True
if options.silent is True:
self.silent = True
if options.counter is False:
self.counter = False
if options.printOut is True:
self.printOut = True
#this is gonna be ugly, but i'm getting tired...
if options.patternAll is not None:
self.pattern = options.patternAll
self.search = True
self.searchType = 'all'
if options.patternArtist is not None:
self.pattern = options.patternArtist
self.search = True
self.searchType = 'artist'
if options.patternTitle is not None:
self.pattern = options.patternTitle
self.search = True
self.searchType = 'title'
if options.patternAlbum is not None:
self.pattern = options.patternAlbum
self.search = True
self.searchType = 'album'
#if options.player is not None:
#if not os.path.isfile(options.player):
# parser.error("%s does not exist or I can't find it." %options.player)
#else:
#self.player = options.player
if options.outfile is not None:
try:
self.m3uFile = open(options.outfile,'w')
self.m3uFile.write("#EXTM3U\n") #m3u header
self.m3uFile.write("# M3U generated by %s\n# http://www.lamberta.org/blog/beejay for more details.\n\n" %version)
self.printM3U = True #set this after to avoid later complications
except IOError, e:
print "Unable to output m3u playlist: %s" %e
if options.defaultLib is True:
options.infile = ITUNESLIBARY
if options.infile == None:
print "'%s --help' for more options" %sys.argv[0]
sys.exit(0)
else:
self.filePath = os.path.expandvars(options.infile)
#now is it even a file?
if not os.path.isfile(self.filePath):
parser.error("%s is not a file." %self.filePath)
else:
try:
from plistlib import readPlist
except ImportError:
print "PlistLib is not installed; <http://svn.red-bean.com/bob/plistlib/trunk/plistlib.py>"
print "Unable to parse the iTunes playlist."
sys.exit(1)
try:
#read file and return a nested dict
fp = open(self.filePath, "r")
self.iTunesPlist = readPlist(fp)
fp.close()
except:
sys.exit("Error reading playlist file.")
class Playlist:
def __init__(self, playlist):
#creates list of track id#'s
self.ordered = [] #how iTunes ordered it
self.playlist = [] #what we're using now
self.filtered = []
self.parsePlaylist(playlist)
self.checkShuffle()
self.checkSearch()
def parsePlaylist(self, playlist):
try:
for key in playlist['Playlists'][0]['Playlist Items']:
self.ordered.append( key.values()[0] )
except:
sys.stderr("Error parsing iTunes playlist.")
def checkShuffle(self):
#see if shuffle requested
if world.shuffle is True:
self.playlist = self.shuffle()
else:
self.playlist = self.ordered
def shuffle(self):
from random import shuffle
shuffledList = self.ordered
shuffle(shuffledList)
return shuffledList
def checkSearch(self):
if world.search is True:
self.playlist = self.filterList(world.pattern, world.searchType)
else:
pass
def filterList(self, pattern, searchType):
for trackid in self.playlist:
song = Song()
song.fillValues(world.iTunesPlist, trackid)
try:
filter = re.compile(pattern, re.IGNORECASE)
except:
print pattern + " is not a valid search expression."
sys.exit(1)
#determine the scope of our search
if searchType == "artist":
if filter.search(song.artist):
self.filtered.append(trackid)
elif searchType == "title":
if filter.search(song.name):
self.filtered.append(trackid)
elif searchType == "album":
if filter.search(song.album):
self.filtered.append(trackid)
elif searchType == "all":
if filter.search(song.artist) or filter.search(song.name) or filter.search(song.album):
self.filtered.append(trackid)
else:
pass #no match
return self.filtered
class Song:
#extracts song info from the iTunes plist file.
def __init__(self):
#sometimes there aren't entries for these
self.name = ""
self.artist = ""
self.album = ""
self.time = ""
self.sec = 0 #length in seconds for m3u output
def fillValues(self, myPlaylist, intkey):
key = str(intkey)
try:
#location first, everything else optional
#if there's no key it'll break away for good,
#-having an issue with non-standard characters
self.location = myPlaylist['Tracks'][key]['Location']
self.trackID = myPlaylist['Tracks'][key]['Track ID']
length = myPlaylist['Tracks'][key]['Total Time']
self.time = self.ms2min(length)
self.name = myPlaylist['Tracks'][key]['Name']
self.artist = myPlaylist['Tracks'][key]['Artist']
self.album = myPlaylist['Tracks'][key]['Album']
except KeyError, e:
#If the key can't be found then fill in the blanks
key=str(e)
if key == "\'Artist\'":
self.artist = " * "
elif key == "\'Name\'":
self.name = " * "
elif key == "\'Album\'":
self.album = " * "
elif key == "\'Total Time\'":
self.time = "--:--"
else:
print "Missing an important key:", e
def ms2min(self, ms):
self.sec = ms/1000 #for m3u output
#convert milliseconds to something I can read
s = ms/1000
m,s = divmod(s,60)
h,m = divmod(m,60)
if h == 0:
return "%i:%02i" %(m,s)
else:
return "%i:%02i:%02i" %(h,m,s)
def getFilePath(self):
from urllib import unquote
#iTunes uses hex url encoding
try:
songPath = unquote(self.location).lstrip(itunesPrefix)
except:
sys.stderr("That didn't work. getFilePath()")
return r'"%s"' %songPath #tricky quotes
#strip this off the path until I find a use for it
itunesPrefix = "file://localhost/"
if __name__ == "__main__":
import os, sys, re
world = World()
myPls = Playlist(world.iTunesPlist)
n = 0
for trackid in myPls.playlist:
n += 1
#a redundant test to make sure it's in the list
if world.iTunesPlist['Tracks'].keys().count( str(trackid) ):
song = Song()
song.fillValues(world.iTunesPlist, trackid)
if world.silent is False: #print check
if world.counter is True:
print "%02i/%02i" %(n, len(myPls.playlist)),
try:
print song.artist + " --",
print song.name + " --",
print song.time
except AttributeError, e:
sys.stderr("Loading error, %s" %e)
else:
sys.stderr("This song is not in list!")
try:
if world.printM3U is True:
#having problems writing unicode charcters.
world.m3uFile.write("#EXTINF:%i,%s - %s\n" %(song.sec,
song.artist.encode('utf-8','replace'),
song.name.encode('utf-8','replace')))
world.m3uFile.write("%s\n" %song.getFilePath().strip('\'"'))
#this should still run through all the previous printing
if world.printOut is False:
#send to os, 1 at a time
os.system(world.player +" "+ song.getFilePath())
except KeyboardInterrupt, e:
#i'd like to capture an key to see if i can move
#around the playlist
#print "you hit: ", e
#trying to exit cleanly
sys.exit(1)
#any cleanup before we leave?
if world.printM3U is True:
world.m3uFile.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment