use std::env::var;
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
use anyhow::{Result, anyhow, bail};
use flate2::read::ZlibDecoder;
use rusqlite::{Connection, params};
const BLOB: &str = "PN_Pq^*N>(JYe*u^8;Yg76HuZ<mR13S?=>)b9;DpoTXV(6ItkU`}8*m6tx_I{Solh_N#dfe{v=";
const BLOB_KEY: &[u8] = b"657f48f84c437cc1";
fn deobfuscate() -> Result<String> {
let data = base85::decode(BLOB).map_err(|e| anyhow!("base85 decode failed: {e:?}"))?;
let xored: Vec<u8> = data
.iter()
.enumerate()
.map(|(i, b)| b ^ BLOB_KEY[i % BLOB_KEY.len()])
.collect();
let mut decoder = ZlibDecoder::new(&xored[..]);
let mut out = String::new();
decoder.read_to_string(&mut out)?;
Ok(out)
}
fn resolve_key() -> Result<String> {
if let Ok(k) = var("REKORDBOX_KEY") {
let k = k.trim().to_owned();
if !k.is_empty() {
return Ok(k);
}
}
deobfuscate()
}
pub fn rekordbox_app_dir() -> Result<PathBuf> {
#[cfg(target_os = "windows")]
{
var("APPDATA")
.map(|appdata| PathBuf::from(appdata).join("Pioneer/rekordbox"))
.map_err(|_| anyhow!("APPDATA env var not found"))
}
#[cfg(target_os = "macos")]
{
var("HOME")
.map(|home| PathBuf::from(home).join("Library/Pioneer/rekordbox"))
.map_err(|_| anyhow!("HOME env var not found"))
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
compile_error!("Rekordbox only runs on macOS and Windows.");
}
pub struct MasterDb {
pub conn: Connection,
pub app_dir: PathBuf,
}
impl MasterDb {
pub fn open() -> Result<Self> {
let app_dir = rekordbox_app_dir()?;
let db_path = app_dir.join("master.db");
if !db_path.exists() {
bail!("Rekordbox master.db not found at {}", db_path.display());
}
let conn = Connection::open(&db_path)?;
let key = resolve_key()?;
conn.execute_batch(&format!("PRAGMA key = '{key}';"))?;
conn.query_row::<i64, _, _>("SELECT count(*) FROM sqlite_master", [], |r| r.get(0))
.map_err(|e| {
anyhow!(
"failed to decrypt master.db (key may be stale for this rekordbox version): {e}"
)
})?;
Ok(Self { conn, app_dir })
}
pub fn resolve_analysis_path(&self, rel: &str) -> PathBuf {
let stripped = rel.trim_start_matches('/');
self.app_dir.join("share").join(stripped)
}
pub fn read_local_usn(&self) -> Result<i64> {
self.conn
.query_row(
"SELECT int_1 FROM agentRegistry WHERE registry_id = 'localUpdateCount'",
[],
|r| r.get::<_, i64>(0),
)
.map_err(|e| anyhow!("read localUpdateCount: {e}"))
}
pub fn write_local_usn(&self, usn: i64) -> Result<()> {
let ts = now_db_string();
let n = self.conn.execute(
"UPDATE agentRegistry SET int_1 = ?1, updated_at = ?2
WHERE registry_id = 'localUpdateCount'",
params![usn, ts],
)?;
if n != 1 {
bail!("localUpdateCount row missing or updated {n} rows");
}
Ok(())
}
pub fn backup(&self) -> Result<PathBuf> {
let backup_dir = backup_dir()?;
std::fs::create_dir_all(&backup_dir)?;
let stamp = chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
let live = self.app_dir.join("master.db");
let target = backup_dir.join(format!("master.db.{stamp}.bak"));
std::fs::copy(&live, &target)?;
for sidecar in ["master.db-wal", "master.db-shm"] {
let src = self.app_dir.join(sidecar);
if src.exists() {
let dst = backup_dir.join(format!("{sidecar}.{stamp}.bak"));
std::fs::copy(&src, &dst)?;
}
}
Ok(target)
}
}
fn backup_dir() -> Result<PathBuf> {
#[cfg(target_os = "windows")]
{
var("LOCALAPPDATA")
.map(|d| PathBuf::from(d).join("rekord-ripper").join("backups"))
.map_err(|_| anyhow!("LOCALAPPDATA env var not found"))
}
#[cfg(target_os = "macos")]
{
var("HOME")
.map(|h| {
PathBuf::from(h)
.join("Library/Application Support/rekord-ripper/backups")
})
.map_err(|_| anyhow!("HOME env var not found"))
}
}
pub fn rekordbox_running() -> bool {
#[cfg(target_os = "macos")]
{
Command::new("pgrep")
.args(["-x", "rekordbox"])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(target_os = "windows")]
{
let out = Command::new("tasklist")
.args(["/FI", "IMAGENAME eq rekordbox.exe", "/NH"])
.output();
match out {
Ok(o) => String::from_utf8_lossy(&o.stdout).contains("rekordbox.exe"),
Err(_) => false,
}
}
}
#[derive(Clone, Copy, Default)]
pub struct SafetyOpts {
pub bypass_rekordbox_check: bool,
}
pub fn safety_preflight(opts: SafetyOpts) -> Result<()> {
if rekordbox_running() && !opts.bypass_rekordbox_check {
bail!(
"rekordbox is running — refusing to write to master.db. \
Close rekordbox, or pass \
--i-know-rekordbox-is-open-and-may-corrupt-my-data to proceed."
);
}
Ok(())
}
pub fn now_db_string() -> String {
chrono::Utc::now()
.format("%Y-%m-%d %H:%M:%S%.3f +00:00")
.to_string()
}