meta: id: rekordbox_pdb title: DeviceSQL database export (probably generated by rekordbox) application: rekordbox file-extension: - pdb license: EPL-1.0 endian: le doc: | This is a relational database format designed to be efficiently used by very low power devices (there were deployments on 16 bit devices with 32K of RAM). Today you are most likely to encounter it within the Pioneer Professional DJ ecosystem, because it is the format that their rekordbox software uses to write USB and SD media which can be mounted in DJ controllers and used to play and mix music. It has been reverse-engineered to facilitate sophisticated integrations with light and laser shows, videos, and other musical instruments, by supporting deep knowledge of what is playing and what is coming next through monitoring the network communications of the players. The file is divided into fixed-size blocks. The first block has a header that establishes the block size, and lists the tables available in the database, identifying their types and the index of the first of the series of linked pages that make up that table. Each table is made up of a series of rows which may be spread across any number of pages. The pages start with a header describing the page and linking to the next page. The rest of the page is used as a heap: rows are scattered around it, and located using an index structure that builds backwards from the end of the page. Each row of a given type has a fixed size structure which links to any variable-sized strings by their offsets within the page. As changes are made to the table, some records may become unused, and there may be gaps within the heap that are too small to be used by other data. There is a bit map in the row index that identifies which rows are actually present. Rows that are not present must be ignored: they do not contain valid (or even necessarily well-formed) data. The majority of the work in reverse-engineering this format was performed by @henrybetts and @flesniak, for which I am hugely grateful. @GreyCat helped me learn the intricacies (and best practices) of Kaitai far faster than I would have managed on my own. doc-ref: https://github.com/Deep-Symmetry/crate-digger/blob/master/doc/Analysis.pdf params: - id: is_ext type: bool doc: | Indicates whether the database schema is export or exportExt. Set this to true when parsing an exportExt.pdb file. seq: - type: u4 doc: | Unknown purpose, perhaps an unoriginal signature, seems to always have the value 0. - id: len_page type: u4 doc: | The database page size, in bytes. Pages are referred to by index, so this size is needed to calculate their offset, and table pages have a row index structure which is built from the end of the page backwards, so finding that also requires this value. - id: num_tables type: u4 doc: | Determines the number of table entries that are present. Each table is a linked list of pages containing rows of a particular type. - id: next_unused_page type: u4 doc: | @flesniak said: "Not used as any `empty_candidate`, points past the end of the file." - type: u4 - id: sequence type: u4 doc: | Sequence number incremented during every edit of the database. - id: gap contents: [0, 0, 0, 0] doc: | Only exposed until https://github.com/kaitai-io/kaitai_struct/issues/825 can be fixed. - id: tables type: table repeat: expr repeat-expr: num_tables doc: | Describes and links to the tables present in the database. types: table: doc: | Each table is a linked list of pages containing rows of a single type. This header describes the nature of the table and links to its pages by index. seq: - id: type type: u4 enum: page_type if: not _root.is_ext doc: | Identifies the kind of rows that are found in this table. - id: type_ext type: u4 enum: page_type_ext if: _root.is_ext doc: | Identifies the kind of rows that are found in this table from an exportExt.pdb file. - id: empty_candidate type: u4 - id: first_page type: page_ref doc: | Links to the chain of pages making up that table. The first page seems to always contain similar garbage patterns and zero rows, but the next page it links to contains the start of the meaningful data rows. - id: last_page type: page_ref doc: | Holds the index of the last page that makes up this table. When following the linked list of pages of the table, you either need to stop when you reach this page, or when you notice that the `next_page` link you followed took you to a page of a different `type`. -webide-representation: '{type}' page_ref: doc: | An index which points to a table page (its offset can be found by multiplying the index by the `page_len` value in the file header). This type allows the linked page to be lazy loaded. seq: - id: index type: u4 doc: | Identifies the desired page number. instances: body: doc: | When referenced, loads the specified page and parses its contents appropriately for the type of data it contains. io: _root._io pos: _root.len_page * index size: _root.len_page type: page page: doc: | A table page, consisting of a short header describing the content of the page and linking to the next page, followed by a heap in which row data is found. At the end of the page there is an index which locates all rows present in the heap via their offsets past the end of the page header. meta: bit-endian: le seq: - id: gap contents: [0, 0, 0, 0] doc: | Only exposed until https://github.com/kaitai-io/kaitai_struct/issues/825 can be fixed. - id: page_index doc: Matches the index we used to look up the page, sanity check? type: u4 - id: type type: u4 enum: page_type if: not _root.is_ext doc: | Identifies the type of information stored in the rows of this page. - id: type_ext type: u4 enum: page_type_ext if: _root.is_ext doc: | Identifies the type of information stored in the rows of this page in an exportExt.pdb file. - id: next_page doc: | Index of the next page containing this type of rows. Points past the end of the file if there are no more. type: page_ref - id: sequence type: u4 doc: | Sequence number updated to the value of sequence from the database header when this page is edited. The value is copied before the value of sequence in the database header is incremented, i.e. the one in the database header is the "next" page sequence number. - size: 4 - id: num_row_offsets type: b13 doc: | Seems to hold the number of row offsets that have ever been allocated, including those that are no longer valid. - id: num_rows type: b11 doc: The number of valid rows currently present in the page. - id: page_flags type: u1 doc: | @flesniak said: "strange pages: 0x44, 0x64; otherwise seen: 0x24, 0x34" - id: free_size type: u2 doc: | Unused space (in bytes) in the page heap, excluding the row index at end of page. - id: used_size type: u2 doc: | The number of bytes that are in use in the page heap. - id: transaction_row_count type: u2 doc: | The number of rows touched in the last transaction on this page, or 0x1fff if the last transaction failed. - id: transaction_row_index type: u2 doc: | The index of the first row touched in the last transaction on this page, or 0x1fff if the last transaction failed. - type: u2 doc: | @flesniak said: "1004 for strange blocks, 0 otherwise" - type: u2 doc: | @flesniak said: "always 0 except 1 for history pages, num entries for strange pages?" - id: heap size-eos: true if: 'false' # never true, but stores pos instances: is_data_page: value: page_flags & 0x40 == 0 -webide-parse-mode: eager heap_pos: value: _io.pos num_row_groups: value: '(num_row_offsets - 1) / 16 + 1' doc: | The number of row groups that are present in the index. Each group can hold up to sixteen rows, but `row_present_flags` must be consulted to determine whether each is valid. row_groups: type: 'row_group(_index)' repeat: expr repeat-expr: num_row_groups doc: | The actual row groups making up the row index. Each group can hold up to sixteen rows. Non-data pages do not have actual rows, and attempting to parse them can crash. if: is_data_page row_group: doc: | A group of row indices, which are built backwards from the end of the page. Holds up to sixteen row offsets, along with a bit mask that indicates whether each row is actually present in the table. params: - id: group_index type: u2 doc: | Identifies which group is being generated. They build backwards from the end of the page. instances: base: value: '_root.len_page - (group_index * 0x24)' doc: | The starting point of this group of row indices. row_present_flags: pos: base - 4 type: u2 doc: | Each bit specifies whether a particular row is present. The low order bit corresponds to the first row in this index, whose offset immediately precedes these flag bits. The second bit corresponds to the row whose offset precedes that, and so on. -webide-parse-mode: eager transaction_row_flags: pos: base type: u2 doc: | Each bit specifies whether a particular row was touched by the last transaction on this row group, using the same layout as row_present_flags. rows: type: row_ref(_index) repeat: expr repeat-expr: 16 doc: | The row offsets in this group. row_ref: doc: | An offset which points to a row in the table, whose actual presence is controlled by one of the bits in `row_present_flags`. This instance allows the row itself to be lazily loaded, unless it is not present, in which case there is no content to be loaded. params: - id: row_index type: u2 doc: | Identifies which row within the row index this reference came from, so the correct flag can be checked for the row presence and the correct row offset can be found. instances: ofs_row: pos: '_parent.base - (6 + (2 * row_index))' type: u2 doc: | The offset of the start of the row (in bytes past the end of the page header). row_base: value: ofs_row + _parent._parent.heap_pos doc: | The location of this row relative to the start of the page. A variety of pointers (such as all device_sql_string values) are calculated with respect to this position. present: value: '(((_parent.row_present_flags >> row_index) & 1) != 0 ? true : false)' doc: | Indicates whether the row index considers this row to be present in the table. Will be `false` if the row has been deleted. -webide-parse-mode: eager body: pos: row_base if: present and not _root.is_ext type: switch-on: _parent._parent.type cases: 'page_type::albums': album_row 'page_type::artists': artist_row 'page_type::artwork': artwork_row 'page_type::colors': color_row 'page_type::genres': genre_row 'page_type::keys': key_row 'page_type::labels': label_row 'page_type::playlist_tree': playlist_tree_row 'page_type::playlist_entries': playlist_entry_row 'page_type::history_playlists': history_playlist_row 'page_type::history_entries': history_entry_row 'page_type::tracks': track_row doc: | The actual content of the row, as long as it is present. -webide-parse-mode: eager body_ext: pos: row_base if: present and _root.is_ext type: switch-on: _parent._parent.type_ext cases: 'page_type_ext::tags': tag_row 'page_type_ext::tag_tracks': tag_track_row doc: | The actual content of the row in an exportExt.pdb file, as long as it is present. -webide-parse-mode: eager -webide-representation: '{body.name.body.text}{body.title.body.text}{body_ext.name.body.text} ({body.id}{body_ext.id})' album_row: doc: | A row that holds an album name and ID. seq: - id: subtype type: u2 doc: | Usually 0x80, but 0x84 means we have a long name offset embedded in the row. - id: index_shift type: u2 doc: TODO name from @flesniak, but what does it mean? - type: u4 - id: artist_id type: u4 doc: | Identifies the artist associated with the album. - id: id type: u4 doc: | The unique identifier by which this album can be requested and linked from other rows (such as tracks). - type: u4 - type: u1 doc: | @flesniak says: "always 0x03, maybe an unindexed empty string" - id: ofs_name_near type: u1 doc: | The location of the variable-length name string, relative to the start of this row, unless subtype is 0x84. instances: ofs_name_far: pos: _parent.row_base + 0x16 type: u2 doc: | For names that might be further than 0xff bytes from the start of this row, this holds a two-byte offset, and is signalled by the subtype value. if: subtype & 0x04 == 0x04 name: pos: '_parent.row_base + (subtype & 0x04 == 0x04? ofs_name_far : ofs_name_near)' type: device_sql_string doc: | The name of this album. -webide-parse-mode: eager artist_row: doc: | A row that holds an artist name and ID. seq: - id: subtype type: u2 doc: | Usually 0x60, but 0x64 means we have a long name offset embedded in the row. - id: index_shift type: u2 doc: TODO name from @flesniak, but what does it mean? - id: id type: u4 doc: | The unique identifier by which this artist can be requested and linked from other rows (such as tracks). - type: u1 doc: | @flesniak says: "always 0x03, maybe an unindexed empty string" - id: ofs_name_near type: u1 doc: | The location of the variable-length name string, relative to the start of this row, unless subtype is 0x64. instances: ofs_name_far: pos: _parent.row_base + 0x0a type: u2 doc: | For names that might be further than 0xff bytes from the start of this row, this holds a two-byte offset, and is signalled by the subtype value. if: subtype & 0x04 == 0x04 name: pos: '_parent.row_base + (subtype & 0x04 == 0x04? ofs_name_far : ofs_name_near)' type: device_sql_string doc: | The name of this artist. -webide-parse-mode: eager artwork_row: doc: | A row that holds the path to an album art image file and the associated artwork ID. seq: - id: id type: u4 doc: | The unique identifier by which this art can be requested and linked from other rows (such as tracks). - id: path type: device_sql_string doc: | The variable-length file path string at which the art file can be found. -webide-representation: '{path.body.text}' color_row: doc: | A row that holds a color name and the associated ID. seq: - size: 5 - id: id type: u2 doc: | The unique identifier by which this color can be requested and linked from other rows (such as tracks). - type: u1 - id: name type: device_sql_string doc: | The variable-length string naming the color. genre_row: doc: | A row that holds a genre name and the associated ID. seq: - id: id type: u4 doc: | The unique identifier by which this genre can be requested and linked from other rows (such as tracks). - id: name type: device_sql_string doc: | The variable-length string naming the genre. key_row: doc: | A row that holds a musical key and the associated ID. seq: - id: id type: u4 doc: | The unique identifier by which this key can be requested and linked from other rows (such as tracks). - id: id2 type: u4 doc: | Seems to be a second copy of the ID? - id: name type: device_sql_string doc: | The variable-length string naming the key. label_row: doc: | A row that holds a label name and the associated ID. seq: - id: id type: u4 doc: | The unique identifier by which this label can be requested and linked from other rows (such as tracks). - id: name type: device_sql_string doc: | The variable-length string naming the label. playlist_tree_row: doc: | A row that holds a playlist name, ID, indication of whether it is an ordinary playlist or a folder of other playlists, a link to its parent folder, and its sort order. seq: - id: parent_id type: u4 doc: | The ID of the `playlist_tree_row` in which this one can be found, or `0` if this playlist exists at the root level. - size: 4 - id: sort_order type: u4 doc: | The order in which the entries of this playlist are sorted. - id: id type: u4 doc: | The unique identifier by which this playlist or folder can be requested and linked from other rows. - id: raw_is_folder type: u4 doc: | Has a non-zero value if this is actually a folder rather than a playlist. - id: name type: device_sql_string doc: | The variable-length string naming the playlist. instances: is_folder: value: raw_is_folder != 0 -webide-parse-mode: eager playlist_entry_row: doc: | A row that associates a track with a position in a playlist. seq: - id: entry_index type: u4 doc: | The position within the playlist represented by this entry. - id: track_id type: u4 doc: | The track found at this position in the playlist. - id: playlist_id type: u4 doc: | The playlist to which this entry belongs. history_playlist_row: doc: | A row that holds a history playlist ID and name, linking to the track IDs captured during a performance on the player. seq: - id: id type: u4 doc: | The unique identifier by which this history playlist can be requested. - id: name type: device_sql_string doc: | The variable-length string naming the playlist. history_entry_row: doc: | A row that associates a track with a position in a history playlist. seq: - id: track_id type: u4 doc: | The track found at this position in the playlist. - id: playlist_id type: u4 doc: | The history playlist to which this entry belongs. - id: entry_index type: u4 doc: | The position within the playlist represented by this entry. track_row: doc: | A row that describes a track that can be played, with many details about the music, and links to other tables like artists, albums, keys, etc. seq: - id: subtype type: u2 doc: | Seems to always be 0x24, and bit 0x04 being set means it uses sixteen-bit offsets, as it does in other tables. Track rows are always big enough to need that size offsets. - id: index_shift type: u2 doc: TODO name from @flesniak, but what does it mean? - id: bitmask type: u4 doc: TODO what do the bits mean? - id: sample_rate type: u4 doc: | Playback sample rate of the audio file. - id: composer_id type: u4 doc: | References a row in the artist table if the composer is known. - id: file_size type: u4 doc: | The length of the audio file, in bytes. - type: u4 doc: | Some ID? Purpose as yet unknown. - type: u2 doc: | From @flesniak: "always 19048?" - type: u2 doc: | From @flesniak: "always 30967?" - id: artwork_id type: u4 doc: | References a row in the artwork table if there is album art. - id: key_id type: u4 doc: | References a row in the keys table if the track has a known main musical key. - id: original_artist_id type: u4 doc: | References a row in the artwork table if this is a cover performance and the original artist is known. - id: label_id type: u4 doc: | References a row in the labels table if the track has a known record label. - id: remixer_id type: u4 doc: | References a row in the artists table if the track has a known remixer. - id: bitrate type: u4 doc: | Playback bit rate of the audio file. - id: track_number type: u4 doc: | The position of the track within an album. - id: tempo type: u4 doc: | The tempo at the start of the track in beats per minute, multiplied by 100. - id: genre_id type: u4 doc: | References a row in the genres table if the track has a known musical genre. - id: album_id type: u4 doc: | References a row in the albums table if the track has a known album. - id: artist_id type: u4 doc: | References a row in the artists table if the track has a known performer. - id: id type: u4 doc: | The id by which this track can be looked up; players will report this value in their status packets when they are playing the track. - id: disc_number type: u2 doc: | The number of the disc on which this track is found, if it is known to be part of a multi-disc album. - id: play_count type: u2 doc: | The number of times this track has been played. - id: year type: u2 doc: | The year in which this track was released. - id: sample_depth type: u2 doc: | The number of bits per sample of the audio file. - id: duration type: u2 doc: | The length, in seconds, of the track when played at normal speed. - type: u2 doc: | From @flesniak: "always 41?" - id: color_id type: u1 doc: | References a row in the colors table if the track has been assigned a color. - id: rating type: u1 doc: | The number of stars to display for the track, 0 to 5. - type: u2 doc: | From @flesniak: "always 1?" - type: u2 doc: | From @flesniak: "alternating 2 or 3" - id: ofs_strings type: u2 repeat: expr repeat-expr: 21 doc: | The location, relative to the start of this row, of a variety of variable-length strings. instances: isrc: type: device_sql_string pos: _parent.row_base + ofs_strings[0] doc: | International Standard Recording Code of track when known (in mangled format). -webide-parse-mode: eager texter: type: device_sql_string pos: _parent.row_base + ofs_strings[1] doc: | A string of unknown purpose, which @flesniak named. -webide-parse-mode: eager unknown_string_2: type: device_sql_string pos: _parent.row_base + ofs_strings[2] doc: | A string of unknown purpose; @flesniak said "thought track number -> wrong!" unknown_string_3: type: device_sql_string pos: _parent.row_base + ofs_strings[3] doc: | A string of unknown purpose; @flesniak said "strange strings, often zero length, sometimes low binary values 0x01/0x02 as content" unknown_string_4: type: device_sql_string pos: _parent.row_base + ofs_strings[4] doc: | A string of unknown purpose; @flesniak said "strange strings, often zero length, sometimes low binary values 0x01/0x02 as content" -webide-parse-mode: eager message: type: device_sql_string pos: _parent.row_base + ofs_strings[5] doc: | A string of unknown purpose, which @flesniak named. -webide-parse-mode: eager kuvo_public: type: device_sql_string pos: _parent.row_base + ofs_strings[6] doc: | A string whose value is always either empty or "ON", and which apparently for some insane reason is used, rather than a single bit somewhere, to control whether the track information is visible on Kuvo. -webide-parse-mode: eager autoload_hot_cues: type: device_sql_string pos: _parent.row_base + ofs_strings[7] doc: | A string whose value is always either empty or "ON", and which apparently for some insane reason is used, rather than a single bit somewhere, to control whether hot-cues are auto-loaded for the track. -webide-parse-mode: eager unknown_string_5: type: device_sql_string pos: _parent.row_base + ofs_strings[8] doc: | A string of unknown purpose. -webide-parse-mode: eager unknown_string_6: type: device_sql_string pos: _parent.row_base + ofs_strings[9] doc: | A string of unknown purpose, usually empty. -webide-parse-mode: eager date_added: type: device_sql_string pos: _parent.row_base + ofs_strings[10] doc: | A string containing the date this track was added to the collection. -webide-parse-mode: eager release_date: type: device_sql_string pos: _parent.row_base + ofs_strings[11] doc: | A string containing the date this track was released, if known. -webide-parse-mode: eager mix_name: type: device_sql_string pos: _parent.row_base + ofs_strings[12] doc: | A string naming the remix of the track, if known. -webide-parse-mode: eager unknown_string_7: type: device_sql_string pos: _parent.row_base + ofs_strings[13] doc: | A string of unknown purpose, usually empty. -webide-parse-mode: eager analyze_path: type: device_sql_string pos: _parent.row_base + ofs_strings[14] doc: | The file path of the track analysis, which allows rapid seeking to particular times in variable bit-rate files, jumping to particular beats, visual waveform previews, and stores cue points and loops. -webide-parse-mode: eager analyze_date: type: device_sql_string pos: _parent.row_base + ofs_strings[15] doc: | A string containing the date this track was analyzed by rekordbox. -webide-parse-mode: eager comment: type: device_sql_string pos: _parent.row_base + ofs_strings[16] doc: | The comment assigned to the track by the DJ, if any. -webide-parse-mode: eager title: type: device_sql_string pos: _parent.row_base + ofs_strings[17] doc: | The title of the track. -webide-parse-mode: eager unknown_string_8: type: device_sql_string pos: _parent.row_base + ofs_strings[18] doc: | A string of unknown purpose, usually empty. -webide-parse-mode: eager filename: type: device_sql_string pos: _parent.row_base + ofs_strings[19] doc: | The file name of the track audio file. -webide-parse-mode: eager file_path: type: device_sql_string pos: _parent.row_base + ofs_strings[20] doc: | The file path of the track audio file. -webide-parse-mode: eager tag_row: doc: | A row that holds a tag name and its ID (found only in exportExt.pdb files). seq: - id: subtype type: u2 doc: | Usually 0x0680, but 0x0684 means we have a long name offset embedded in the row. - id: tag_index type: u2 doc: | Increasing index for each row in multiples of 0x20. - type: u8 doc: | Seems to always be zero. - id: category type: u4 doc: | The ID of the tag category this tag belongs to. If this row represents a tag category, this field is zero. - id: category_pos type: u4 doc: | The zero-based position of this tag in its category. If this row represents a tag category, the zero-based position of the category itself in the category list. - id: id type: u4 doc: | The ID of this tag or tag category. Referenced by tag_track_row if this row is a tag. - id: raw_is_category type: u4 doc: | Non-zero when this row stores a tag category instead of a tag. - type: u1 doc: | @flesniak says: "always 0x03, maybe an unindexed empty string" - id: ofs_name_near type: u1 doc: | The location of the variable-length name string, relative to the start of this row, unless subtype is 0x64. - id: ofs_unknown_near type: u1 doc: | Offset to a string that seems always to be empty. instances: is_category: value: raw_is_category != 0 doc: | Indicates whether this row stores a tag category instead of a tag. -webide-parse-mode: eager ofs_name_far: pos: _parent.row_base + 0x1e type: u2 doc: | For names that might be further than 0xff bytes from the start of this row, this holds a two-byte offset, and is signalled by the subtype value. if: subtype & 0x04 == 0x04 name: pos: '_parent.row_base + (subtype & 0x04 == 0x04? ofs_name_far : ofs_name_near)' type: device_sql_string doc: | The name of this tag or tag category. -webide-parse-mode: eager tag_track_row: doc: | A row that associates a track and a tag (found only in exportExt.pdb files). seq: - type: u4 doc: | Seems to always be zero. - id: track_id type: u4 doc: | The ID of the track that has a tag assigned to it. - id: tag_id type: u4 doc: | The ID of the tag that has been assigned to a track. - type: u4 doc: | Seems to always be 0x03 0x00 0x00 0x00. device_sql_string: doc: | A variable length string which can be stored in a variety of different encodings. seq: - id: length_and_kind type: u1 doc: | Mangled length of an ordinary ASCII string if odd, or a flag indicating another encoding with a longer length value to follow. - id: body type: switch-on: length_and_kind cases: 0x40: device_sql_long_ascii 0x90: device_sql_long_utf16le _: device_sql_short_ascii(length_and_kind) -webide-parse-mode: eager -webide-representation: '{body.text}' device_sql_short_ascii: doc: | An ASCII-encoded string up to 127 bytes long. params: - id: length_and_kind type: u1 doc: | Contains the actual length, incremented, doubled, and incremented again. Go figure. seq: - id: text type: str size: length - 1 encoding: ASCII doc: | The content of the string. instances: length: value: '(length_and_kind >> 1)' doc: | the length extracted of the entire device_sql_short_ascii type -webide-parse-mode: eager device_sql_long_ascii: doc: | An ASCII-encoded string preceded by a two-byte length field in a four-byte header. seq: - id: length type: u2 doc: | Contains the length of the string in bytes. - type: u1 - id: text type: str size: length - 4 encoding: ASCII doc: | The content of the string. device_sql_long_utf16le: doc: | A UTF-16LE-encoded string preceded by a two-byte length field in a four-byte header. seq: - id: length type: u2 doc: | Contains the length of the string in bytes, plus four trailing bytes that must be ignored. - type: u1 - id: text type: str size: length - 4 encoding: UTF-16LE doc: | The content of the string. enums: page_type: 0: id: tracks doc: | Holds rows describing tracks, such as their title, artist, genre, artwork ID, playing time, etc. 1: id: genres doc: | Holds rows naming musical genres, for reference by tracks and searching. 2: id: artists doc: | Holds rows naming artists, for reference by tracks and searching. 3: id: albums doc: | Holds rows naming albums, for reference by tracks and searching. 4: id: labels doc: | Holds rows naming music labels, for reference by tracks and searching. 5: id: keys doc: | Holds rows naming musical keys, for reference by tracks and searching. 6: id: colors doc: | Holds rows naming color labels, for reference by tracks and searching. 7: id: playlist_tree doc: | Holds rows that describe the hierarchical tree structure of available playlists and folders grouping them. 8: id: playlist_entries doc: | Holds rows that enumerate the tracks found in playlists and the playlists they belong to. 9: id: unknown_9 10: id: unknown_10 11: id: history_playlists doc: | Holds rows that assign IDs and give names to the history playlists that have been captured by the player, such as "HISTORY 001". 12: id: history_entries doc: | Holds rows that enumerate the tracks found in history playlists and the playlists they belong to. 13: id: artwork doc: | Holds rows pointing to album artwork images. 14: id: unknown_14 15: id: unknown_15 16: id: columns doc: | TODO figure out and explain 17: id: unknown_17 18: id: unknown_18 19: id: history doc: | Holds information to help rekordbox sync history playlists. page_type_ext: 0: id: unknown_0 1: id: unknown_1 2: id: unknown_2 3: id: tags doc: | Holds rows naming tags. 4: id: tag_tracks doc: | Holds rows associating tags and tracks. 5: id: unknown_5 6: id: unknown_6 7: id: unknown_7 8: id: unknown_8