Created
April 16, 2010 02:46
-
-
Save lamberta/367939 to your computer and use it in GitHub Desktop.
beejay.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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