Adding DJ Software Support to the Collections Package
Contents
Overview
The collection
package is designed to work with any serialized DJ collection that contains all the information about tracks and playlists.
Each of these three components, Collection
, Playlist
, and Track
, has an interface with which djtools
uses to implement these features.
In order to extend djtools
so that these features can be used for other DJ platforms (Denon, Serato, Traktor, Virtual DJ, etc.), these three components must be subclassed to implement several abstract methods.
Collections
Every Collection
subclass must implement an __init__
that accepts a pathlib.Path
(or a str
if you decorate it with @make_path
).
The __init__
must deserialize the Track
and Playlist
data under the Path
to create a Collection
object.
The other method a Collection
must implement is serialize
which will write the Collection
data in whatever format is native for that DJ platform and return the Path
to it.
Subclasses of Collection
inherit a few methods necessary to execute on the djtools
feature set:
Appending a Playlist
object to the root Playlist
:
Getting the root Playlist
or, if a name
is provided, the list of Playlist
objects with matching names (supports fuzzy matching with the glob
parameter):
Getting the dictionary mapping track IDs to Track
objects:
Set the dictionary mapping track IDs to Track
objects:
Get a dictionary with the sorted set of genre tags and non-genre tags:
Tracks
Subclasses of the Track
class have 16 abstract methods to be implemented, but 14 of those methods are simple getters or setters.
The two primary abstract methods are, again, __init__
and serialize
.
The requirements for a Track
subclass' initialization are only that it parses from the input object the dozen or so attributes that the other abstract methods either get or set:
A Track
subclass must implement a serialize
which returns an exact match of the input object used with __init__
. In other words, it must be the case that input == Track(input).serialize()
:
Let's not enumerate the many getter and setters of the Track
object here, but you can check them out for yourself:
Playlists
A subclass of the Playlist
class must implement five abstract methods for working with a recursive playlist structure.
Like the other components of a collection, the Playlist
class also requires an __init__
and serialize
implementation.
The __init__
method must take an input that's either a playlist folder or a playlist that contains tracks:
As with Track
, Playlist
subclasses must implement a serialize
which returns an exact match of the input object used with __init__
. In other words, it must be the case that input == Playlist(input).serialize()
:
The other abstract methods that must be implemented are:
The get_name
and the is_folder
methods are a simple getter and condition check:
A class method called new_playlist
which can create a Playlist
object from either a list of Playlist
objects or a dictionary of Track
objects:
Appendix
Rekordbox as an example
RekordboxCollection
Rekordbox supports exporting a collection to an XML file which contains two primary sections: a COLLECTION tag with TRACK tags, and a PLAYLISTS tag with NODE tags with either more NODE tags or TRACK tags that have a key referencing the COLLECTION TRACKS. A minimal example XML can be seen in the test data.
The RekordboxCollection
implements __init__
by parsing the XML with BeautifulSoup, deserializing the tracks as a dictionary of RekordboxTrack
objects, and deserializing the playlists into the root node of a RekordboxPlaylist
tree:
The serialize
method of RekordboxCollection
builds a new XML document, populates the COLLECTION tag by serializing the values of the RekordboxTrack
dictionary, and then populates the PLAYLISTS tag by iterating the root RekordboxPlaylist
and serializing its children:
RekordboxTrack
A track in an XML contains all the information about tracks except for what playlists they belong to. This information is stored directly in the attributes of the TRACK tag with the exception of the beat grid and hot cue data which are represented as sub-tags TEMPO and POSITION_MARK, respectively.
The RekordboxTrack
implements __init__
by enumerating the attributes of the input Track tag and deserializing the string values as types that are more useful in Python, such as datetime
objects, lists
, sets
, and numerical values:
The serialize
method of RekordboxTrack
builds a new XML tag for a TRACK and populates the attributes of that tag using the RekordboxTrack
members. Because Rekordbox serializes TRACK tags inside of the PLAYLISTS differently, this method accepts a parameter to indicate that the RekordboxTrack
should serialize containing only its ID:
RekordboxPlaylist
A playlist in an XML is a NODE tag which is a recursive structure that contains other NODE tags. The leaves of this tree are NODE tags which contain only TRACK tags with just a single attribute, KEY, which maps to the TrackID attribute of the TRACK tags under the COLLECTION tag.
NODE tags also have attributes for the Name and Type (folder or not) and either a Count or an Entries attribute which has the number of playlists or number of tracks, respectively.
The __init__
method deserializes a NODE tag by either recursively deserializing its children NODE tags (if it's a folder) or else creating a dictionary of tracks from RekordboxCollection
object's tracks passed as a parameter:
The new_playlist
method creates a new Node tag and deserializes it as a RekordboxPlaylist
before setting its members with either a RekordboxTrack
dictionary or a list of RekordboxPlaylist
objects:
The serialize
method of RekordboxPlaylist
builds a new XML tag for a NODE and populates the attributes and sub-tags recursively: