Kjetil's Information Center: A Blog About My Projects

XSPF Tag Playlist Generator

Once again I am working with XSPF playlists to organize music. I have made two new Python 3 scripts to handle tagging, so that files can be tagged (with multiple tags if needed) and then playlists can be generated based on this data. A JSON file serves as the database for the tags and is the link between the two scripts.

First script, for tagging files and manipulating the JSON database:

#!/usr/bin/python3

import os
import json

class TagDB(object):
    def __init__(self, db_file):
        self._db_file = db_file
        try:
            db_fh = open(db_file, "r")
            self._db = json.load(db_fh)
            db_fh.close()
        except:
            self._db = dict()
    
    def apply(self, filename, tags):
        local = tags.copy()
        if filename in self._db:
            local.extend(self._db[filename])
        self._db[filename] = list(dict.fromkeys(local)) # Remove duplicates!

    def delete(self, filename, tags):
        deleted = list()
        if filename in self._db:
            for t in tags:
                try:
                    self._db[filename].remove(t)
                    deleted.append(t)
                except:
                    pass
        return deleted

    def retrieve(self, filename):
        if filename in self._db:
            return self._db[filename]
        else:
            return list()

    def all_tags(self):
        tags = dict()
        for v in self._db.values():
            for t in v:
                tags[t] = True
        return list(tags.keys())

    def save(self):
        untagged = list()
        for filename in self._db:
            if len(self._db[filename]) == 0:
                untagged.append(filename)
        for filename in untagged:
            del self._db[filename]

        db_fh = open(self._db_file, "w")
        json.dump(self._db, db_fh)
        db_fh.close()

def usage(progname):
    print("%s <options> <files>" % (progname))
    print("-h       Help!")
    print("-r       Recursively go through directories.")
    print("-l       List previously used tags.")
    print("-d       Delete tag instead of applying.")
    print("-f DB    Database file to use.")
    print("-t TAG   Tag to apply (or delete).")
    print("")
    print("The -t can be specified multiple times for multiple tags.")
    print("If -t is omitted, current tags are listed for found files.")
    print("")

if __name__ == "__main__":
    import sys
    import getopt

    try:
        opts, args = getopt.getopt(sys.argv[1:], "hrldf:t:")
    except getopt.GetoptError as err:
        print(err)
        usage(sys.argv[0])
        sys.exit(1)

    recursive = False
    list_tags = False
    delete = False
    database = None
    tags = list()

    for o, a in opts:
        if o == "-h":
            usage(sys.argv[0])
            sys.exit(0)
        elif o == "-r":
            recursive = True
        elif o == "-l":
            list_tags = True
        elif o == "-d":
            delete = True
        elif o == "-f":
            database = a
        elif o == "-t":
            tags.append(a)

    if not database:
        print("Please specify the database file!")
        usage(sys.argv[0])
        sys.exit(1)

    db = TagDB(database)

    if list_tags:
        print("Previously used tags from database:")
        for t in db.all_tags():
            print("  '%s'" % t)
        print("")

    filenames = list()
    for a in args:
        if os.path.isfile(a):
            filenames.append(os.path.abspath(a))
        if os.path.isdir(a) and recursive:
            for root, dirs, files in os.walk(a):
                for name in files:
                    filenames.append(os.path.abspath(os.path.join(root, name)))

    for filename in filenames:
        if len(tags) == 0:
            actual = db.retrieve(filename)
            if len(actual) > 0:
                print(filename, "===", actual)
        else:
            if delete:
                actual = db.delete(filename, tags)
                if len(actual) > 0:
                    print(filename, "---", actual)
            else:
                db.apply(filename, tags)
                print(filename, "+++", tags)
    
    if len(tags) > 0:
        db.save()
          


Second script, for generating the XSPF playlist from the JSON database:

#!/usr/bin/python3

import json
import sys
from urllib.parse import quote

if len(sys.argv) != 2:
    print("Usage: %s <database>" % (sys.argv[0]))
    sys.exit(1)

fh = open(sys.argv[1], "r")
db = json.load(fh)
fh.close()

playlist = dict()

for filename in db:
    for tag in db[filename]:
        if not tag in playlist:
            playlist[tag] = list()
        playlist[tag].append(filename)

for tag in playlist:
    fh = open("%s.xspf" % (tag), "w")
    fh.write('<?xml version="1.0" encoding="UTF-8"?>\n')
    fh.write('<playlist version="1" xmlns="http://xspf.org/ns/0/">\n')
    fh.write('  <title>%s</title>\n' % (tag))
    fh.write('  <trackList>\n')
    for filename in sorted(playlist[tag]):
        fh.write('    <track>\n')
        fh.write('      <location>file://%s</location>\n' % (quote(filename)))
        fh.write('    </track>\n')
    fh.write('  </trackList>\n')
    fh.write('</playlist>\n')
    fh.close()
          


Topic: Scripts and Code, by Kjetil @ 22/08-2025, Article Link