use std::env::var;
use std::io::Read;
use std::path::PathBuf;
use std::process::{Command, Stdio};
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 use crate::paths::rekordbox_app_dir;
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 = unique_stamp(&backup_dir);
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)
}
}
use crate::paths::backup_dir;
fn unique_stamp(dir: &std::path::Path) -> String {
let base = chrono::Utc::now().format("%Y%m%dT%H%M%S%3fZ").to_string();
let taken = |s: &str| dir.join(format!("master.db.{s}.bak")).exists();
if !taken(&base) {
return base;
}
(1..)
.map(|n| format!("{base}-{n}"))
.find(|s| !taken(s))
.unwrap_or(base)
}
pub fn rekordbox_running() -> bool {
#[cfg(target_os = "macos")]
{
Command::new("pgrep")
.args(["-x", "rekordbox"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.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()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn back_to_back_backups_never_share_a_name() {
let dir = std::env::temp_dir().join(format!("rr-stamp-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut stamps = Vec::new();
for _ in 0..5 {
let s = unique_stamp(&dir);
std::fs::write(dir.join(format!("master.db.{s}.bak")), b"x").unwrap();
stamps.push(s);
}
let mut unique = stamps.clone();
unique.sort();
unique.dedup();
assert_eq!(unique.len(), stamps.len(), "collided: {stamps:?}");
let _ = std::fs::remove_dir_all(&dir);
}
}