Source code for pyrekordbox.mysettings.file

# -*- coding: utf-8 -*-
# Author: Dylan Jones
# Date:   2023-02-01

"""Rekordbox My-Setting file handlers."""

import re
from collections.abc import MutableMapping
from pathlib import Path
from typing import Any, Dict, Iterator, Type, Union

from construct import Struct

from . import structs

# fmt: off
CRC16_XMODEM_TABLE = [
    0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
    0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
    0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
    0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
    0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
    0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
    0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
    0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
    0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
    0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
    0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
    0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
    0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
    0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
    0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
    0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
    0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
    0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
    0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
    0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
    0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
    0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
    0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
    0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
    0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
    0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
    0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
    0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
    0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
    0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
    0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
    0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,
]
# fmt: on

RE_INVALID_KEY = re.compile("[_u][0-9]?", flags=re.IGNORECASE)


[docs] def compute_checksum(data: bytes, struct: Struct) -> int: """Computes the CRC16 XModem checksum for My-Setting files. The checksum is calculated over the contents of the `data` field, except for `DJMSETTING.DAT` files where the checksum is calculated over all preceding bytes including the length fields. Parameters ---------- data : bytes The binary filey contents of the My-Setting file for which the checksum is computed. struct : Struct The ``Struct`` of the My-Setting file. This is used for deciding which bytes are used for computing the checksum. Returns ------- crc : int The calculated CRC16-XModem checksum. References ---------- https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-16-xmodem """ start = 0 if struct == structs.DjmMySetting else 104 crc = 0 for byte in data[start:-4]: crc = ((crc << 8) & 0xFF00) ^ CRC16_XMODEM_TABLE[((crc >> 8) & 0xFF) ^ byte] return crc
def _is_valid_key(k: str) -> bool: return not RE_INVALID_KEY.match(k)
[docs] class SettingsFile(MutableMapping): # type: ignore[type-arg] """Base class for the Rekordbox My-Setting file handler. The base class implements the getters and setter defined by the keys and default values in the ``defaults`` class attribute. The keys of the ``defaults`` dictionary, which has to be defined by the inheriting class, defines the valid attributes of the My-Setting file. The values are used as default values if a new (and empty) My-Setting file is initialized. """ struct: Struct defaults: Dict[str, str] version: str = "" # only used by DEVSETTING def __init__(self) -> None: super().__init__() self.parsed = None self._items: Dict[str, str] = dict()
[docs] @classmethod def parse(cls, data: bytes) -> "SettingsFile": """Parses the in-memory data of a Rekordbox settings binary file. Parameters ---------- data : bytes The in-memory binary contents of a Rekordbox settings file. Returns ------- self : SettingsFile The new instance with the parsed file content. """ self = cls() self._parse(data) return self
[docs] @classmethod def parse_file(cls, path: Union[str, Path]) -> "SettingsFile": """Reads and parses a Rekordbox settings binary file. Parameters ---------- path : str or Path The path of a Rekordbox settings file which is used to read the file contents before parsing the binary data. Returns ------- self : SettingsFile The new instance with the parsed file content. See Also -------- SettingsFile.parse: Parses the data of a Rekordbox settings file. """ with open(path, "rb") as fh: data = fh.read() return cls.parse(data)
def _parse(self, data: bytes) -> None: parsed = self.struct.parse(data) keys = filter(_is_valid_key, parsed.data.keys()) items = dict() for key in keys: items[key] = str(parsed.data[key]) self.parsed = parsed self._items.update(items) def __len__(self) -> int: return len(self.defaults.keys()) def __iter__(self) -> Iterator[str]: return iter(self.defaults) def __getitem__(self, key: str) -> str: try: return self._items[key] except KeyError: return self.defaults[key] def __setitem__(self, key: str, value: str) -> None: if key not in self.defaults.keys(): raise KeyError(f"Key {key} not a valid field of {self.__class__.__name__}") self._items[key] = value def __delitem__(self, key: str) -> None: del self._items[key]
[docs] def get(self, key: str, default: str = None) -> Union[str, None]: # type: ignore[override] """Returns the value of a setting of the My-Setting file. If the key is not found in the My-Setting data, but it is present in the ``defaults`` class dictionary, that default value is used. Otherwise, the parameter ``default`` is used as default value. Parameters ---------- key : str The key of the setting. default : Any, optional The default value returned if the setting does not exist in the My-Setting file data or the ``defaults`` dictionary. Returns ------- value : Any The value of the setting. """ try: return self.__getitem__(key) except KeyError: return default
[docs] def set(self, key: str, value: str) -> None: """Sets the value of a setting of the My-Setting file. Parameters ---------- key : str The key of the setting. value : Any The new value for updating the setting. """ self.__setitem__(key, value)
[docs] def build(self) -> bytes: """Constructs the binary data for saving the My-Setting file. Returns ------- byte_data : bytes The binary file contents fot eh My-Setting file. """ # Copy defaults and update with cuirrent data items = self.defaults.copy() items.update(self._items) # Create file data file_items: Dict[str, Any] = {"data": items, "checksum": 0} if self.version: file_items["version"] = self.version # Compute and update checksum. For this the data has to be serialized twice: # Once for generating the checksum and another time for writing the data # with the updated checksum data = self.struct.build(file_items) checksum = compute_checksum(data, self.struct) file_items["checksum"] = checksum # Write data with updated checksum bytedata: bytes = self.struct.build(file_items) return bytedata
[docs] def save(self, path: Union[str, Path]) -> None: """Save the contents of the My-Setting file object. Parameters ---------- path : str The file path used for saving. See Also -------- build: Constructs the binary data of the file. """ data = self.build() with open(path, "wb") as fh: fh.write(data)
def __repr__(self) -> str: return f"{self.__class__.__name__}()"
[docs] class MySettingFile(SettingsFile): """Rekordbox `MYSETTING.DAT` file handler. See Also -------- SettingsFile : Base class implementing getters and setter defined by the keys and default values in the ``defaults`` class attribute. """ struct = structs.MySetting defaults = { "auto_cue": structs.AutoCue.on, "auto_cue_level": structs.AutoCueLevel.memory, "disc_slot_illumination": structs.DiscSlotIllumination.bright, "eject_lock": structs.EjectLock.unlock, "hotcue_autoload": structs.HotCueAutoLoad.on, "hotcue_color": structs.HotCueColor.off, "jog_mode": structs.JogMode.vinyl, "jog_ring_brightness": structs.JogRingBrightness.bright, "jog_ring_indicator": structs.JogRingIndicator.on, "language": structs.Language.english, "lcd_brightness": structs.LCDBrightness.three, "master_tempo": structs.MasterTempo.off, "needle_lock": structs.NeedleLock.lock, "on_air_display": structs.OnAirDisplay.on, "phase_meter": structs.PhaseMeter.type1, "play_mode": structs.PlayMode.single, "quantize": structs.Quantize.on, "quantize_beat_value": structs.QuantizeBeatValue.one, "slip_flashing": structs.SlipFlashing.on, "sync": structs.Sync.off, "tempo_range": structs.TempoRange.ten, "time_mode": structs.TimeMode.remain, }
[docs] class MySetting2File(SettingsFile): """Rekordbox `MYSETTING2.DAT` file handler. See Also -------- SettingsFile : Base class implementing getters and setter defined by the keys and default values in the ``defaults`` class attribute. """ struct = structs.MySetting2 defaults = { "vinyl_speed_adjust": structs.VinylSpeedAdjust.touch, "jog_display_mode": structs.JogDisplayMode.auto, "pad_button_brightness": structs.PadButtonBrightness.three, "jog_lcd_brightness": structs.JogLCDBrightness.three, "waveform_divisions": structs.WaveformDivisions.phrase, "waveform": structs.Waveform.waveform, "beat_jump_beat_value": structs.BeatJumpBeatValue.sixteen, }
[docs] class DjmMySettingFile(SettingsFile): """Rekordbox `DJMMYSETTING.DAT` file handler. See Also -------- SettingsFile : Base class implementing getters and setter defined by the keys and default values in the ``defaults`` class attribute. """ struct = structs.DjmMySetting defaults = { "channel_fader_curve": structs.ChannelFaderCurve.linear, "cross_fader_curve": structs.CrossfaderCurve.fast_cut, "headphones_pre_eq": structs.HeadphonesPreEQ.post_eq, "headphones_mono_split": structs.HeadphonesMonoSplit.stereo, "beat_fx_quantize": structs.BeatFXQuantize.on, "mic_low_cut": structs.MicLowCut.on, "talk_over_mode": structs.TalkOverMode.advanced, "talk_over_level": structs.TalkOverLevel.minus_18db, "midi_channel": structs.MidiChannel.one, "midi_button_type": structs.MidiButtonType.toggle, "display_brightness": structs.MixerDisplayBrightness.five, "indicator_brightness": structs.MixerIndicatorBrightness.three, "channel_fader_curve_long": structs.ChannelFaderCurveLong.exponential, }
[docs] class DevSettingFile(SettingsFile): """Rekordbox `DEVSETTING.DAT` file handler. Warnings -------- The data of the `DEVSETTING.DAT` file is not supported. Only the header can be parsed and written. This class is implemented for completness only. See Also -------- SettingsFile : Base class implementing getters and setter defined by the keys and default values in the ``defaults`` class attribute. """ struct = structs.DevSetting defaults = dict(entries="")
FILES: Dict[str, Type[SettingsFile]] = { "DEVSETTING.DAT": DevSettingFile, "DJMMYSETTING.DAT": DjmMySettingFile, "MYSETTING.DAT": MySettingFile, "MYSETTING2.DAT": MySetting2File, }