You are not logged in.

#1 2009-07-20 12:07:30

Barrucadu
Member
From: York, England
Registered: 2008-03-30
Posts: 1,158
Website

Two tools for MPD: dynamic and smart playlists

I present to you, fellow Archers, two little programs. One I've had for months but haven't shared for some reason, and the other I finished a short while ago.

MPDDP
MPDDP: MPD Dynamic Playlists is a program to generate, as the name would imply, a dynamic playlist for MPD. You specify rules about what tracks you want added, and it keeps adding them randomly.

Source:

#!/usr/bin/env python

# MPDDP: MPD Dynamic Playlists
# Call this and run it in the background (eg mpddp &>/dev/null &)
# Configured in /etc/mpddp.conf, See /etc/mpddp.conf.example.

import mpd, random, os, time, sys, string

client = mpd.MPDClient()

host = "" # The host MPD is operating upon
port = 0  # The port MPD is operating upon

playlistlen    = 0  # The len of the playlist
changeafter    = 0  # The number of tracks before more are added/removed to/from the playlist
clearinitially = '' # Whether to clear the playlist initially or not.
saveonquit     = '' # Whether to save/load the playlist on exit/start or not.
update         = '' # Whether to periodically check the config file / filesystem for changes.

confdir = '/etc/mpddp.conf' # The path of the main MPDDP config file.
savedir = ''                # The folder where MPDDP saves and loads files.

alltracks = [] # All the tracks that can be played.
oldconfig = [] # The configuration as it was last loaded.

def pickNewTrack(): # Pick and remove a track from the list, append it to the current list, and return the name.
    global client
    global alltracks

    index = random.randint(0, len(alltracks) - 1)
    track = alltracks[index]

    return track

def addNewTrackToPlaylist(): # Pick a new track, update the lists, and add it to the playlist.
    global client
    global host
    global port
    global playlistlen
    
    client.connect(host, port)
    playlist = client.playlistinfo()
    client.disconnect()

    if len(playlist) < playlistlen:
        track = pickNewTrack()

        print "Adding", track
        
        client.connect(host, port)
        client.add(track)
        client.disconnect()

def removeLastTrackFromPlaylist(): # Delete the oldest track from the playlist.
    global client
    global host
    global port
    
    client.connect(host, port)
    playlist = client.playlistinfo()
    client.delete(0)
    client.disconnect()

    print "Removing", playlist[0]['file']

def checkMPDPlaylist(): # Add enough tracks to the MPD playlist to repopulate it if it is almost empty.
    global client
    global host
    global port
    global playlistlen
    global alltracks
    
    client.connect(host, port)
    playlist = client.playlistinfo()
    client.disconnect()

    if len(playlist) < playlistlen:
        while len(playlist) < playlistlen:
            addNewTrackToPlaylist()
    
def updatePlaylist(): # Update the MPD playlist, and the internal representation of it if necessary.
    checkMPDPlaylist()
    removeLastTrackFromPlaylist()
    addNewTrackToPlaylist()

def getFilenamesFromMPDSPL(expression):
    os.system('mpdspl -s -n misc "' + expression + '" >/tmp/mpddp-mpdspl-temp.txt')
    a = open('/tmp/mpddp-mpdspl-temp.txt')
    ot = a.read()
    ot = ot.splitlines()
    a.close()
    os.remove('/tmp/mpddp-mpdspl-temp.txt')
    return ot

def getFilenamesFromMPD(rules): # Gets the filenames from MPD of all files which match the specified rules.
    global client
    global host
    global port

    paths     = []
    playlists = []
    smarts    = []
    nevers    = []
    tracks    = []
    
    for rule in rules:
        if rule[0] == 'path' and not rule[1] in paths:
            paths.append(rule[1])
        elif rule[0] == 'playlist' and not rule[1] in playlists:
            playlists.append(rule[1])
        elif rule[0] == 'smart' and not rule[1] in smarts:
            smarts.append(rule[1])
        elif rule[0] == 'never' and not rule[1] in nevers:
            nevers.append(rule[1])
    
    client.connect(host, port)
    
    for path in paths:
        temptracks = client.search("file", path)
        for track in temptracks:
            if isinstance(track, dict):
                track = track['file']
            dontadd = False
            for never in nevers:
                if never in track:
                    dontadd = True
            if dontadd == False and not track in tracks:
                tracks.append(track)

    for playlist in playlists:
        temptracks = client.listplaylist(playlist)
        for track in temptracks:
            if isinstance(track, dict):
                track = track['file']
            dontadd = False
            for never in nevers:
                if never in track:
                    dontadd = True
            if dontadd == False and not track in tracks:
                tracks.append(track)

    for smart in smarts:
        temptracks = getFilenamesFromMPDSPL(smart)
        for track in temptracks:
            dontadd = False
            for never in nevers:
                if never in track:
                    dontadd = True
            if dontadd == False and not track in tracks:
                tracks.append(track)

    client.disconnect()
    return tracks

