mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 10:02:53 -05:00
Initial work on database rewrite
This commit is contained in:
parent
7e64c67b46
commit
7f57367aad
3 changed files with 91 additions and 263 deletions
|
@ -14,7 +14,6 @@ categories = ["multimedia::audio"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
file-format = { version = "0.17.3", features = ["reader", "serde"] }
|
file-format = { version = "0.17.3", features = ["reader", "serde"] }
|
||||||
lofty = "0.14.0"
|
lofty = "0.14.0"
|
||||||
rusqlite = { version = "0.29.0", features = ["bundled"] }
|
|
||||||
serde = { version = "1.0.164", features = ["derive"] }
|
serde = { version = "1.0.164", features = ["derive"] }
|
||||||
time = "0.3.22"
|
time = "0.3.22"
|
||||||
toml = "0.7.5"
|
toml = "0.7.5"
|
||||||
|
@ -32,3 +31,5 @@ futures = "0.3.28"
|
||||||
rubato = "0.12.0"
|
rubato = "0.12.0"
|
||||||
arrayvec = "0.7.4"
|
arrayvec = "0.7.4"
|
||||||
discord-presence = "0.5.18"
|
discord-presence = "0.5.18"
|
||||||
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
|
bincode = "1.3.3"
|
||||||
|
|
|
@ -1,38 +1,72 @@
|
||||||
use file_format::{FileFormat, Kind};
|
use file_format::{FileFormat, Kind};
|
||||||
use serde::Deserialize;
|
use lofty::{AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType};
|
||||||
use lofty::{Accessor, AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType};
|
use std::{error::Error, io::BufReader};
|
||||||
use rusqlite::{params, Connection};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use time::Date;
|
use chrono::{DateTime, Utc, serde::ts_seconds_option};
|
||||||
use walkdir::WalkDir;
|
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;
|
use crate::music_controller::config::Config;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
pub struct AlbumArt {
|
||||||
pub struct Song {
|
pub path: Option<URI>;
|
||||||
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>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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{
|
pub enum URI{
|
||||||
Local(String),
|
Local(String),
|
||||||
Remote(Service, String),
|
Remote(Service, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
pub enum Service {
|
pub enum Service {
|
||||||
InternetRadio,
|
InternetRadio,
|
||||||
Spotify,
|
Spotify,
|
||||||
|
@ -45,74 +79,40 @@ pub struct Playlist {
|
||||||
cover_art: Box<Path>,
|
cover_art: Box<Path>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_db() -> Result<(), rusqlite::Error> {
|
/// Initialize the database
|
||||||
let path = "./music_database.db3";
|
///
|
||||||
let db_connection = Connection::open(path)?;
|
/// 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")?;
|
match config.db_path.try_exists() {
|
||||||
db_connection.pragma_update(None, "journal_mode", "WAL")?;
|
Ok(_) => {
|
||||||
|
// The database exists, so get it from the file
|
||||||
// Create the important tables
|
let database = fs::File::open(config.db_path.into_boxed_path())?;
|
||||||
db_connection.execute(
|
let reader = BufReader::new(database);
|
||||||
"CREATE TABLE music_collection (
|
library = deserialize_from(reader)?;
|
||||||
song_path TEXT PRIMARY KEY,
|
},
|
||||||
title TEXT,
|
Err(_) => {
|
||||||
album TEXT,
|
// Create the database if it does not exist
|
||||||
tracknum INTEGER,
|
let mut writer = BufWriter::new(
|
||||||
artist TEXT,
|
fs::File::create(config.db_path.into_boxed_path())?
|
||||||
date INTEGER,
|
);
|
||||||
genre TEXT,
|
serialize_into(&mut writer, &library)?;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(library)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn path_in_db(query_path: &Path, library: &Vec<Song>) -> bool {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_all_music(
|
pub fn find_all_music(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
target_path: &str,
|
target_path: &str,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> 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();
|
let mut current_dir = PathBuf::new();
|
||||||
for entry in WalkDir::new(target_path).follow_links(true).into_iter().filter_map(|e| e.ok()) {
|
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 normal file, add it to the database
|
||||||
// if it's a cuesheet, do a bunch of fancy stuff
|
// if it's a cuesheet, do a bunch of fancy stuff
|
||||||
if format.kind() == Kind::Audio {
|
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" {
|
} else if extension.to_ascii_lowercase() == "cue" {
|
||||||
// TODO: implement cuesheet support
|
// 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(())
|
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
|
// TODO: Fix error handling here
|
||||||
let tagged_file = match lofty::read_from_path(target_file) {
|
let tagged_file = match lofty::read_from_path(target_file) {
|
||||||
Ok(tagged_file) => tagged_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 custom_insert = String::new();
|
||||||
let mut loops = 0;
|
let mut loops = 0;
|
||||||
for item in tag.items() {
|
for (loops, item) in tag.items().enumerate() {
|
||||||
let mut custom_key = String::new();
|
let mut custom_key = String::new();
|
||||||
match item.key() {
|
match item.key() {
|
||||||
ItemKey::TrackArtist |
|
ItemKey::TrackArtist |
|
||||||
|
@ -203,10 +194,6 @@ pub fn add_file_to_db(target_file: &Path, connection: &Connection) {
|
||||||
if loops > 0 {
|
if loops > 0 {
|
||||||
custom_insert.push_str(", ");
|
custom_insert.push_str(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
custom_insert.push_str(&format!(" (?1, '{}', '{}')", custom_key.replace("\'", "''"), custom_value.replace("\'", "''")));
|
|
||||||
|
|
||||||
loops += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the format as a string
|
// 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
|
// TODO: Fix error handling
|
||||||
let binding = fs::canonicalize(target_file).unwrap();
|
let binding = fs::canonicalize(target_file).unwrap();
|
||||||
let abs_path = binding.to_str().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 fn add_song_to_db(new_song: Song) {
|
||||||
pub enum Tag {
|
|
||||||
SongPath,
|
|
||||||
Title,
|
|
||||||
Album,
|
|
||||||
TrackNum,
|
|
||||||
Artist,
|
|
||||||
Date,
|
|
||||||
Genre,
|
|
||||||
Plays,
|
|
||||||
Favorited,
|
|
||||||
Format,
|
|
||||||
Duration,
|
|
||||||
Custom{tag: String, tag_value: String},
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
#[derive(Debug)]
|
||||||
|
@ -305,93 +233,9 @@ impl MusicObject {
|
||||||
|
|
||||||
/// Query the database, returning a list of items
|
/// Query the database, returning a list of items
|
||||||
pub fn query (
|
pub fn query (
|
||||||
config: &Config,
|
query_string: &String, // The query itself
|
||||||
text_input: &String,
|
target_tags: &Vec<String>, // The tags to search
|
||||||
queried_tags: &Vec<&Tag>,
|
sort_by: &Vec<String>, // Tags to sort the resulting data by
|
||||||
order_by_tags: &Vec<&Tag>,
|
|
||||||
) -> Option<Vec<MusicObject>> {
|
) -> Option<Vec<MusicObject>> {
|
||||||
let db_connection = Connection::open(&*config.db_path).unwrap();
|
unimplemented!()
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,10 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use crate::music_controller::config::Config;
|
use crate::music_controller::config::Config;
|
||||||
use rusqlite::{params, Connection};
|
|
||||||
|
|
||||||
pub fn playlist_add(
|
pub fn playlist_add(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
playlist_name: &str,
|
playlist_name: &str,
|
||||||
song_paths: &Vec<&Path>
|
song_paths: &Vec<&Path>
|
||||||
) {
|
) {
|
||||||
let db_connection = Connection::open(&*config.db_path).unwrap();
|
unimplemented!()
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue