rusting
This commit is contained in:
1 parent
0a970d6c60
commit
76bf94823b
7 files changed
+281
-32
No files matched your search
@@ -0,0 +1,63 @@
|
||||
use lofty::read_from_path;
|
||||
use lofty::prelude::*;
|
||||
use std::path::Path;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
fn main() {
|
||||
let music_path = "data/maple";
|
||||
|
||||
if !Path::new(music_path).exists() {
|
||||
println!("Music directory {} not found", music_path);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Scanning music files in {}...\n", music_path);
|
||||
|
||||
for entry in WalkDir::new(music_path)
|
||||
.follow_links(true)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
|
||||
// Skip _missing_tags directory
|
||||
if path.to_string_lossy().contains("_missing_tags") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(extension) = path.extension() {
|
||||
let ext = extension.to_string_lossy().to_lowercase();
|
||||
if ext == "mp3" || ext == "flac" {
|
||||
if let Err(e) = print_file_metadata(path) {
|
||||
println!("Error reading {}: {}", path.display(), e);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_file_metadata(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tagged_file = read_from_path(path)?;
|
||||
|
||||
println!("File: {}", path.display());
|
||||
|
||||
if let Some(tag) = tagged_file.primary_tag() {
|
||||
if let Some(title) = tag.title() { println!(" Title: {}", title); }
|
||||
if let Some(artist) = tag.artist() { println!(" Artist: {}", artist); }
|
||||
if let Some(album) = tag.album() { println!(" Album: {}", album); }
|
||||
if let Some(year) = tag.year() { println!(" Year: {}", year); }
|
||||
if let Some(track) = tag.track() { println!(" Track: {}", track); }
|
||||
if let Some(genre) = tag.genre() { println!(" Genre: {}", genre); }
|
||||
} else {
|
||||
println!(" No metadata found");
|
||||
}
|
||||
|
||||
let properties = tagged_file.properties();
|
||||
println!(" Duration: {}:{:02}",
|
||||
properties.duration().as_secs() / 60,
|
||||
properties.duration().as_secs() % 60);
|
||||
println!(" Bitrate: {} kbps", properties.overall_bitrate().unwrap_or(0));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
use rusqlite::{params, Connection, Result};
|
||||
use std::io::Read;
|
||||
use flate2::read::ZlibDecoder;
|
||||
use base85;
|
||||
|
||||
const BLOB_KEY: &[u8] = b"657f48f84c437cc1";
|
||||
const BLOB: &[u8] = b"PN_Pq^*N>(JYe*u^8;Yg76HuZ<mR13S?=>)b9;DpoTXV(6ItkU`}8*m6tx_I{Solh_N#dfe{v=";
|
||||
|
||||
fn deobfuscate() -> Result<String, Box<dyn std::error::Error>> {
|
||||
// 1. Base85 decode
|
||||
let data = base85::decode(std::str::from_utf8(BLOB)?)?;
|
||||
|
||||
// 2. XOR with key
|
||||
let xored: Vec<u8> = data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &b)| b ^ BLOB_KEY[i % BLOB_KEY.len()])
|
||||
.collect();
|
||||
|
||||
// 3. zlib decompress
|
||||
let mut decoder = ZlibDecoder::new(&xored[..]);
|
||||
let mut decompressed = Vec::new();
|
||||
decoder.read_to_end(&mut decompressed)?;
|
||||
|
||||
// 4. UTF-8 decode
|
||||
Ok(String::from_utf8(decompressed)?)
|
||||
}
|
||||
|
||||
// Structs for mapping rows
|
||||
#[derive(Debug)]
|
||||
struct Playlist {
|
||||
id: i64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Track {
|
||||
id: i64,
|
||||
title: String,
|
||||
}
|
||||
|
||||
// List all playlists
|
||||
fn list_playlists(conn: &Connection) -> Result<Vec<Playlist>> {
|
||||
let mut stmt = conn.prepare("SELECT ID, Name FROM plPlaylist ORDER BY ID")?;
|
||||
let playlists = stmt.query_map([], |row| {
|
||||
Ok(Playlist {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(playlists.filter_map(|x| x.ok()).collect())
|
||||
}
|
||||
|
||||
// List tracks in a playlist
|
||||
fn list_tracks_in_playlist(conn: &Connection, playlist_id: i64) -> Result<Vec<Track>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT c.ID, c.Name
|
||||
FROM plSongPlaylistRel r
|
||||
JOIN plContent c ON r.SongID = c.ID
|
||||
WHERE r.PlaylistID = ?
|
||||
ORDER BY r.SortOrder",
|
||||
)?;
|
||||
let tracks = stmt.query_map([playlist_id], |row| {
|
||||
Ok(Track {
|
||||
id: row.get(0)?,
|
||||
title: row.get(1)?,
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(tracks.filter_map(|x| x.ok()).collect())
|
||||
}
|
||||
|
||||
// Add a track to a playlist
|
||||
fn add_track_to_playlist(conn: &Connection, playlist_id: i64, track_id: i64) -> Result<()> {
|
||||
// Get next ID for relation
|
||||
let next_id: i64 = conn.query_row(
|
||||
"SELECT IFNULL(MAX(ID), 0) + 1 FROM plSongPlaylistRel",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
// Get next sort order for playlist
|
||||
let next_sort: i64 = conn.query_row(
|
||||
"SELECT IFNULL(MAX(SortOrder), 0) + 1 FROM plSongPlaylistRel WHERE PlaylistID = ?",
|
||||
[playlist_id],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO plSongPlaylistRel (ID, PlaylistID, SongID, SortOrder)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
params![next_id, playlist_id, track_id, next_sort],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Remove a track from a playlist
|
||||
fn remove_track_from_playlist(conn: &Connection, playlist_id: i64, track_id: i64) -> Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM plSongPlaylistRel WHERE PlaylistID = ? AND SongID = ?",
|
||||
params![playlist_id, track_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all tables in a Rekordbox SQLite DB (master.db or export.pdb)
|
||||
fn list_tables(conn: &Connection) -> Result<Vec<String>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| row.get(0))?;
|
||||
|
||||
let mut tables = Vec::new();
|
||||
for r in rows {
|
||||
if let Ok(name) = r {
|
||||
tables.push(name);
|
||||
}
|
||||
}
|
||||
Ok(tables)
|
||||
}
|
||||
|
||||
// Open Rekordbox DB (unencrypted). For Rekordbox 6+, needs SQLCipher build.
|
||||
fn open_rekordbox_db(path: &str, key: String) -> Result<Connection> {
|
||||
let conn = Connection::open(path)?;
|
||||
// conn.pragma_update(None, "key", key)?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let key = deobfuscate()?;
|
||||
let conn = open_rekordbox_db("../data/PIONEER/rekordbox/export.pdb", key)?;
|
||||
|
||||
list_tables(&conn)?;
|
||||
|
||||
// // Example: list playlists
|
||||
// for p in list_playlists(&conn)? {
|
||||
// println!("Playlist {}: {}", p.id, p.name);
|
||||
// }
|
||||
|
||||
// // Example: add track 123 to playlist 10
|
||||
// add_track_to_playlist(&conn, 10, 123)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user