This is a simple vibecoded python script that allows you to query Virtual DJ databases from the command line. ```plaintext python parse_vdj.py --db D: -t arty +---------------+--------------------------------------------------------+---------+-------+-------------------------------------------------------------------------------------------------+ | Artist | Title | BPM | Key | File Path | |---------------+--------------------------------------------------------+---------+-------+-------------------------------------------------------------------------------------------------| | Ferry Corsten | Punk (Arty Rock-n-Rolla Mix) | 132 | Am | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-13 Punk (Arty Rock-n-Rolla Mix).m4a | | ARTY | The Wall (Arty Re-Mode Radio Edit) [feat. Tania Zygar] | 130.002 | Dm | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-12 The Wall (Arty Re-Mode Radio Ed.m4a | | Reverse | Absolute Reality (Arty Remix) | 129.996 | A# | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-10 Absolute Reality (Arty Remix).m4a | | Tilt | The World Doesn't Know (Arty Remix) | 132 | Gm | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-11 The World Doesn't Know (Arty Re.m4a | | ARTY | Twilight Tonight (Arty Remode Edit) | 130.009 | Eb | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-09 Twilight Tonight (Arty Remode E.m4a | | Sneijder | Away from Here (Arty Remix) | 133.002 | Gm | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-08 Away from Here (Arty Remix).m4a | | Kyau & Albert | Are You Fine? (Arty Remix) | 130.002 | C#m | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-05 Are You Fine_ (Arty Remix).m4a | | D-Mad | She Gave Happiness (Arty Remix Radio Edit) | 132 | F#m | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-04 She Gave Happiness (Arty Remix.m4a | | D-Mad | She Gave Happiness (Arty Remix Radio Edit) | 132 | F#m | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-04 She Gave Happiness (Arty Remix 1.m4a | +---------------+--------------------------------------------------------+---------+-------+-------------------------------------------------------------------------------------------------+ python parse_vdj.py --db d: -t arty -b +131 +---------------+--------------------------------------------+---------+-------+-------------------------------------------------------------------------------------------------+ | Artist | Title | BPM | Key | File Path | |---------------+--------------------------------------------+---------+-------+-------------------------------------------------------------------------------------------------| | Ferry Corsten | Punk (Arty Rock-n-Rolla Mix) | 132 | Am | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-13 Punk (Arty Rock-n-Rolla Mix).m4a | | Tilt | The World Doesn't Know (Arty Remix) | 132 | Gm | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-11 The World Doesn't Know (Arty Re.m4a | | Sneijder | Away from Here (Arty Remix) | 133.002 | Gm | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-08 Away from Here (Arty Remix).m4a | | D-Mad | She Gave Happiness (Arty Remix Radio Edit) | 132 | F#m | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-04 She Gave Happiness (Arty Remix.m4a | | D-Mad | She Gave Happiness (Arty Remix Radio Edit) | 132 | F#m | D:\dj_tracks\A State of Trance 550 (Mixed by Armin va\4-04 She Gave Happiness (Arty Remix 1.m4a | +---------------+--------------------------------------------+---------+-------+-------------------------------------------------------------------------------------------------+ ``` ```py import argparse import os import re import sys import xml.etree.ElementTree as ET from pathlib import Path from typing import Dict, List, Optional, Tuple, Union try: from tabulate import tabulate, tabulate_formats except ImportError: print("Error: 'tabulate' library is required. Install it using: pip install tabulate", file=sys.stderr) sys.exit(1) class VirtualDJDatabase: """Parses a VirtualDJ database.xml file into structured Python objects.""" def __init__(self, db_path: Optional[Union[str, Path]] = None): self.db_path = self._resolve_db_path(db_path) if db_path else self._get_default_db_path() if not self.db_path.exists(): raise FileNotFoundError(f"VirtualDJ database not found at: {self.db_path}") self.tracks: List[Dict] = [] self.version: Optional[str] = None @staticmethod def _is_wsl() -> bool: if sys.platform.startswith("linux"): try: with open("/proc/version", "r") as f: return "microsoft" in f.read().lower() except OSError: return False return False @classmethod def _get_wsl_windows_user(cls) -> Optional[str]: import subprocess try: cmd = ["cmd.exe", "/c", "echo %USERNAME%"] result = subprocess.run(cmd, capture_output=True, text=True, check=True) win_user = result.stdout.strip() return win_user if win_user else None except (subprocess.SubprocessError, FileNotFoundError): return None @classmethod def _resolve_db_path(cls, path_input: Union[str, Path]) -> Path: """Resolves drive letters (D:, E:) or volume names (MyUsb) to a database.xml path.""" p_str = str(path_input).strip() is_wsl = cls._is_wsl() # Handle drive letters (e.g., 'd:', 'D:', 'e:\', 'E:/') drive_match = re.match(r"^([a-zA-Z]):?[/\\]*$", p_str) if drive_match: drive_letter = drive_match.group(1).lower() if is_wsl: drive_root = Path(f"/mnt/{drive_letter}") elif sys.platform.startswith("win"): drive_root = Path(f"{drive_letter.upper()}:\\") else: drive_root = Path(p_str) candidate = drive_root / "VirtualDJ" / "database.xml" if candidate.exists(): return candidate return drive_root / "database.xml" # Handle volume names / custom paths given_path = Path(p_str) # 1. Direct path check (if full path was provided) if given_path.is_file(): return given_path if (given_path / "database.xml").exists(): return given_path / "database.xml" if (given_path / "VirtualDJ" / "database.xml").exists(): return given_path / "VirtualDJ" / "database.xml" # 2. Volume search on macOS (/Volumes/) if sys.platform == "darwin": mac_vol = Path("/Volumes") / p_str if (mac_vol / "VirtualDJ" / "database.xml").exists(): return mac_vol / "VirtualDJ" / "database.xml" if (mac_vol / "database.xml").exists(): return mac_vol / "database.xml" # 3. Volume search on Linux / WSL (/media// or /mnt/) if sys.platform.startswith("linux"): for base_dir in [Path("/media"), Path("/mnt")]: if base_dir.exists(): for match in base_dir.rglob(p_str): if match.is_dir(): if (match / "VirtualDJ" / "database.xml").exists(): return match / "VirtualDJ" / "database.xml" if (match / "database.xml").exists(): return match / "database.xml" return given_path @classmethod def _get_default_db_path(cls) -> Path: if cls._is_wsl(): win_user = cls._get_wsl_windows_user() if win_user: win_home = Path(f"/mnt/c/Users/{win_user}") new_path = win_home / "AppData" / "Local" / "VirtualDJ" / "database.xml" if new_path.exists(): return new_path legacy_path = win_home / "Documents" / "VirtualDJ" / "database.xml" if legacy_path.exists(): return legacy_path raise FileNotFoundError("Could not auto-detect VirtualDJ database in Windows user profile from WSL.") if sys.platform.startswith("win"): appdata = os.environ.get("LOCALAPPDATA") if appdata: new_path = Path(appdata) / "VirtualDJ" / "database.xml" if new_path.exists(): return new_path return Path.home() / "Documents" / "VirtualDJ" / "database.xml" if sys.platform == "darwin": home = Path.home() new_path = home / "Library" / "Application Support" / "VirtualDJ" / "database.xml" if new_path.exists(): return new_path return home / "Documents" / "VirtualDJ" / "database.xml" raise NotImplementedError(f"Unsupported operating system: {sys.platform}") def parse(self) -> List[Dict]: tree = ET.parse(self.db_path) root = tree.getroot() self.version = root.attrib.get("Version") self.tracks = [] for song in root.findall("Song"): track_info = { "file_path": song.attrib.get("FilePath", ""), "title": "", "artist": "", "bpm": None, "key": "", } for child in song: tag = child.tag.lower() if tag == "tags": track_info["title"] = child.attrib.get("Title", "") track_info["artist"] = child.attrib.get("Author", "") if child.attrib.get("Key"): track_info["key"] = child.attrib.get("Key") elif tag == "scan": raw_bpm = child.attrib.get("Bpm") if raw_bpm: try: val = float(raw_bpm) if val > 0: track_info["bpm"] = 60.0 / val if val < 10 else val except ValueError: track_info["bpm"] = None if not track_info["key"] and child.attrib.get("Key"): track_info["key"] = child.attrib.get("Key") self.tracks.append(track_info) return self.tracks def parse_bpm_range(bpm_str: str) -> Tuple[Optional[float], Optional[float]]: """Parses BPM criteria (+128, -128, 120-132) into (min_bpm, max_bpm).""" bpm_str = bpm_str.strip() if bpm_str.startswith("+"): return float(bpm_str[1:]), None elif bpm_str.startswith("-"): return None, float(bpm_str[1:]) elif "-" in bpm_str: parts = bpm_str.split("-") return float(parts[0]), float(parts[1]) else: val = float(bpm_str) return val, val def filter_tracks( tracks: List[Dict], artist_regex: Optional[str] = None, title_regex: Optional[str] = None, bpm_range: Optional[str] = None, keys: Optional[List[str]] = None, ) -> List[Dict]: filtered = [] artist_pattern = re.compile(artist_regex, re.IGNORECASE) if artist_regex else None title_pattern = re.compile(title_regex, re.IGNORECASE) if title_regex else None min_bpm, max_bpm = parse_bpm_range(bpm_range) if bpm_range else (None, None) key_set = {k.strip().lower() for k in keys} if keys else None for track in tracks: if artist_pattern and not artist_pattern.search(track["artist"]): continue if title_pattern and not title_pattern.search(track["title"]): continue if min_bpm is not None or max_bpm is not None: track_bpm = track["bpm"] if track_bpm is None: continue if min_bpm is not None and track_bpm < min_bpm: continue if max_bpm is not None and track_bpm > max_bpm: continue if key_set: track_key = track["key"].lower() if track_key not in key_set: continue filtered.append(track) return filtered def main(): parser = argparse.ArgumentParser( description="Query and filter a VirtualDJ database.xml file." ) parser.add_argument( "--db", type=str, help="Path, drive letter (e.g. D:), or volume name for database.xml", ) parser.add_argument( "-a", "--artist", type=str, help="Regex filter for track artist", ) parser.add_argument( "-t", "--title", type=str, help="Regex filter for track title", ) parser.add_argument( "-b", "--bpm", type=str, help="BPM range: +128 (>=128), -128 (<=128), or 120-132 (range)", ) parser.add_argument( "-k", "--key", type=str, help="Comma-separated list of musical keys (e.g., 8A,8B,9A)", ) parser.add_argument( "-f", "--table-format", type=str, default="psql", help="Output format for tabulate (default: psql)", ) parser.add_argument( "--list-table-formats", action="store_true", help="List available table formats and exit", ) args = parser.parse_args() if args.list_table_formats: print("Available tabulate table formats:") for fmt in tabulate_formats: print(f" - {fmt}") sys.exit(0) try: vdj_db = VirtualDJDatabase(db_path=args.db) tracks = vdj_db.parse() except Exception as err: print(f"Error loading database: {err}", file=sys.stderr) sys.exit(1) key_list = [k.strip() for k in args.key.split(",")] if args.key else None results = filter_tracks( tracks, artist_regex=args.artist, title_regex=args.title, bpm_range=args.bpm, keys=key_list, ) table_data = [] for t in results: bpm_fmt = f"{t['bpm']:.3f}" if t["bpm"] is not None else "" table_data.append([t["artist"], t["title"], bpm_fmt, t["key"], t["file_path"]]) headers = ["Artist", "Title", "BPM", "Key", "File Path"] try: print(tabulate(table_data, headers=headers, tablefmt=args.table_format)) except ValueError as e: print(f"Format error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main() ``` ## Prompt Write a Python CLI script using `argparse` to parse and query VirtualDJ `database.xml` files cross-platform (native Windows, macOS, and WSL). ### Requirements **Database Resolution (`VirtualDJDatabase` Class)** * **Default Locations:** If `--db` is omitted, auto-detect the default user database location: * **Windows:** Check `%LOCALAPPDATA%\VirtualDJ\database.xml` (VDJ 2023+), falling back to `~/Documents/VirtualDJ/database.xml`. * **macOS:** Check `~/Library/Application Support/VirtualDJ/database.xml`, falling back to `~/Documents/VirtualDJ/database.xml`. * **WSL:** Detect WSL via `/proc/version`. Query `cmd.exe /c echo %USERNAME%` to resolve the Windows user profile, then locate the database under `/mnt/c/Users//AppData/Local/VirtualDJ/database.xml` (or legacy `Documents`). * **Volume/Drive Specifiers:** If `--db` is passed as a drive letter (e.g., `D:`, `e:`) or volume name (e.g., `MyUsbStick`), check mounted locations (`/mnt/` under WSL, `D:\` on Windows, `/Volumes/` on macOS, `/media/` or `/mnt/` on Linux) for `VirtualDJ/database.xml` or `database.xml` before falling back to direct path evaluation. **XML Parsing & Data Extraction** * Parse `` elements using `xml.etree.ElementTree`. * Map the `` attribute `Author` to the track's artist field (`track_info["artist"]`), and read `Title` and `Key`. * Read BPM from ``. VirtualDJ stores BPM as period duration (seconds per beat). Convert it to beats per minute ($\text{BPM} = 60 / \text{val}$) if the raw float value is $<10$. * Extract `FilePath` from `` attributes. **Filtering (`filter_tracks`)** * **`-a`, `--artist`:** Case-insensitive regex search against the artist name (`Author`). * **`-t`, `--title`:** Case-insensitive regex search against track title. * **`-b`, `--bpm`:** Support exact values (`128`), threshold filters (`+128` for $\ge 128$, `-128` for $\le 128$), and range filters (`120-132` for $120 \le \text{BPM} \le 132$). * **`-k`, `--key`:** Comma-separated list of musical keys (e.g., `8A,8B,9A`), evaluated case-insensitively. **CLI & Output Formatting** * Format query output using the `tabulate` library displaying **Artist**, **Title**, **BPM** (formatted to 3 decimal places), **Key**, and **File Path**. * Set the default `--table-format` to `psql` (`-f` / `--table-format` to override). * Include a `--list-table-formats` flag that prints available `tabulate` format strings and exits cleanly. * Wrap database resolution/parsing errors in informative `sys.stderr` error exits.