diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 00000000..f0c6e67e --- /dev/null +++ b/src/device.rs @@ -0,0 +1,476 @@ +// Copyright (c) 2023 Jan Holthuis +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy +// of the MPL was not distributed with this file, You can obtain one at +// http://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 + +//! High-level API for working with Rekordbox device exports. + +use crate::{ + pdb::{Header, Page, PageType, PlaylistTreeNode, PlaylistTreeNodeId, Row, Track, TrackId}, + setting, + setting::Setting, +}; +use binrw::{BinRead, ReadOptions}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +/// Represents a Rekordbox device export. +#[derive(Debug, PartialEq)] +pub struct DeviceExport { + path: PathBuf, + pdb: Option, + devsetting: Option, + djmmysetting: Option, + mysetting: Option, + mysetting2: Option, +} + +impl DeviceExport { + /// Load device export from the given path. + /// + /// The path should contain a `PIONEER` directory. + #[must_use] + pub fn new(path: PathBuf) -> Self { + Self { + path, + pdb: None, + devsetting: None, + djmmysetting: None, + mysetting: None, + mysetting2: None, + } + } + + /// Get the device path. + #[must_use] + pub fn get_path(&self) -> &Path { + &self.path + } + + fn read_setting_file(path: &PathBuf) -> crate::Result { + let mut reader = std::fs::File::open(path)?; + let setting = Setting::read(&mut reader)?; + Ok(setting) + } + + /// Load setting files. + pub fn load_settings(&mut self) -> crate::Result<()> { + let path = self.path.join("PIONEER"); + self.devsetting = Some(Self::read_setting_file(&path.join("DEVSETTING.DAT"))?); + self.djmmysetting = Some(Self::read_setting_file(&path.join("DJMMYSETTING.DAT"))?); + self.mysetting = Some(Self::read_setting_file(&path.join("MYSETTING.DAT"))?); + self.mysetting2 = Some(Self::read_setting_file(&path.join("MYSETTING2.DAT"))?); + + Ok(()) + } + + fn read_pdb_file(path: &PathBuf) -> crate::Result { + let mut reader = std::fs::File::open(path)?; + let header = Header::read(&mut reader)?; + let pages = header + .tables + .iter() + .flat_map(|table| { + header + .read_pages( + &mut reader, + &ReadOptions::new(binrw::Endian::NATIVE), + (&table.first_page, &table.last_page), + ) + .into_iter() + }) + .flatten() + .collect::>(); + + let pdb = Pdb { header, pages }; + Ok(pdb) + } + + /// Load PDB file. + pub fn load_pdb(&mut self) -> crate::Result<()> { + let path = self + .path + .join("PIONEER") + .join("rekordbox") + .join("export.pdb"); + self.pdb = Some(Self::read_pdb_file(&path)?); + Ok(()) + } + + /// Get the settings from this export. + #[must_use] + pub fn get_settings(&self) -> Settings { + let mut settings = Settings::default(); + [ + &self.mysetting, + &self.mysetting2, + &self.djmmysetting, + &self.devsetting, + ] + .into_iter() + .flatten() + .for_each(|setting| match &setting.data { + setting::SettingData::MySetting(data) => { + settings.set_mysetting(data); + } + setting::SettingData::MySetting2(data) => { + settings.set_mysetting2(data); + } + setting::SettingData::DJMMySetting(data) => { + settings.set_djmmysetting(data); + } + setting::SettingData::DevSetting(data) => { + settings.set_devsetting(data); + } + }); + + settings + } + + /// Get the playlists tree. + pub fn get_playlists(&self) -> crate::Result> { + match &self.pdb { + Some(pdb) => pdb.get_playlists(), + None => Err(crate::Error::NotLoadedError), + } + } + + /// Get the entries for a single playlist. + pub fn get_playlist_entries( + &self, + id: PlaylistTreeNodeId, + ) -> crate::Result> + '_> { + match &self.pdb { + Some(pdb) => Ok(pdb.get_playlist_entries(id)), + None => Err(crate::Error::NotLoadedError), + } + } + + /// Get the tracks. + pub fn get_tracks(&self) -> crate::Result> + '_> { + match &self.pdb { + Some(pdb) => Ok(pdb.get_tracks()), + None => Err(crate::Error::NotLoadedError), + } + } +} + +/// Settings object containing for all device settings. +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct Settings { + // MYSETTING.DAT + /// "ON AIR DISPLAY" setting. + pub on_air_display: Option, + /// "LCD BRIGHTNESS" setting. + pub lcd_brightness: Option, + /// "QUANTIZE" setting. + pub quantize: Option, + /// "AUTO CUE LEVEL" setting. + pub auto_cue_level: Option, + /// "LANGUAGE" setting. + pub language: Option, + /// "JOG RING BRIGHTNESS" setting. + pub jog_ring_brightness: Option, + /// "JOG RING INDICATOR" setting. + pub jog_ring_indicator: Option, + /// "SLIP FLASHING" setting. + pub slip_flashing: Option, + /// "DISC SLOT ILLUMINATION" setting. + pub disc_slot_illumination: Option, + /// "EJECT/LOAD LOCK" setting. + pub eject_lock: Option, + /// "SYNC" setting. + pub sync: Option, + /// "PLAY MODE / AUTO PLAY MODE" setting. + pub play_mode: Option, + /// Quantize Beat Value setting. + pub quantize_beat_value: Option, + /// "HOT CUE AUTO LOAD" setting. + pub hotcue_autoload: Option, + /// "HOT CUE COLOR" setting. + pub hotcue_color: Option, + /// "NEEDLE LOCK" setting. + pub needle_lock: Option, + /// "TIME MODE" setting. + pub time_mode: Option, + /// "TIME MODE" setting. + pub jog_mode: Option, + /// "AUTO CUE" setting. + pub auto_cue: Option, + /// "MASTER TEMPO" setting. + pub master_tempo: Option, + /// "TEMPO RANGE" setting. + pub tempo_range: Option, + /// "PHASE METER" setting. + pub phase_meter: Option, + + // MYSETTING2.DAT + /// "VINYL SPEED ADJUST" setting. + pub vinyl_speed_adjust: Option, + /// "JOG DISPLAY MODE" setting. + pub jog_display_mode: Option, + /// "PAD/BUTTON BRIGHTNESS" setting. + pub pad_button_brightness: Option, + /// "JOG LCD BRIGHTNESS" setting. + pub jog_lcd_brightness: Option, + /// "WAVEFORM DIVISIONS" setting. + pub waveform_divisions: Option, + /// "WAVEFORM / PHASE METER" setting. + pub waveform: Option, + /// "BEAT JUMP BEAT VALUE" setting. + pub beat_jump_beat_value: Option, + + // DJMSETTING.DAT + /// "CH FADER CURVE" setting. + pub channel_fader_curve: Option, + /// "CROSSFADER CURVE" setting. + pub crossfader_curve: Option, + /// "HEADPHONES PRE EQ" setting. + pub headphones_pre_eq: Option, + /// "HEADPHONES MONO SPLIT" setting. + pub headphones_mono_split: Option, + /// "BEAT FX QUANTIZE" setting. + pub beat_fx_quantize: Option, + /// "MIC LOW CUT" setting. + pub mic_low_cut: Option, + /// "TALK OVER MODE" setting. + pub talk_over_mode: Option, + /// "TALK OVER LEVEL" setting. + pub talk_over_level: Option, + /// "MIDI CH" setting. + pub midi_channel: Option, + /// "MIDI BUTTON TYPE" setting. + pub midi_button_type: Option, + /// "BRIGHTNESS > DISPLAY" setting. + pub display_brightness: Option, + /// "BRIGHTNESS > INDICATOR" setting. + pub indicator_brightness: Option, + /// "CH FADER CURVE (LONG FADER)" setting. + pub channel_fader_curve_long_fader: Option, + + // DEVSETTING.DAT + /// "Type of the overview Waveform" setting. + pub overview_waveform_type: Option, + /// "Waveform color" setting. + pub waveform_color: Option, + /// "Key display format" setting. + pub key_display_format: Option, + /// "Waveform Current Position" setting. + pub waveform_current_position: Option, +} + +impl Settings { + fn set_mysetting(&mut self, data: &setting::MySetting) { + self.on_air_display = data.on_air_display.into(); + self.lcd_brightness = data.lcd_brightness.into(); + self.quantize = data.quantize.into(); + self.auto_cue_level = data.auto_cue_level.into(); + self.language = data.language.into(); + self.jog_ring_brightness = data.jog_ring_brightness.into(); + self.jog_ring_indicator = data.jog_ring_indicator.into(); + self.slip_flashing = data.slip_flashing.into(); + self.disc_slot_illumination = data.disc_slot_illumination.into(); + self.eject_lock = data.eject_lock.into(); + self.sync = data.sync.into(); + self.play_mode = data.play_mode.into(); + self.quantize_beat_value = data.quantize_beat_value.into(); + self.hotcue_autoload = data.hotcue_autoload.into(); + self.hotcue_color = data.hotcue_color.into(); + self.needle_lock = data.needle_lock.into(); + self.time_mode = data.time_mode.into(); + self.jog_mode = data.jog_mode.into(); + self.auto_cue = data.auto_cue.into(); + self.master_tempo = data.master_tempo.into(); + self.tempo_range = data.tempo_range.into(); + self.phase_meter = data.phase_meter.into(); + } + + fn set_mysetting2(&mut self, data: &setting::MySetting2) { + self.vinyl_speed_adjust = data.vinyl_speed_adjust.into(); + self.jog_display_mode = data.jog_display_mode.into(); + self.pad_button_brightness = data.pad_button_brightness.into(); + self.jog_lcd_brightness = data.jog_lcd_brightness.into(); + self.waveform_divisions = data.waveform_divisions.into(); + self.waveform = data.waveform.into(); + self.beat_jump_beat_value = data.beat_jump_beat_value.into(); + } + + fn set_djmmysetting(&mut self, data: &setting::DJMMySetting) { + self.channel_fader_curve = data.channel_fader_curve.into(); + self.crossfader_curve = data.crossfader_curve.into(); + self.headphones_pre_eq = data.headphones_pre_eq.into(); + self.headphones_mono_split = data.headphones_mono_split.into(); + self.beat_fx_quantize = data.beat_fx_quantize.into(); + self.mic_low_cut = data.mic_low_cut.into(); + self.talk_over_mode = data.talk_over_mode.into(); + self.talk_over_level = data.talk_over_level.into(); + self.midi_channel = data.midi_channel.into(); + self.midi_button_type = data.midi_button_type.into(); + self.display_brightness = data.display_brightness.into(); + self.indicator_brightness = data.indicator_brightness.into(); + self.channel_fader_curve_long_fader = data.channel_fader_curve_long_fader.into(); + } + + fn set_devsetting(&mut self, data: &setting::DevSetting) { + self.overview_waveform_type = data.overview_waveform_type.into(); + self.waveform_color = data.waveform_color.into(); + self.key_display_format = data.key_display_format.into(); + self.waveform_current_position = data.waveform_current_position.into(); + } +} + +/// Represent a PDB file. +#[derive(Debug, PartialEq)] +pub struct Pdb { + header: Header, + pages: Vec, +} + +/// Represents either a playlist folder or a playlist. +#[derive(Debug, PartialEq)] +pub enum PlaylistNode { + /// Represents a playlist folder that contains `PlaylistNode`s. + Folder(PlaylistFolder), + /// Represents a playlist. + Playlist(Playlist), +} + +/// Represents a playlist folder that contains `PlaylistNode`s. +#[derive(Debug, PartialEq)] +pub struct PlaylistFolder { + /// ID of this node in the playlist tree. + pub id: PlaylistTreeNodeId, + /// Name of the playlist folder. + pub name: String, + /// Child nodes of the playlist folder. + pub children: Vec, +} + +/// Represents a playlist. +#[derive(Debug, PartialEq, Eq)] +pub struct Playlist { + /// ID of this node in the playlist tree. + pub id: PlaylistTreeNodeId, + /// Name of the playlist. + pub name: String, +} + +impl Pdb { + /// Create a new `Pdb` object by reading the PDB file at the given path. + pub fn new_from_path(path: &PathBuf) -> crate::Result { + let mut reader = std::fs::File::open(path)?; + let header = Header::read(&mut reader)?; + let pages = header + .tables + .iter() + .flat_map(|table| { + header + .read_pages( + &mut reader, + &ReadOptions::new(binrw::Endian::NATIVE), + (&table.first_page, &table.last_page), + ) + .into_iter() + }) + .flatten() + .collect::>(); + + let pdb = Pdb { header, pages }; + + Ok(pdb) + } + + fn get_rows_by_page_type(&self, page_type: PageType) -> impl Iterator + '_ { + self.pages + .iter() + .filter(move |page| page.page_type == page_type) + .flat_map(|page| page.row_groups.iter()) + .flat_map(|row_group| row_group.present_rows()) + } + + /// Get playlist tree. + pub fn get_playlists(&self) -> crate::Result> { + let mut playlists: HashMap> = HashMap::new(); + self.get_rows_by_page_type(PageType::PlaylistTree) + .map(|row| { + if let Row::PlaylistTreeNode(playlist_tree) = row { + Ok(playlist_tree) + } else { + Err(crate::Error::IntegrityError( + "encountered non-playlist tree row in playlist table", + )) + } + }) + .try_for_each(|row| { + row.map(|node| playlists.entry(node.parent_id).or_default().push(node)) + })?; + + fn get_child_nodes<'a>( + playlists: &'a HashMap>, + id: PlaylistTreeNodeId, + ) -> impl Iterator> + 'a { + playlists + .get(&id) + .into_iter() + .flat_map(|nodes| nodes.iter()) + .map(|node| -> crate::Result { + let child_node = if node.is_folder() { + let folder = PlaylistFolder { + id: node.id, + name: node.name.clone().into_string()?, + children: get_child_nodes(playlists, node.id) + .collect::>>()?, + }; + PlaylistNode::Folder(folder) + } else { + let playlist = Playlist { + id: node.id, + name: node.name.clone().into_string()?, + }; + PlaylistNode::Playlist(playlist) + }; + Ok(child_node) + }) + } + + get_child_nodes(&playlists, PlaylistTreeNodeId(0)) + .collect::>>() + } + + /// Get playlist entries. + pub fn get_playlist_entries( + &self, + playlist_id: PlaylistTreeNodeId, + ) -> impl Iterator> + '_ { + self.get_rows_by_page_type(PageType::PlaylistEntries) + .filter_map(move |row| { + if let Row::PlaylistEntry(entry) = row { + if entry.playlist_id == playlist_id { + Some(Ok((entry.entry_index, entry.track_id))) + } else { + None + } + } else { + Some(Err(crate::Error::IntegrityError( + "encountered non-playlist tree row in playlist table", + ))) + } + }) + } + + /// Get tracks. + pub fn get_tracks(&self) -> impl Iterator> + '_ { + self.get_rows_by_page_type(PageType::Tracks).map(|row| { + if let Row::Track(track) = row { + Ok(track.clone()) + } else { + Err(crate::Error::IntegrityError( + "encountered non-track row in track table", + )) + } + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index cafbb35e..018368a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,10 +24,12 @@ #![cfg_attr(not(debug_assertions), deny(clippy::used_underscore_binding))] pub mod anlz; +pub mod device; pub mod pdb; pub mod setting; pub mod util; pub(crate) mod xor; +pub use crate::device::DeviceExport; pub use crate::util::RekordcrateError as Error; pub use crate::util::RekordcrateResult as Result; diff --git a/src/main.rs b/src/main.rs index 95aa29d9..011bb1e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,9 +9,9 @@ use binrw::{BinRead, ReadOptions}; use clap::{Parser, Subcommand}; use rekordcrate::anlz::ANLZ; -use rekordcrate::pdb::{Header, PageType, Row}; +use rekordcrate::pdb::Header; use rekordcrate::setting::Setting; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(Parser)] #[command(author, version, about)] @@ -29,6 +29,21 @@ enum Commands { #[arg(value_name = "PDB_FILE")] path: PathBuf, }, + /// Display settings from a Rekordbox device export. + ListSettings { + /// Path to parse. + #[arg(value_name = "EXPORT_PATH")] + path: PathBuf, + }, + /// Export the playlists from a Pioneer Database (`.PDB`) file to M3U files. + ExportPlaylists { + /// File to parse. + #[arg(value_name = "EXPORT_PATH")] + path: PathBuf, + /// Output directory to write M3U files to. + #[arg(value_name = "OUTPUT_DIR")] + output_dir: PathBuf, + }, /// Parse and dump a Rekordbox Analysis (`ANLZXXXX.DAT`) file. DumpANLZ { /// File to parse. @@ -49,66 +64,446 @@ enum Commands { }, } -fn list_playlists(path: &PathBuf) -> rekordcrate::Result<()> { - use rekordcrate::pdb::{PlaylistTreeNode, PlaylistTreeNodeId}; - use std::collections::HashMap; +fn list_playlists(path: &Path) -> rekordcrate::Result<()> { + use rekordcrate::device::PlaylistNode; + use rekordcrate::DeviceExport; + + let mut export = DeviceExport::new(path.into()); + export.load_pdb()?; + let playlists = export.get_playlists()?; - fn print_children_of( - tree: &HashMap>, - id: PlaylistTreeNodeId, - level: usize, - ) { - tree.get(&id) - .iter() - .flat_map(|nodes| nodes.iter()) - .for_each(|node| { + fn walk_tree(export: &DeviceExport, node: PlaylistNode, level: usize) { + let indent = " ".repeat(level); + match node { + PlaylistNode::Folder(folder) => { + println!("{}🗀 {}", indent, folder.name); + folder + .children + .into_iter() + .for_each(|child| walk_tree(export, child, level + 1)); + } + PlaylistNode::Playlist(playlist) => { + let num_tracks = export + .get_playlist_entries(playlist.id) + .expect("failed to get playlist entries") + .count(); println!( - "{}{} {}", - " ".repeat(level), - if node.is_folder() { "🗀" } else { "🗎" }, - node.name.clone().into_string().unwrap(), - ); - print_children_of(tree, node.id, level + 1); - }); + "{}🗎 {} ({} {})", + indent, + playlist.name, + num_tracks, + if num_tracks == 1 { "track" } else { "tracks" } + ) + } + }; } + playlists + .into_iter() + .for_each(|node| walk_tree(&export, node, 0)); - let mut reader = std::fs::File::open(path)?; - let header = Header::read(&mut reader)?; + Ok(()) +} - let mut tree: HashMap> = HashMap::new(); - - header - .tables - .iter() - .filter(|table| table.page_type == PageType::PlaylistTree) - .flat_map(|table| { - header - .read_pages( - &mut reader, - &ReadOptions::new(binrw::Endian::NATIVE), - (&table.first_page, &table.last_page), - ) - .unwrap() - .into_iter() - .flat_map(|page| page.row_groups.into_iter()) - .flat_map(|row_group| { - row_group - .present_rows() - .map(|row| { - if let Row::PlaylistTreeNode(playlist_tree) = row { - playlist_tree - } else { - unreachable!("encountered non-playlist tree row in playlist table"); - } - }) - .cloned() - .collect::>() - .into_iter() - }) - }) - .for_each(|row| tree.entry(row.parent_id).or_default().push(row)); - - print_children_of(&tree, PlaylistTreeNodeId(0), 0); +fn export_playlists(path: &Path, output_dir: &PathBuf) -> rekordcrate::Result<()> { + use rekordcrate::device::PlaylistNode; + use rekordcrate::pdb::{Track, TrackId}; + use rekordcrate::DeviceExport; + use std::collections::HashMap; + use std::io::Write; + + let mut export = DeviceExport::new(path.into()); + export.load_pdb()?; + let playlists = export.get_playlists()?; + let mut tracks: HashMap = HashMap::new(); + export.get_tracks()?.try_for_each(|result| { + if let Ok(track) = result { + tracks.insert(track.id, track); + Ok(()) + } else { + result.map(|_| ()) + } + })?; + + fn walk_tree( + export: &DeviceExport, + tracks: &HashMap, + node: PlaylistNode, + path: &PathBuf, + ) -> rekordcrate::Result<()> { + match node { + PlaylistNode::Folder(folder) => { + folder.children.into_iter().try_for_each(|child| { + walk_tree(export, tracks, child, &path.join(&folder.name)) + })?; + } + PlaylistNode::Playlist(playlist) => { + let mut playlist_entries = export + .get_playlist_entries(playlist.id)? + .collect::>>()?; + playlist_entries.sort_by_key(|entry| entry.0); + + std::fs::create_dir_all(path)?; + let playlist_path = path.join(format!("{}.m3u", playlist.name)); + + println!("{}", playlist_path.display()); + let mut file = std::fs::File::create(playlist_path)?; + playlist_entries + .into_iter() + .filter_map(|(_index, id)| tracks.get(&id)) + .try_for_each(|track| -> rekordcrate::Result<()> { + let track_path = track.file_path.clone().into_string()?; + Ok(writeln!( + &mut file, + "{}", + export + .get_path() + .canonicalize()? + .join(track_path.strip_prefix('/').unwrap_or(&track_path)) + .display(), + )?) + })?; + } + }; + + Ok(()) + } + + playlists + .into_iter() + .try_for_each(|node| walk_tree(&export, &tracks, node, output_dir))?; + + Ok(()) +} + +fn list_settings(path: &Path) -> rekordcrate::Result<()> { + use rekordcrate::DeviceExport; + + let mut export = DeviceExport::new(path.into()); + export.load_settings()?; + let settings = export.get_settings(); + + println!( + "On Air Display: {}", + settings + .on_air_display + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "LCD Brightness: {}", + settings + .lcd_brightness + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Quantize: {}", + settings + .quantize + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Auto Cue Level: {}", + settings + .auto_cue_level + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Language: {}", + settings + .language + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Jog Ring Brightness: {}", + settings + .jog_ring_brightness + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Jog Ring Indicator: {}", + settings + .jog_ring_indicator + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Slip Flashing: {}", + settings + .slip_flashing + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Disc Slot Illumination: {}", + settings + .disc_slot_illumination + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Eject Lock: {}", + settings + .eject_lock + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Sync: {}", + settings + .sync + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Play Mode: {}", + settings + .play_mode + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Quantize Beat Value: {}", + settings + .quantize_beat_value + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Hotcue Autoload: {}", + settings + .hotcue_autoload + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Hotcue Color: {}", + settings + .hotcue_color + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Needle Lock: {}", + settings + .needle_lock + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Time Mode: {}", + settings + .time_mode + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Jog Mode: {}", + settings + .jog_mode + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Auto Cue: {}", + settings + .auto_cue + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Master Tempo: {}", + settings + .master_tempo + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Tempo Range: {}", + settings + .tempo_range + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Phase Meter: {}", + settings + .phase_meter + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Vinyl Speed Adjust: {}", + settings + .vinyl_speed_adjust + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Jog Display Mode: {}", + settings + .jog_display_mode + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Pad Button Brightness: {}", + settings + .pad_button_brightness + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Jog LCD Brightness: {}", + settings + .jog_lcd_brightness + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Waveform Divisions: {}", + settings + .waveform_divisions + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Waveform: {}", + settings + .waveform + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Beat Jump Beat Value: {}", + settings + .beat_jump_beat_value + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Channel Fader Curve: {}", + settings + .channel_fader_curve + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Crossfader Curve: {}", + settings + .crossfader_curve + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Headphones Pre Eq: {}", + settings + .headphones_pre_eq + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Headphones Mono Split: {}", + settings + .headphones_mono_split + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Beat FX Quantize: {}", + settings + .beat_fx_quantize + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Mic Low Cut: {}", + settings + .mic_low_cut + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Talk Over Mode: {}", + settings + .talk_over_mode + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Talk Over Level: {}", + settings + .talk_over_level + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "MIDI Channel: {}", + settings + .midi_channel + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "MIDI Button Type: {}", + settings + .midi_button_type + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Display Brightness: {}", + settings + .display_brightness + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Indicator Brightness: {}", + settings + .indicator_brightness + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Channel Fader Curve Long Fader: {}", + settings + .channel_fader_curve_long_fader + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Overview Waveform Type: {}", + settings + .overview_waveform_type + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Waveform Color: {}", + settings + .waveform_color + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Key Display Format: {}", + settings + .key_display_format + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); + println!( + "Waveform Current Position: {}", + settings + .waveform_current_position + .map(|v| v.to_string()) + .unwrap_or_else(|| "".to_string()) + ); Ok(()) } @@ -165,6 +560,8 @@ fn main() -> rekordcrate::Result<()> { match &cli.command { Commands::ListPlaylists { path } => list_playlists(path), + Commands::ListSettings { path } => list_settings(path), + Commands::ExportPlaylists { path, output_dir } => export_playlists(path, output_dir), Commands::DumpPDB { path } => dump_pdb(path), Commands::DumpANLZ { path } => dump_anlz(path), Commands::DumpSetting { path } => dump_setting(path), diff --git a/src/pdb/mod.rs b/src/pdb/mod.rs index 8922d288..1b2ddc32 100644 --- a/src/pdb/mod.rs +++ b/src/pdb/mod.rs @@ -661,11 +661,11 @@ impl PlaylistTreeNode { #[brw(little)] pub struct PlaylistEntry { /// Position within the playlist. - entry_index: u32, + pub entry_index: u32, /// ID of the track played at this position in the playlist. - track_id: TrackId, + pub track_id: TrackId, /// ID of the playlist. - playlist_id: PlaylistTreeNodeId, + pub playlist_id: PlaylistTreeNodeId, } /// Contains the kinds of Metadata Categories tracks can be browsed by @@ -742,7 +742,7 @@ pub struct Track { /// Artist row ID for this track (non-zero if set). artist_id: ArtistId, /// Row ID of this track (non-zero if set). - id: TrackId, + pub id: TrackId, /// Disc number of this track (non-zero if set). disc_number: u16, /// Number of times this track was played. @@ -825,7 +825,7 @@ pub struct Track { filename: DeviceSQLString, /// Path of the file. #[br(offset = base_offset, parse_with = FilePtr16::parse)] - file_path: DeviceSQLString, + pub file_path: DeviceSQLString, } // #[bw(little)] on #[binread] types does diff --git a/src/util.rs b/src/util.rs index abedcd05..60c91adf 100644 --- a/src/util.rs +++ b/src/util.rs @@ -24,9 +24,17 @@ pub enum RekordcrateError { #[error(transparent)] ParseError(#[from] binrw::Error), + /// Represents a failure to validate a constraint. + #[error("failed integrity constraint: {0}")] + IntegrityError(&'static str), + /// Represents an `std::io::Error`. #[error(transparent)] IOError(#[from] std::io::Error), + + /// Represents an `std::io::Error`. + #[error("component not loaded")] + NotLoadedError, } /// Type alias for results where the error is a `RekordcrateError`.