def parseConfigIncludes(conf, path): # Parse a config file
    outconf = ""
    paths = path
    
    for line in conf:
        line = line.split("#")
        line = line[0]
        line = line.strip()
        
        if len(line) > 0:
            if line[0:7] == 'include':
                toinclude = line[8:].strip()
                toinclude = toinclude.replace("~", os.path.expanduser("~"))
                if not toinclude in paths:
                    paths.append(toinclude)
                    filehandler = open(toinclude)
                    newconf = parseConfigIncludes(filehandler, paths)
                    outconf = outconf + newconf
                    filehandler.close()
            else:
                outconf = outconf + line + "\r\n"
                
    return outconf

def parseConfigLine(line): # Parse a line from the configuration file and return what it means.
    line = line.split("#")
    line = line[0]
    line = line.strip()

    if len(line) > 0:
        if ("=" in line) and (not ":" in line):
            pline = line.split("=", 1)
            parsed = {'type'  : pline[0].strip(),
                      'value' : pline[1].strip()}
            return parsed
        elif ":" in line:
            pline = line.split(":", 1)
            parsed = {'type'  : 'rule',
                      'value' : [pline[0].strip(), pline[1].strip()]}
            return parsed
        else:
            return {'type' : 'unrecognised'}
    else:
        return {'type' : 'blankline'}

def parseConfigFile(): # Open the configuration file and parse the rules.
    global confdir
    
    filehandler = open(confdir)
    conf = parseConfigIncludes(filehandler, [confdir])
    output = {'rules'          : [],
              'server'         : 'localhost',
              'port'           : 6600,
              'playlistlen'    : 15,
              'changeafter'    : 8,
              'clearinitially' : 'yes',
              'saveonquit'     : 'no',
              'savedir'        : '/var/lib/mpddp/',
              'update'         : 'no'}
    
    for line in conf.splitlines():
        result = parseConfigLine(line)
        if result['type'] == 'rule':
            output['rules'].append(result['value'])
        elif result['type'] == 'clearinitially' or result['type'] == 'saveonquit':
            if result['value'] == 'yes' or result['value'] == 'no':
                output[result['type']] = result['value']
            else:
                print "Invalid value specified for", result['type']
        elif result['type'] == 'port' or result['type'] == 'playlistlen' or result['type'] == 'changeafter':
            try:
                if (result['type'] == 'port' and int(result['value']) > 0 and int(result['value']) <= 65536) or not result['type'] == 'port':
                    output[result['type']] = int(result['value'])
                else:
                    print "Invalid value specified for", result['type']
            except TypeError:
                print "Invalid value specified for", result['type']
        elif result['type'] == 'server' or result['type'] == 'savedir' or result['type'] == 'update':
            output[result['type']] = result['value']

    filehandler.close()

    return output

def loadPlaylistFromSaved(): # Load the previously saved playlist, if it exists. Then fill any space remaining with newly-added tracks.
    global playlistlen
    global client
    global host
    global port
    global savedir

    loaded = []
    
    try:
        filehandler = open(savedir + 'playlist')
        for line in filehandler:
            loaded.append(line)
        filehandler.close()

        client.connect(host, port)
        for track in loaded:
            track = track.strip()
            if len(track) > 0:
                try:
                    client.add(track)
                    print "Loading", track
                except mpd.CommandError:
                    print "Error loading", track
        client.disconnect()

        for i in range(len(loaded), playlistlen):
            addNewTrackToPlaylist()
    except IOError:
        for i in range(0, playlistlen):
            addNewTrackToPlaylist()

