Initial work on database rewrite

This commit is contained in:
G2-Games 2023-09-29 23:28:12 -05:00
parent 7e64c67b46
commit 7f57367aad
3 changed files with 91 additions and 263 deletions

View file

@ -14,7 +14,6 @@ categories = ["multimedia::audio"]
[dependencies]
file-format = { version = "0.17.3", features = ["reader", "serde"] }
lofty = "0.14.0"
rusqlite = { version = "0.29.0", features = ["bundled"] }
serde = { version = "1.0.164", features = ["derive"] }
time = "0.3.22"
toml = "0.7.5"
@ -32,3 +31,5 @@ futures = "0.3.28"
rubato = "0.12.0"
arrayvec = "0.7.4"
discord-presence = "0.5.18"
chrono = { version = "0.4.31", features = ["serde"] }
bincode = "1.3.3"

View file

@ -1,38 +1,72 @@
use file_format::{FileFormat, Kind};
use serde::Deserialize;
use lofty::{Accessor, AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType};
use rusqlite::{params, Connection};
use std::fs;
use std::path::{Path, PathBuf};
use lofty::{AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType};
use std::{error::Error, io::BufReader};
use std::time::Duration;
use time::Date;
use chrono::{DateTime, Utc, serde::ts_seconds_option};
use walkdir::WalkDir;
use std::io::BufWriter;
use std::fs;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use bincode::{serialize_into, deserialize_from};
use crate::music_controller::config::Config;
#[derive(Debug, Clone)]
pub struct Song {
pub path: URI,
pub title: Option<String>,
pub album: Option<String>,
pub tracknum: Option<usize>,
pub artist: Option<String>,
pub date: Option<Date>,
pub genre: Option<String>,
pub plays: Option<usize>,
pub favorited: Option<bool>,
pub format: Option<FileFormat>, // TODO: Make this a proper FileFormat eventually
pub duration: Option<Duration>,
pub custom_tags: Option<Vec<Tag>>,
pub struct AlbumArt {
pub path: Option<URI>;
}
#[derive(Clone, Debug)]
/// Stores information about a single song
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Song {
pub path: URI,
pub plays: i32,
pub skips: i32,
pub favorited: bool,
pub rating: u8,
pub format: Option<FileFormat>,
pub duration: Duration,
pub play_time: Duration,
#[serde(with = "ts_seconds_option")]
pub last_played: Option<DateTime<Utc>>,
#[serde(with = "ts_seconds_option")]
pub date_added: Option<DateTime<Utc>>,
pub tags: Vec<(String, String)>,
}
impl Song {
pub fn get_tag(&self, target_key: String) -> Option<String> {
for tag in self.tags {
if tag.0 == target_key {
return Some(tag.1)
}
}
None
}
pub fn get_tags(&self, target_keys: Vec<String>) -> Vec<Option<String>> {
let mut results = Vec::new();
for tag in self.tags {
for key in target_keys {
if tag.0 == key {
results.push(Some(tag.1))
}
}
results.push(None);
}
results
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum URI{
Local(String),
Remote(Service, String),
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Service {
InternetRadio,
Spotify,
@ -45,74 +79,40 @@ pub struct Playlist {
cover_art: Box<Path>,
}
pub fn create_db() -> Result<(), rusqlite::Error> {
let path = "./music_database.db3";
let db_connection = Connection::open(path)?;
/// Initialize the database
///
/// If the database file already exists, return the database, otherwise create it first
/// This needs to be run before anything else to retrieve the library vec
pub fn init_db(config: &Config) -> Result<Vec<Song>, Box<dyn Error>> {
let mut library: Vec<Song> = Vec::new();
db_connection.pragma_update(None, "synchronous", "0")?;
db_connection.pragma_update(None, "journal_mode", "WAL")?;
// Create the important tables
db_connection.execute(
"CREATE TABLE music_collection (
song_path TEXT PRIMARY KEY,
title TEXT,
album TEXT,
tracknum INTEGER,
artist TEXT,
date INTEGER,
genre TEXT,
plays INTEGER,
favorited BLOB,
format TEXT,
duration INTEGER
)",
(), // empty list of parameters.
)?;
db_connection.execute(
"CREATE TABLE playlists (
playlist_name TEXT NOT NULL,
song_path TEXT NOT NULL,
FOREIGN KEY(song_path) REFERENCES music_collection(song_path)
)",
(), // empty list of parameters.
)?;
db_connection.execute(
"CREATE TABLE custom_tags (
song_path TEXT NOT NULL,
tag TEXT NOT NULL,
tag_value TEXT,
FOREIGN KEY(song_path) REFERENCES music_collection(song_path)
)",
(), // empty list of parameters.
)?;
Ok(())
}
fn path_in_db(query_path: &Path, connection: &Connection) -> bool {
let query_string = format!("SELECT EXISTS(SELECT 1 FROM music_collection WHERE song_path='{}')", query_path.to_string_lossy());
let mut query_statement = connection.prepare(&query_string).unwrap();
let mut rows = query_statement.query([]).unwrap();
match rows.next().unwrap() {
Some(value) => value.get::<usize, bool>(0).unwrap(),
None => false
match config.db_path.try_exists() {
Ok(_) => {
// The database exists, so get it from the file
let database = fs::File::open(config.db_path.into_boxed_path())?;
let reader = BufReader::new(database);
library = deserialize_from(reader)?;
},
Err(_) => {
// Create the database if it does not exist
let mut writer = BufWriter::new(
fs::File::create(config.db_path.into_boxed_path())?
);
serialize_into(&mut writer, &library)?;
}
};
Ok(library)
}
fn path_in_db(query_path: &Path, library: &Vec<Song>) -> bool {
unimplemented!()
}
pub fn find_all_music(
config: &Config,
target_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let db_connection = Connection::open(&*config.db_path)?;
db_connection.pragma_update(None, "synchronous", "0")?;
db_connection.pragma_update(None, "journal_mode", "WAL")?;
let mut current_dir = PathBuf::new();
for entry in WalkDir::new(target_path).follow_links(true).into_iter().filter_map(|e| e.ok()) {
@ -134,25 +134,16 @@ pub fn find_all_music(
// If it's a normal file, add it to the database
// if it's a cuesheet, do a bunch of fancy stuff
if format.kind() == Kind::Audio {
add_file_to_db(target_file.path(), &db_connection)
add_file_to_db(target_file.path())
} else if extension.to_ascii_lowercase() == "cue" {
// TODO: implement cuesheet support
}
}
// create the indexes after all the data is inserted
db_connection.execute(
"CREATE INDEX path_index ON music_collection (song_path)", ()
)?;
db_connection.execute(
"CREATE INDEX custom_tags_index ON custom_tags (song_path)", ()
)?;
Ok(())
}
pub fn add_file_to_db(target_file: &Path, connection: &Connection) {
pub fn add_file_to_db(target_file: &Path) {
// TODO: Fix error handling here
let tagged_file = match lofty::read_from_path(target_file) {
Ok(tagged_file) => tagged_file,
@ -179,7 +170,7 @@ pub fn add_file_to_db(target_file: &Path, connection: &Connection) {
let mut custom_insert = String::new();
let mut loops = 0;
for item in tag.items() {
for (loops, item) in tag.items().enumerate() {
let mut custom_key = String::new();
match item.key() {
ItemKey::TrackArtist |
@ -203,10 +194,6 @@ pub fn add_file_to_db(target_file: &Path, connection: &Connection) {
if loops > 0 {
custom_insert.push_str(", ");
}
custom_insert.push_str(&format!(" (?1, '{}', '{}')", custom_key.replace("\'", "''"), custom_value.replace("\'", "''")));
loops += 1;
}
// Get the format as a string
@ -222,69 +209,10 @@ pub fn add_file_to_db(target_file: &Path, connection: &Connection) {
// TODO: Fix error handling
let binding = fs::canonicalize(target_file).unwrap();
let abs_path = binding.to_str().unwrap();
// Add all the info into the music_collection table
connection.execute(
"INSERT INTO music_collection (
song_path,
title,
album,
tracknum,
artist,
date,
genre,
plays,
favorited,
format,
duration
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![abs_path, tag.title(), tag.album(), tag.track(), tag.artist(), tag.year(), tag.genre(), 0, false, short_format, duration],
).unwrap();
//TODO: Fix this, it's horrible
if custom_insert != "" {
connection.execute(
&format!("INSERT INTO custom_tags ('song_path', 'tag', 'tag_value') VALUES {}", &custom_insert),
params![
abs_path,
]
).unwrap();
}
}
#[derive(Debug, Deserialize, Clone)]
pub enum Tag {
SongPath,
Title,
Album,
TrackNum,
Artist,
Date,
Genre,
Plays,
Favorited,
Format,
Duration,
Custom{tag: String, tag_value: String},
}
pub fn add_song_to_db(new_song: Song) {
impl Tag {
fn as_str(&self) -> &str {
match self {
Tag::SongPath => "song_path",
Tag::Title => "title",
Tag::Album => "album",
Tag::TrackNum => "tracknum",
Tag::Artist => "artist",
Tag::Date => "date",
Tag::Genre => "genre",
Tag::Plays => "plays",
Tag::Favorited => "favorited",
Tag::Format => "format",
Tag::Duration => "duration",
Tag::Custom{tag, ..} => tag,
}
}
}
#[derive(Debug)]
@ -305,93 +233,9 @@ impl MusicObject {
/// Query the database, returning a list of items
pub fn query (
config: &Config,
text_input: &String,
queried_tags: &Vec<&Tag>,
order_by_tags: &Vec<&Tag>,
query_string: &String, // The query itself
target_tags: &Vec<String>, // The tags to search
sort_by: &Vec<String>, // Tags to sort the resulting data by
) -> Option<Vec<MusicObject>> {
let db_connection = Connection::open(&*config.db_path).unwrap();
// Set up some database settings
db_connection.pragma_update(None, "synchronous", "0").unwrap();
db_connection.pragma_update(None, "journal_mode", "WAL").unwrap();
// Build the "WHERE" part of the SQLite query
let mut where_string = String::new();
let mut loops = 0;
for tag in queried_tags {
if loops > 0 {
where_string.push_str("OR ");
}
match tag {
Tag::Custom{tag, ..} => where_string.push_str(&format!("custom_tags.tag = '{tag}' AND custom_tags.tag_value LIKE '{text_input}' ")),
Tag::SongPath => where_string.push_str(&format!("music_collection.{} LIKE '{text_input}' ", tag.as_str())),
_ => where_string.push_str(&format!("{} LIKE '{text_input}' ", tag.as_str()))
}
loops += 1;
}
// Build the "ORDER BY" part of the SQLite query
let mut order_by_string = String::new();
let mut loops = 0;
for tag in order_by_tags {
match tag {
Tag::Custom{..} => continue,
_ => ()
}
if loops > 0 {
order_by_string.push_str(", ");
}
order_by_string.push_str(tag.as_str());
loops += 1;
}
// Build the final query string
let query_string = format!("
SELECT music_collection.*, JSON_GROUP_ARRAY(JSON_OBJECT('Custom',JSON_OBJECT('tag', custom_tags.tag, 'tag_value', custom_tags.tag_value))) AS custom_tags
FROM music_collection
LEFT JOIN custom_tags ON music_collection.song_path = custom_tags.song_path
WHERE {where_string}
GROUP BY music_collection.song_path
ORDER BY {order_by_string}
");
let mut query_statement = db_connection.prepare(&query_string).unwrap();
let mut rows = query_statement.query([]).unwrap();
let mut final_result:Vec<MusicObject> = vec![];
while let Some(row) = rows.next().unwrap() {
let custom_tags: Vec<Tag> = match row.get::<usize, String>(11) {
Ok(result) => serde_json::from_str(&result).unwrap_or(vec![]),
Err(_) => vec![]
};
let file_format: FileFormat = FileFormat::from(row.get::<usize, String>(9).unwrap().as_bytes());
let new_song = Song {
// TODO: Implement proper errors here
path: URI::Local(String::from("URI")),
title: row.get::<usize, String>(1).ok(),
album: row.get::<usize, String>(2).ok(),
tracknum: row.get::<usize, usize>(3).ok(),
artist: row.get::<usize, String>(4).ok(),
date: Date::from_calendar_date(row.get::<usize, i32>(5).unwrap_or(0), time::Month::January, 1).ok(), // TODO: Fix this to get the actual date
genre: row.get::<usize, String>(6).ok(),
plays: row.get::<usize, usize>(7).ok(),
favorited: row.get::<usize, bool>(8).ok(),
format: Some(file_format),
duration: Some(Duration::from_secs(row.get::<usize, u64>(10).unwrap_or(0))),
custom_tags: Some(custom_tags),
};
final_result.push(MusicObject::Song(new_song));
};
Some(final_result)
unimplemented!()
}

View file

@ -1,27 +1,10 @@
use std::path::Path;
use crate::music_controller::config::Config;
use rusqlite::{params, Connection};
pub fn playlist_add(
config: &Config,
playlist_name: &str,
song_paths: &Vec<&Path>
) {
let db_connection = Connection::open(&*config.db_path).unwrap();
for song_path in song_paths {
db_connection.execute(
"INSERT INTO playlists (
playlist_name,
song_path
) VALUES (
?1,
?2
)",
params![
playlist_name,
song_path.to_str().unwrap()
],
).unwrap();
}
unimplemented!()
}