# -*- coding: utf-8 -*-
# Author: Dylan Jones
# Date: 2023-09-10
import xml.etree.cElementTree as xml
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from ..config import get_config
from ..utils import pretty_xml
Attribs = Dict[str, Any]
[docs]
class XmlElementNotInitializedError(Exception):
"""Raised when an XML element is not initialized."""
def __init__(self, name: str) -> None:
super().__init__(f"XML element {name} is not initialized!")
[docs]
class MasterPlaylistXml:
"""Rekordbox v6 masterPlaylists6.xml file handler.
Rekordbox stores some playlist information in the masterPlaylists6.xml file.
Each playlist is represented by a <PLAYLIST> element, containing the following
attributes:
- Id: The playlist ID in hexadecimal format.
- ParentId: The parent playlist ID in hexadecimal format. The root playlist has
- Attributes: The type of playlist. 0 = normal, 1 = folder, 4 = smart playlist.
- Timestamp: The last time the playlist was updated.
- Lib_Type: ? (0 for palylists/folders)
- CheckType: ? (always 0)
"""
KEYS = ["Id", "ParentId", "Attributes", "Timestamp", "Lib_Type", "CheckType"]
def __init__(self, path: Union[str, Path] = None, db_dir: Union[str, Path] = None):
if path is None:
if db_dir is None:
db_dir = get_config("rekordbox6", "db_dir")
path = Path(db_dir) / "masterPlaylists6.xml"
tree = xml.parse(str(path))
self.path = path
self.root = tree.getroot()
self.product = self.root.find("PRODUCT")
self.playlists = self.root.find("PLAYLISTS")
self._changed = False
@property
def version(self) -> str:
return self.root.attrib["Version"]
@property
def automatic_sync(self) -> str:
return self.root.attrib["AutomaticSync"]
@property
def rekordbox_version(self) -> str:
if self.product is None:
raise XmlElementNotInitializedError("product")
return self.product.attrib["Version"]
@property
def modified(self) -> bool:
return self._changed
[docs]
def get_playlists(self) -> List[Dict[str, Any]]:
"""Returns a list of the attributes of all playlist elements."""
if self.playlists is None:
raise XmlElementNotInitializedError("playlists")
items = list()
for playlist in self.playlists:
items.append(playlist.attrib)
return items
[docs]
def get(self, playlist_id: Union[str, int]) -> Optional[Attribs]:
"""Returns element attribs with the PlaylistID used in the `master.db` database.
Parameters
----------
playlist_id : str or int
The playlist ID used in the main `master.db` database. This id is converted
to hexadecimal format before searching.
Returns
-------
playlist : dict
"""
if self.playlists is None:
raise XmlElementNotInitializedError("playlists")
hex_id = f"{int(playlist_id):X}"
element = self.playlists.find(f'.//NODE[@Id="{hex_id}"]')
if element is None:
return None
attribs: Attribs = dict(element.attrib)
attribs["Attribute"] = int(attribs["Attribute"])
attribs["Timestamp"] = datetime.fromtimestamp(int(attribs["Timestamp"]) / 1000)
attribs["Lib_Type"] = int(attribs["Lib_Type"])
attribs["CheckType"] = int(attribs["CheckType"])
return attribs
[docs]
def add(
self,
playlist_id: str,
parent_id: str,
attribute: int,
updated_at: datetime,
lib_type: int = 0,
check_type: int = 0,
) -> xml.Element:
"""Adds a new element with the PlaylistID used in the `master.db` database.
Parameters
----------
playlist_id : str or int
The playlist ID used in the main `master.db` database. This id is converted
to hexadecimal format before searching.
parent_id : str or int, optional
The parent playlist ID used in the main `master.db` database. This id is
converted to hexadecimal format.
attribute : int, optional
The type of playlist. 0 = normal, 1 = folder, 4 = smart playlist.
updated_at : datetime, optional
The last time the playlist was updated.
lib_type : int, optional
The libarray type. It seems to be always 0 for playlists.
check_type : int, optional
The check type. It seems to be always 0.
Returns
-------
element : xml.Element
The newly created element.
"""
if self.playlists is None:
raise XmlElementNotInitializedError("playlists")
hex_id = f"{int(playlist_id):X}"
parent_id = f"{int(parent_id):X}" if parent_id != "root" else "0"
timestamp = int(updated_at.timestamp() * 1000)
attrib = {
"Id": hex_id,
"ParentId": parent_id,
"Attribute": str(attribute),
"Timestamp": str(timestamp),
"Lib_Type": str(lib_type),
"CheckType": str(check_type),
}
if self.playlists.find(f'.//NODE[@Id="{hex_id}"]') is not None:
raise ValueError(f"Playlist with ID {playlist_id} ({hex_id}) exists.")
element = xml.SubElement(self.playlists, "NODE", attrib=attrib)
self._changed = True
return element
[docs]
def remove(self, playlist_id: Union[str, int]) -> None:
"""Removes the element with the PlaylistID used in the `master.db` database.
Parameters
----------
playlist_id : str or int
The playlist ID used in the main `master.db` database. This id is converted
to hexadecimal format before searching.
"""
if self.playlists is None:
raise XmlElementNotInitializedError("playlists")
hex_id = f"{int(playlist_id):X}"
element = self.playlists.find(f'.//NODE[@Id="{hex_id}"]')
if element is None:
raise ValueError(f"Playlist with ID {playlist_id} ({hex_id}) not found.")
self.playlists.remove(element)
self._changed = True
[docs]
def update(
self,
playlist_id: str,
parent_id: str = None,
attribute: int = None,
updated_at: datetime = None,
lib_type: int = None,
check_type: int = None,
) -> None:
"""Updates the element with the PlaylistID used in the `master.db` database.
Parameters
----------
playlist_id : str or int
The playlist ID used in the main `master.db` database. This id is converted
to hexadecimal format before searching.
parent_id : str or int, optional
The parent playlist ID used in the main `master.db` database. This id is
converted to hexadecimal format.
attribute : int, optional
The type of playlist. 0 = normal, 1 = folder, 4 = smart playlist.
updated_at : datetime, optional
The last time the playlist was updated.
lib_type : int, optional
The libarray type. It seems to be always 0 for playlists.
check_type : int, optional
The check type. It seems to be always 0.
"""
if self.playlists is None:
raise XmlElementNotInitializedError("playlists")
hex_id = f"{int(playlist_id):X}"
element = self.playlists.find(f'.//NODE[@Id="{hex_id}"]')
if element is None:
raise ValueError(f"Playlist with ID {playlist_id} ({hex_id}) not found.")
attribs = dict()
if parent_id is not None:
attribs["ParentId"] = f"{int(parent_id):X}" if parent_id != "root" else "0"
if attribute is not None:
attribs["Attribute"] = str(attribute)
if updated_at is not None:
attribs["Timestamp"] = str(int(updated_at.timestamp() * 1000))
if lib_type is not None:
attribs["Lib_Type"] = str(lib_type)
if check_type is not None:
attribs["CheckType"] = str(check_type)
element.attrib.update(attribs)
self._changed = True
[docs]
def to_string(self, indent: str = None) -> str:
text: str = pretty_xml(self.root, indent, encoding="utf-8")
return text
[docs]
def save(self, path: Union[str, Path] = None, indent: str = None) -> None:
if path is None:
path = self.path
path = str(path)
string = self.to_string(indent)
with open(path, "w") as fh:
fh.write(string)
self._changed = False