def populateLists(redoing): # Parse the configuration file, and grab the tracks from MPD to populate the lists.
    global playlistlen
    global changeafter
    global clearinitially
    global client
    global host
    global port
    global alltracks
    global saveonquit
    global savedir
    global update
    global oldconfig
    
    config = parseConfigFile()
    rules  = config['rules']
    if redoing == False or (redoing == True and not oldconfig == config):
        host           = config['server']
        port           = config['port']
        playlistlen    = config['playlistlen']
        changeafter    = config['changeafter']
        clearinitially = config['clearinitially']
        saveonquit     = config['saveonquit']
        savedir        = config['savedir']
        update         = config['update']
        oldconfig      = config
        
        print "Configuration updated:", config
    
    tracks = getFilenamesFromMPD(rules)
    
    if redoing == True:
        alltracks = []
        
    if redoing == False or not alltracks == tracks:
        alltracks = tracks

    client.connect(host, port)    
    if clearinitially == 'yes' and redoing == False:
        client.clear()
    client.random(0)
    client.disconnect()

    if redoing == False:
        if saveonquit == 'no':
            for i in range(0, playlistlen):
                addNewTrackToPlaylist()
        else:
            loadPlaylistFromSaved()

        client.connect(host, port)    
        client.play()
        client.disconnect()

def dieGracefully():
    global saveonquit
    
    if saveonquit == 'yes':
        try:
            os.remove('/var/lib/mpddp/playlist')
            print "Removed old playlist..."
        except OSError:
            print "No old playlist to remove."

        try:
            os.remove('/tmp/killmpddp')
            print "Removed kill file..."
        except OSError:
            print "No kill file to remove."

        print "Saving playlist to", savedir, "playlist"
        filehandler = open(savedir + 'playlist', 'w')
        playlist = client.playlistinfo()
        for track in playlist:
            print "Writing", track['file']
            filehandler.write(track['file'] + '\n')
        filehandler.close()

    print "Quitting..."
    sys.exit()

# Execute the program main loop
populateLists(False)
loops = 0
try:
    while True:
        if os.path.exists('/tmp/killmpddp'):
            dieGracefully()
        
        client.connect(host, port)
        info = client.currentsong()
        status = client.status()
        playlist = client.playlistinfo()
        client.disconnect()
        
        if len(info) > 0:
            if int(status['song']) >= changeafter:
                for i in range(changeafter - 1, int(status['song'])):
                    updatePlaylist()
        if len(playlist) < playlistlen:
            for i in range(len(playlist), playlistlen):
                addNewTrackToPlaylist()
        
        if loops == 59:
            if update == 'yes':
                populateLists(True)
            loops = 0
        else:
            loops = loops + 1
        
        time.sleep(1)
except KeyboardInterrupt:
    dieGracefully()

/etc/mpddp.conf.example:

server         = localhost       # The server that MPD is operating upon.
port           = 6600            # The port that MPD is operating upon.
playlistlen    = 15              # The number of tracks to have in the playlist.
changeafter    = 8               # The number of tracks listened to initially before the add/remove loop begins.
clearinitially = no              # Whether to clear the playlist upon starting or not.
saveonquit     = no              # Whether to save/load the playlist upon exit/start or not.
savedir        = /var/lib/mpddp/ # The directory to save/load the playlist.
update         = no              # Periodically check to ensure that the config file hasn't been updated, and that no tracks have been added/removed.

#include /home/USER/.mpddp       # An additional config file to parse. I suggest you use this to specify tracks to listen to

#path:PATH                       # Add all tracks where the file path contains PATH.
#playlist:PLAYLIST               # Add all the playlists where the name is PLAYLIST.
#smart:RULES                     # Add all the tracks which match the smart playlist RULES. Requires MPDSPL to be in your $PATH.
#never:STRING                    # Never add tracks where the file path contains STRING.

And, for reference, here's a small chunk of my config file:

# -*-conf-*-

playlistlen    = 50 # For full-screen
changeafter    = 26
#playlistlen    = 19 # For half-screen
#changeafter    = 10
clearinitially = no

# Playlists
#playlist:Wordless
#playlist:Steamcowboy

# Musicals
#smart:fp=/(Cats Original London Cast|Joseph and the Amazing Technicolour Dreamcoat|Les Misérables .*|Mary Poppins|Miss Saigon .*|Phantom Of The Opera .*|Sound of Music 40th Anniversary Special Edition, The).*\//
#path:Cats Original London Cast/
#path:Joseph and the Amazing Technicolour Dreamcoat/
#path:Les Misérables Complete Symphonic Recording/
#path:Les Misérables Original Broadway Cast/
#path:Les Misérables Original Paris Cast/
#path:Mary Poppins/
#path:Miss Saigon CSR/
#path:Miss Saigon (Original London Cast)/
#path:Phantom Of The Opera 2004 Film, The/
#path:Phantom of the Opera Original London Cast, The/
#path:Sound of Music 40th Anniversary Special Edition, The/

MPDSPL
MPDSPL: MPD Smart PlayLists makes smart playlists for MPD. See the "smart:" line in my config file above? that's an example of a MPDSPL playlist description. Playlist descriptions are in regex, and use keywords ("ar" for artist, "al" for album, etc).

Source:

#!/usr/bin/env python

# A script to parse the MPD database into a list of dictionaries (or at least, it was going to be before I decided to finish it).
# Now with patronising comments which assume almost no Python knowledge!

# cPickle is a faster version of the pickle library. It is used to save data structures to a file. Like lists and dictionaries. os is needed for file stuff, sys for arguments, and re for regex.
import cPickle, os, sys, re

# Info about new playlists
newname  = ""
newrules = []

# Place to look for the MPD database and config files, and the loaded MPD config (well, only the values useful to us).
confpath = "/etc/mpd.conf"
mpd      = {"music_directory" : "", "playlist_directory" : "", "db_file" : "", "user" : ""}

# There is an environmental variable XDG_CACHE_HOME which specifies where to save cache files. However, if not set, a default of ~/.cache should be used.
cachehome = os.path.expanduser(os.environ['XDG_CACHE_HOME'])
if cachehome == "":
    cachehome = os.environ['HOME'] + "/.cache"
cachepath = cachehome + "/mpdspl/mpddb.cache"

# $XDG_DATA_HOME specifies where to save data files. Like a record of playlists which have been created. If unset a default of ~/.local/share should be used. This is currently unused as there is no actual creation of playlists yet :p
datahome = os.path.expanduser(os.environ['XDG_DATA_HOME'])
if datahome == "":
    datahome = os.environ['HOME'] + "/.local/share/"
datapath = datahome + "/mpdspl"
# If the data directory does not exist, create it.
if not os.path.isdir(datapath):
    os.mkdir(datapath)

tracks = []
forceupdate  = False
simpleoutput = False

# A nice little help function. Read on to see how it is called...
def showhelp():
    print "Usage: mpdspl [options]\n"
    print "A script to generate smart playlists for MPD. Currently does nothing of use :p\n"
    print "Options:"
    print "    -f, --force              - Force an update of the cache file and any playlists."
    print "    -dFILE, --dbpath=FILE    - Location of the database file."
    print "    -cFILE, --cachepath=FILE - Location of the cache file."
    print "    -CFILE, --confpath=FILE  - Location of the MPD config file."
    print "    -uUSER, --mpduser=USER   - Location of the MPD config file."
    print "    -n, --new [name] [rules] - Create a new playlist."
    print "    -s, --simple             - (used with -n) Only print the final track list (with paths relative to the MPD root dir) to STDOUT."
    print "    -h, --help               - Display this text and exit.\n"
    print "Playlist rules:"
    print "    These are specified as a string of Python-compatible regular expressions separated by keywords, spaces, and slashes."
    print "    They are matched by re.search, not re.match, and no special flags are passed, other than re.IGNORECASE when requested.\n"
    print "    These keywords are:"
    print "        ar = Artist"
    print "        al = Album"
    print "        ti = Title"
    print "        tr = Track Number"
    print "        ge = Genre"
    print "        ye = Year"
    print "        le = Length (seconds)"
    print "        fp = File Path (relative to MPD root dir, including filename)"
    print "        fn = File Name\n"
    print "    Regular expressions are specified within slashes (/regex/)."
    print "    If the first slash is preceeded by an 'i', the regular expression is interpreted as case-insensitive."
    print "    If the final slash is succeeded by a 'n', the result of the match is negated.\n"
    print "    For example, a rule for all tracks by 'Fred' or 'George', which have a title containing (case insensitive) 'The' and 'and', but not 'when' would be:"
    print "        ar=/(Fred|George)/ ti=i/(the.*and|and.*the)/ ti=i/when/n\n"
    print "Notes:"
    print "    Paths specified in the MPD config file containing a '~' will have the '~'s replaced by the user MPD runs as."
    print "    If the user is not specified in the MPD config file, or by the -u parameter, it is assumed the user is root."
    print "    Backslashes must be escaped in playlist rules.\n"
    sys.exit()

# Parse the rules regex
def parserules(rulestr):
    # rules will be our list of rules, bufferstr will be the buffer for our parser, and i will be a counter
    rules     = []
    bufferstr = ""
    i         = 0

    # We want to use the same identifiers as the track dictionaries:
    keywords = {"ar" : "Artist", "al" : "Album", "ti" : "Title", "tr" : "Track", "ge" : "Genre", "ye" : "Date", "le" : "Time", "fp" : "file", "fn" : "key"}

    # For every character in rulestr (we do it characterwise, hence needing a buffer)
    for c in rulestr:
        # Add the character to the buffer
        bufferstr += c

        # If the buffer matches one of our keywords, we have hit a new rule, and so create a blank dictionary, and clear the buffer.
        if bufferstr.strip() in ["ar", "al", "ti", "tr", "ge", "ye", "le", "fp", "fn"]:
            rules.append({"type" : keywords[bufferstr.strip()], "regex" : "", "compiled" : None, "inverse" : False, "negate" : False})
            bufferstr = ""
        # If we're at the start of a blank case-insensitive regex, record that, and clear the buffer.
        elif bufferstr == "=i/":
            rules[i]["i"] = True
            bufferstr = ""
        # If not, just clear the buffer for the coming regex.
        elif bufferstr == "=/":
            bufferstr = ""
        # If at the end of a regex, stick it all (sans the trailing slash, they're just a nice separater for our parser) to the dictionary, increment the counter, and clear the buffer ready for the next rule.
        elif bufferstr[-1] == "/" and not bufferstr[-2] == "\\":
            rules[i]["regex"] = bufferstr[:-1]
            bufferstr = ""
            i += 1
        # Get rid of the escape backslash if a forward slash has been used.
        elif bufferstr[-1] == "/" and not bufferstr[-2] == "\\":
            bufferstr[-2] = ""
        # If set to 'n' and the regex has been set, negate it.
        elif bufferstr == "n" and not rules[i - 1]["regex"] == "":
            bufferstr = ""
            rules[i - 1]["negate"] = True

    # This isn't needed. But it makes things faster and allows us to have case insensetivity.
    for rule in rules:
        regex = None
        if rule["inverse"]:
            # If case insensitive, compile it as such.
            regex = re.compile(rule["regex"], re.IGNORECASE)
        else:
            regex = re.compile(rule["regex"])

        # Overwrite the regex string with the compiled object
        rule["compiled"] = regex

    return rules

# Splitting things up into functions is good :D
def parseargs():
    # global lets us access variables specified outside our function.
    global forceupdate
    global mpd
    global confpath
    global cachepath
    global newname
    global newrules
    global simpleoutput
    
    newarg = 0
    
    for argument in sys.argv:
        if not newarg == 0:
            # We're making a new playlist. If we're only on the first option after -n, that's the name. If the second, that's the description.
            if newarg == 2:
                newname = argument
            elif newarg == 1:
                newrules = parserules(argument)
            newarg -= 1
        else:
            if argument == "-f" or argument == "--force":
                # If a "-f" or "--force" parameter is sent, force the cache to be updated even if it doesn't look like it needs to be.
                forceupdate = True
            elif argument[:2] == "-d" or argument[:9] == "--dbpath=":
                # Looks like their db is somewhere other than /var/lib/mpd/mpd.db...
                if argument[:2] == "-d":
                    # Python can't work with ~, which has a reasonable chance of being used (eg: ~/.mpd/mpd.db"), so it needs to be expanded.
                    mpd["db_file"] = os.path.expanduser(argument[2:])
                elif argument[:9] == "--dbpath=":
                    mpd["db_file"] = os.path.expanduser(argument[9:])
            elif argument[:2] == "-c" or argument[:12] == "--cachepath=":
                # Silly person, not keeping their cache where XDG says it should be...
                if argument[:2] == "-c":
                    cachepath = os.path.expanduser(argument[2:])
                elif argument[:12] == "--cachepath=":
                    cachepath = os.path.expanduser(argument[12:])
            elif argument[:2] == "-C" or argument[:11] == "--confpath=":
                # Now any person which this code applies to is just awkward.
                if argument[:2] == "-C":
                    confpath = os.path.expanduser(argument[2:])
                elif argument[:11] == "--confpath=":
                    confpath = os.path.expanduser(argument[11:])
            elif argument[:2] == "-u" or argument[:10] == "--mpduser=":
                # As is any person to whom this applies...
                if argument[:2] == "-u":
                    mpd["user"] = argument[2:]
                elif argument[:10] == "--mpdpath=":
                    mpd["user"] = argument[10:]
            elif argument == "-n" or argument == "--new":
                # Do special treatment to the next 2 arguments
                newarg = 2
            elif argument == "-s" or argument == "--simple":
                # Ooh, this means that (probably) MPDDP is being used! Yay!
                simpleoutput = True
            elif argument == "-h" or argument == "--help":
                showhelp()
            elif not argument == sys.argv[0]: # The first argument is the filename. Don't complain about not understanding it...
                # Ooh, stderr. I never actually knew how to send stuff through stderr in python.
                print >> sys.stderr, "Unrecognised parameter '" + argument + "'"
                sys.exit(1)

# A function to parse a MPD database and make a huge list of tracks
def parsedatabase(database):
    global tracks
    
    i        = -1
    parsing  = False

    for line in database:
        # For every line in the database, remove any whitespace at the beginning and end so the script isn't buggered.
        line = line.strip()

        # If entering a songList, start parsing. If exiting one, stop. Fairly self explanatory.
        if not parsing and line == "songList begin":
            parsing = True
        elif parsing and line == "songList end":
            parsing = False

        # If we get a line to parse which is not a "songList begin" statement (because it's be stupid to do things with that)
        if parsing and not line == "songList begin":
            if line[0:5] == "key: ":
                i += 1
                # Increment the counter and make an empty dictionary if we hit the beginning of a track
                tracks.append({"key" : "", "file" : "", "Time" : "", "Genre" : "", "Title" : "", "Artist" : "", "Date" : "", "Album" : "", "Track" : "", "mtime" : ""})

            # Split the line by the first ": ", the string MPD uses, and stick the second part (the value) in the bit of the dictionary referred to by the first part (the key)
            splitted = line.split(": ", 1)
            tracks[i][splitted[0]] = splitted[1]

# Grabbing stuff from the MPD config, a very important step
def parsempdconf():
    global confpath
    global mpd
    
    config   = open(confpath, "r")
    # Don't load the user or db_file values if they've already been told to us
    holduser = not mpd["user"] == ""
    holddb   = not mpd["db_file"] == ""

    for line in config:
        line = line.strip()
        if line[:15] == "music_directory":
            rest = line[15:].strip()
            mpd["music_directory"] = rest[1:-1]
        elif line[:18] == "playlist_directory":
            rest = line[18:].strip()
            mpd["playlist_directory"] = rest[1:-1]
        elif line[:7] == "db_file" and not holddb:
            rest = line[7:].strip()
            mpd["db_file"] = rest[1:-1]
        # The rest of the code in this function wouldn't be needed if I could assume nobody would use "~" in their MPD config...
        elif line[:4] == "user" and not holduser:
            rest = line[4:].strip()
            mpd["user"] = rest[1:-1]

    if mpd["user"] == "":
        mpd["user"] = "root"

    homedir = "/home/" + mpd["user"]
    if homedir == "/home/root":
        homedir = "/root"

    if "~" in mpd["music_directory"]:
        mpd["music_directory"] = mpd["music_directory"].replace("~", homedir)
    if "~" in mpd["playlist_directory"]:
        mpd["playlist_directory"] = mpd["playlist_directory"].replace("~", homedir)
    if "~" in mpd["db_file"]:
        mpd["db_file"] = mpd["db_file"].replace("~", homedir)

def findtracks():
    global tracks
    global newrules
    
    # matchingtracks will hold all tracks which match all of the criteria.
    matchingtracks = []
    
    for track in tracks:
        # Initially assume a track *will* be added.
        addtrack = True
        
        for rule in newrules:
            # For every track, check it with every rule
            if rule["negate"]:
                if not re.search(rule["compiled"], track[rule["type"]]) == None:
                    # If the regular expression matches the track, do not add it to the matchingtracks list.
                    addtrack = False
            else:
                if re.search(rule["compiled"], track[rule["type"]]) == None:
                    # If the regular expression does not match the track, do not add it to the matchingtracks list.
                    addtrack = False
        
        if addtrack:
            # Add the track if appropriate
            matchingtracks.append(track)

    return matchingtracks

def genplaylist(tracks):
    global mpd
    # Parse a list of track dictionaries into a playlist. Thankfully, m3u is a *very* simple format.
    playlist = ""
    
    for track in tracks:
        playlist += mpd["music_directory"] + "/" + track["file"] + "\n"

    return playlist

# Save some random gubbage to a file
def savegubbage(data, path):
    if not os.path.isdir(os.path.dirname(path)):
        os.mkdir(os.path.dirname(path))

    # Open the file for writing in binary mode
    outfile = open(path, "wb")
    # Send the stuff to the file with the magic of cPickle
    cPickle.dump(data, outfile)
    # Close the file handler. Tidy u[p.
    outfile.close()

    # We might be running as someone other than the user, so make the file writable
    os.chmod(path, 438)

def loadgubbage(path):
    infile = open(path, "rb")
    data = cPickle.load(infile)
    infile.close()

    return data

def saveplaylist():
    global newname
    global newrules
    global mpd
    global datapath
    global simpleoutput
    
    matchingtracks = findtracks()
    playlist = genplaylist(matchingtracks)

    if simpleoutput:
        for track in matchingtracks:
            print track["file"]
    else:
        print "Saving playlist '" + newname + "'."
        
        # Write the contents of the playlist to the m3u file
        newlist = open(mpd["playlist_directory"] + "/" + newname + ".m3u", "w")
        newlist.write(playlist)
        newlist.close()
        
        # Save as list object. This lets us load them all into a big list nicely.
        savegubbage([newname, newrules], datapath + "/" + newname)


# Parse some options!
parseargs()
parsempdconf()

# Check that the database is actually there before attempting to do stuff with it.
if not os.path.exists(mpd["db_file"]):
    print >> sys.stderr, "The database file '" + mpd["db_file"] + "' could not be found."
    sys.exit(1)

# If the cache file does not exist OR the database has been modified since the cache file has this has the side-effect of being able to touch the cache file to stop it from being updated. Good thing we have the -f option for any accidental touches (or if you copy the cache to a new location).
if not os.path.exists(cachepath) or os.path.getmtime(mpd["db_file"]) > os.path.getmtime(cachepath) or forceupdate:
    if not simpleoutput:
        print "Updating database cache..."

    # If the cache directory does not exist, create it. The dirname function just removes the "/mpddb.cache" from the end.
    if not os.path.isdir(os.path.dirname(cachepath)):
        os.mkdir(os.path.dirname(cachepath))

    database = open(mpd["db_file"], "r")

    # Now, parse that database!
    parsedatabase(database)
    
    # Save the parsed stuff to the cache file and close the database file handler. That's not strictly required, python will clean up when the script ends, but you can't unmount volumes with file handlers pointing to them, so it makes a mess.
    savegubbage(tracks, cachepath)
    
    database.close()
    
    if not simpleoutput:
        # Let's update those playlists!
        playlistfiles = os.listdir(datapath)
        playlists     = []
        
        for playlistfile in playlistfiles:
            playlists.append(loadgubbage(datapath + "/" + playlistfile))
            
        # Backup the values first.
        oldnewname  = newname
        oldnewrules = newrules
        
        # Now regenerate!
        for playlist in playlists:
            newname  = playlist[0]
            newrules = playlist[1]
            saveplaylist()

        # And restore.
        newname  = oldnewname
        newrules = oldnewrules
else:
    # Oh, goodie, we don't need to go through all that arduous parsing as we have a valid cache file :D
    if not simpleoutput:
        print "Loading database cache..."
    # Open it for reading, load the stuff in the file into the tracks list, close the file handler, and have a party.
    tracks = loadgubbage(cachepath)
    
# See if we're making a new playlist or not
if not newname == "":
    # We are, go go go!
    saveplaylist()

The source is full of simple comments because I was supposed to be helping a friend who's less proficient in Python make it but… I got bored of waiting tongue
If your MPD playlists directory is somewhere which you don't have write access to, run it with `sudo -E`

If you need any help with either, ask and ye shall receive. If what you need help with is covered in this post, mpdspl -h, or /etc/mpddp.conf.example, ask and ye shall be laughed at tongue

Last edited by Barrucadu (2009-07-20 17:08:32)

Offline

Board footer

Powered by FluxBB