use chrono::Utc;
use diesel::prelude::*;
use diesel::result::{DatabaseErrorKind, Error};
#[cfg(feature = "napi")]
use napi_derive::napi;
#[cfg(feature = "pyo3")]
use pyo3::prelude::*;
#[cfg(feature = "pyo3")]
use rbox_derives::PyMutableMapping;
use std::path::PathBuf;
use uuid::Uuid;
use super::agent_registry::AgentRegistry;
use super::djmd_album::DjmdAlbum;
use super::djmd_artist::DjmdArtist;
use super::djmd_color::DjmdColor;
use super::djmd_device::DjmdDevice;
use super::djmd_genre::DjmdGenre;
use super::djmd_key::DjmdKey;
use super::djmd_label::DjmdLabel;
use super::{Date, DateString, RandomIdGenerator};
use crate::enums::{Analyzed, FileType};
use crate::error::Error as MasterDbError;
use crate::schema::{
djmdAlbum, djmdArtist, djmdColor, djmdContent, djmdGenre, djmdKey, djmdLabel, djmdProperty,
};
use rbox_core::{PathExt, Track, TrackMetadata};
use rbox_db::{load_related, Model, ModelInsert, ModelRelations, ModelUpdate};
#[derive(
Debug, Clone, PartialEq, Default, HasQuery, Identifiable, Insertable, AsChangeset, Associations,
)]
#[diesel(table_name = djmdContent)]
#[diesel(primary_key(id))]
#[diesel(belongs_to(DjmdArtist, foreign_key = artist_id))]
#[diesel(belongs_to(DjmdAlbum, foreign_key = album_id))]
#[diesel(belongs_to(DjmdGenre, foreign_key = genre_id))]
#[diesel(belongs_to(DjmdLabel, foreign_key = label_id))]
#[diesel(belongs_to(DjmdKey, foreign_key = key_id))]
#[diesel(belongs_to(DjmdColor, foreign_key = color_id))]
#[diesel(belongs_to(DjmdDevice, foreign_key = device_id))]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
#[cfg_attr(feature = "pyo3", pyclass(from_py_object, get_all, set_all, mapping))]
#[cfg_attr(feature = "pyo3", derive(PyMutableMapping))]
#[cfg_attr(feature = "napi", napi(object))]
pub struct DjmdContent {
pub id: String,
pub uuid: String,
pub rb_data_status: i32,
pub rb_local_data_status: i32,
pub rb_local_deleted: i32,
pub rb_local_synced: i32,
pub usn: Option<i32>,
pub rb_local_usn: Option<i32>,
#[diesel(serialize_as = DateString)]
#[diesel(deserialize_as = DateString)]
pub created_at: Date,
#[diesel(serialize_as = DateString)]
#[diesel(deserialize_as = DateString)]
pub updated_at: Date,
pub folder_path: Option<String>,
pub file_name_l: Option<String>,
pub file_name_s: Option<String>,
pub title: Option<String>,
pub artist_id: Option<String>,
pub album_id: Option<String>,
pub genre_id: Option<String>,
pub bpm: Option<i32>,
pub length: Option<i32>,
pub track_no: Option<i32>,
pub bit_rate: Option<i32>,
pub bit_depth: Option<i32>,
pub commnt: Option<String>,
pub file_type: i32,
pub rating: Option<i32>,
pub release_year: Option<i32>,
pub remixer_id: Option<String>,
pub label_id: Option<String>,
pub org_artist_id: Option<String>,
pub key_id: Option<String>,
pub stock_date: Option<String>,
pub color_id: Option<String>,
pub dj_play_count: Option<i32>,
pub image_path: Option<String>,
pub master_db_id: Option<String>,
pub master_song_id: Option<String>,
pub analysis_data_path: Option<String>,
pub search_str: Option<String>,
pub file_size: Option<i32>,
pub disc_no: Option<i32>,
pub composer_id: Option<String>,
pub subtitle: Option<String>,
pub sample_rate: Option<i32>,
pub disable_quantize: Option<i32>,
pub analysed: Option<i32>,
pub release_date: Option<String>,
pub date_created: Option<String>,
pub content_link: Option<i32>,
pub tag: Option<String>,
pub modified_by_rbm: Option<String>,
pub hot_cue_auto_load: Option<String>,
pub delivery_control: Option<String>,
pub delivery_comment: Option<String>,
pub cue_updated: Option<String>,
pub analysis_updated: Option<String>,
pub track_info_updated: Option<String>,
pub lyricist: Option<String>,
pub isrc: Option<String>,
pub sampler_track_info: Option<i32>,
pub sampler_play_offset: Option<i32>,
pub sampler_gain: Option<f64>,
pub video_associate: Option<String>,
pub lyric_status: Option<i32>,
pub service_id: Option<i32>,
pub org_folder_path: Option<String>,
pub reserved1: Option<String>,
pub reserved2: Option<String>,
pub reserved3: Option<String>,
pub reserved4: Option<String>,
pub ext_info: Option<String>,
pub rb_file_id: Option<String>,
pub device_id: Option<String>,
pub rb_local_folder_path: Option<String>,
pub src_id: Option<String>,
pub src_title: Option<String>,
pub src_artist_name: Option<String>,
pub src_album_name: Option<String>,
pub src_length: Option<i32>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(from_py_object, get_all))]
#[cfg_attr(feature = "napi", napi(object))]
pub struct DjmdContentView {
pub content: DjmdContent,
pub artist: Option<DjmdArtist>,
pub remixer: Option<DjmdArtist>,
pub original_artist: Option<DjmdArtist>,
pub composer: Option<DjmdArtist>,
pub album: Option<DjmdAlbum>,
pub album_artist: Option<DjmdArtist>,
pub genre: Option<DjmdGenre>,
pub label: Option<DjmdLabel>,
pub key: Option<DjmdKey>,
pub color: Option<DjmdColor>,
}
impl std::ops::Deref for DjmdContentView {
type Target = DjmdContent;
fn deref(&self) -> &Self::Target {
&self.content
}
}
impl ModelRelations for DjmdContent {
type Resolved = DjmdContentView;
fn resolve_many(
conn: &mut SqliteConnection,
models: Vec<Self>,
) -> QueryResult<Vec<Self::Resolved>> {
if models.is_empty() {
return Ok(Vec::new());
}
conn.transaction(|conn| {
let albums = load_related(
conn,
models.iter().filter_map(|row| row.album_id.clone()),
|conn, ids| {
DjmdAlbum::query()
.filter(djmdAlbum::id.eq_any(ids))
.load(conn)
},
|row| row.id.clone(),
)?;
let artists = load_related(
conn,
models
.iter()
.flat_map(|row| {
[
row.artist_id.clone(),
row.remixer_id.clone(),
row.org_artist_id.clone(),
row.composer_id.clone(),
]
.into_iter()
.flatten()
})
.chain(
albums
.values()
.filter_map(|album| album.album_artist_id.clone()),
),
|conn, ids| {
DjmdArtist::query()
.filter(djmdArtist::id.eq_any(ids))
.load(conn)
},
|row| row.id.clone(),
)?;
let genres = load_related(
conn,
models.iter().filter_map(|row| row.genre_id.clone()),
|conn, ids| {
DjmdGenre::query()
.filter(djmdGenre::id.eq_any(ids))
.load(conn)
},
|row| row.id.clone(),
)?;
let labels = load_related(
conn,
models.iter().filter_map(|row| row.label_id.clone()),
|conn, ids| {
DjmdLabel::query()
.filter(djmdLabel::id.eq_any(ids))
.load(conn)
},
|row| row.id.clone(),
)?;
let keys = load_related(
conn,
models.iter().filter_map(|row| row.key_id.clone()),
|conn, ids| DjmdKey::query().filter(djmdKey::id.eq_any(ids)).load(conn),
|row| row.id.clone(),
)?;
let colors = load_related(
conn,
models.iter().filter_map(|row| row.color_id.clone()),
|conn, ids| {
DjmdColor::query()
.filter(djmdColor::id.eq_any(ids))
.load(conn)
},
|row| row.id.clone(),
)?;
Ok(models
.into_iter()
.map(|content| DjmdContentView {
artist: content
.artist_id
.as_ref()
.and_then(|id| artists.get(id))
.cloned(),
remixer: content
.remixer_id
.as_ref()
.and_then(|id| artists.get(id))
.cloned(),
original_artist: content
.org_artist_id
.as_ref()
.and_then(|id| artists.get(id))
.cloned(),
composer: content
.composer_id
.as_ref()
.and_then(|id| artists.get(id))
.cloned(),
album_artist: content
.album_id
.as_ref()
.and_then(|id| albums.get(id))
.and_then(|album| album.album_artist_id.as_ref())
.and_then(|id| artists.get(id))
.cloned(),
album: content
.album_id
.as_ref()
.and_then(|id| albums.get(id))
.cloned(),
genre: content
.genre_id
.as_ref()
.and_then(|id| genres.get(id))
.cloned(),
label: content
.label_id
.as_ref()
.and_then(|id| labels.get(id))
.cloned(),
key: content.key_id.as_ref().and_then(|id| keys.get(id)).cloned(),
color: content
.color_id
.as_ref()
.and_then(|id| colors.get(id))
.cloned(),
content,
})
.collect())
})
}
}
impl Model for DjmdContent {
type Id = str;
fn all(conn: &mut SqliteConnection) -> QueryResult<Vec<Self>> {
Self::query().load(conn)
}
fn find(conn: &mut SqliteConnection, id: &Self::Id) -> QueryResult<Option<Self>> {
Self::query().find(id).first(conn).optional()
}
fn id_exists(conn: &mut SqliteConnection, id: &Self::Id) -> QueryResult<bool> {
diesel::dsl::select(diesel::dsl::exists(Self::query().find(id))).get_result(conn)
}
}
impl ModelUpdate for DjmdContent {
fn update(mut self, conn: &mut SqliteConnection) -> QueryResult<Self> {
self.updated_at = Utc::now();
self.rb_local_usn = Some(AgentRegistry::increment_local_usn_by(conn, 1)?);
diesel::update(djmdContent::table.find(self.id.clone()))
.set(self)
.get_result(conn)
}
}
impl DjmdContent {
pub fn all_with_relations(conn: &mut SqliteConnection) -> QueryResult<Vec<DjmdContentView>> {
Ok(conn.transaction::<_, Error, _>(|conn| {
let contents = DjmdContent::all(conn)?;
Self::resolve_many(conn, contents)
})?)
}
pub fn find_with_relations(
conn: &mut SqliteConnection,
id: &str,
) -> crate::Result<Option<DjmdContentView>> {
Ok(conn.transaction::<_, Error, _>(|conn| {
Self::find(conn, id)?
.map(|content| content.resolve(conn))
.transpose()
})?)
}
pub fn by_ids(conn: &mut SqliteConnection, ids: &[&str]) -> QueryResult<Vec<Self>> {
Self::query().filter(djmdContent::id.eq_any(ids)).load(conn)
}
pub fn by_ids_with_relations(
conn: &mut SqliteConnection,
ids: &[&str],
) -> crate::Result<Vec<DjmdContentView>> {
conn.transaction::<_, MasterDbError, _>(|conn| {
let contents = Self::by_ids(conn, ids)?;
Ok(Self::resolve_many(conn, contents)?)
})
}
pub fn find_by_path(conn: &mut SqliteConnection, name: &str) -> QueryResult<Option<Self>> {
Self::query()
.filter(djmdContent::folder_path.eq(name))
.first(conn)
.optional()
}
pub fn find_by_path_with_relations(
conn: &mut SqliteConnection,
id: &str,
) -> crate::Result<Option<DjmdContentView>> {
Ok(conn.transaction::<_, Error, _>(|conn| {
Self::find_by_path(conn, id)?
.map(|content| content.resolve(conn))
.transpose()
})?)
}
pub fn path_exists(conn: &mut SqliteConnection, path: &str) -> QueryResult<bool> {
let query = Self::query().filter(djmdContent::folder_path.eq(path));
diesel::dsl::select(diesel::dsl::exists(query)).get_result(conn)
}
pub fn file_id_exists(conn: &mut SqliteConnection, id: &str) -> QueryResult<bool> {
let query = Self::query().filter(djmdContent::rb_file_id.eq(id));
diesel::dsl::select(diesel::dsl::exists(query)).get_result(conn)
}
pub fn find_anlz_path(conn: &mut SqliteConnection, id: &str) -> QueryResult<Option<String>> {
djmdContent::table
.find(id)
.select(djmdContent::analysis_data_path)
.first(conn)
}
pub fn update(mut self, conn: &mut SqliteConnection) -> QueryResult<Self> {
self.updated_at = Utc::now();
self.rb_local_usn = Some(AgentRegistry::increment_local_usn_by(conn, 1)?);
diesel::update(djmdContent::table.find(self.id.clone()))
.set(self)
.get_result(conn)
}
pub fn set_album_id(
conn: &mut SqliteConnection,
id: &str,
album_id: &str,
) -> QueryResult<usize> {
diesel::update(djmdContent::table.find(id))
.set(djmdContent::album_id.eq(album_id))
.execute(conn)
}
pub fn set_artist_id(
conn: &mut SqliteConnection,
id: &str,
artist_id: &str,
) -> QueryResult<usize> {
diesel::update(djmdContent::table.find(id))
.set(djmdContent::artist_id.eq(artist_id))
.execute(conn)
}
pub fn set_remixer_id(
conn: &mut SqliteConnection,
id: &str,
artist_id: &str,
) -> QueryResult<usize> {
diesel::update(djmdContent::table.find(id))
.set(djmdContent::remixer_id.eq(artist_id))
.execute(conn)
}
pub fn set_original_artist_id(
conn: &mut SqliteConnection,
id: &str,
artist_id: &str,
) -> QueryResult<usize> {
diesel::update(djmdContent::table.find(id))
.set(djmdContent::org_artist_id.eq(artist_id))
.execute(conn)
}
pub fn set_composer_id(
conn: &mut SqliteConnection,
id: &str,
artist_id: &str,
) -> QueryResult<usize> {
diesel::update(djmdContent::table.find(id))
.set(djmdContent::composer_id.eq(artist_id))
.execute(conn)
}
pub fn set_genre_id(
conn: &mut SqliteConnection,
id: &str,
genre_id: &str,
) -> QueryResult<usize> {
diesel::update(djmdContent::table.find(id))
.set(djmdContent::genre_id.eq(genre_id))
.execute(conn)
}
pub fn set_label_id(
conn: &mut SqliteConnection,
id: &str,
label_id: &str,
) -> QueryResult<usize> {
diesel::update(djmdContent::table.find(id))
.set(djmdContent::label_id.eq(label_id))
.execute(conn)
}
pub fn set_key_id(conn: &mut SqliteConnection, id: &str, key_id: &str) -> QueryResult<usize> {
diesel::update(djmdContent::table.find(id))
.set(djmdContent::key_id.eq(key_id))
.execute(conn)
}
pub fn set_path(conn: &mut SqliteConnection, id: &str, path: &str) -> QueryResult<usize> {
diesel::update(djmdContent::table.find(id))
.set(djmdContent::folder_path.eq(path))
.execute(conn)
}
fn generate_id(conn: &mut SqliteConnection) -> QueryResult<String> {
let generator = RandomIdGenerator::new(true);
let mut id: String = String::new();
for id_result in generator {
if let Ok(tmp_id) = id_result {
if !Self::id_exists(conn, &tmp_id)? {
id = tmp_id;
break;
}
}
}
Ok(id)
}
fn generate_file_id(conn: &mut SqliteConnection) -> QueryResult<String> {
let generator = RandomIdGenerator::new(true);
let mut id: String = String::new();
for id_result in generator {
if let Ok(tmp_id) = id_result {
if !Self::file_id_exists(conn, &tmp_id)? {
id = tmp_id;
break;
}
}
}
Ok(id)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "pyo3", pyclass(from_py_object, get_all, set_all, mapping))]
#[cfg_attr(feature = "pyo3", derive(PyMutableMapping))]
#[cfg_attr(feature = "napi", napi(object))]
pub struct NewDjmdContent {
pub folder_path: String,
pub title: Option<String>,
pub artist_id: Option<String>,
pub album_id: Option<String>,
pub genre_id: Option<String>,
pub bpm: Option<i32>,
pub length: Option<i32>,
pub track_no: Option<i32>,
pub bit_rate: Option<i32>,
pub bit_depth: Option<i32>,
pub commnt: Option<String>,
pub file_type: Option<i32>,
pub rating: Option<i32>,
pub release_year: Option<i32>,
pub remixer_id: Option<String>,
pub label_id: Option<String>,
pub org_artist_id: Option<String>,
pub key_id: Option<String>,
pub stock_date: Option<String>,
pub color_id: Option<String>,
pub dj_play_count: Option<i32>,
pub image_path: Option<String>,
pub analysis_data_path: Option<String>,
pub search_str: Option<String>,
pub disc_no: Option<i32>,
pub composer_id: Option<String>,
pub subtitle: Option<String>,
pub sample_rate: Option<i32>,
pub release_date: Option<String>,
pub date_created: Option<String>,
pub content_link: Option<i32>,
pub tag: Option<String>,
pub delivery_comment: Option<String>,
pub lyricist: Option<String>,
pub isrc: Option<String>,
pub sampler_track_info: Option<i32>,
pub sampler_play_offset: Option<i32>,
pub sampler_gain: Option<f64>,
pub video_associate: Option<String>,
pub lyric_status: Option<i32>,
pub org_folder_path: Option<String>,
pub reserved1: Option<String>,
pub reserved2: Option<String>,
pub reserved3: Option<String>,
pub reserved4: Option<String>,
pub ext_info: Option<String>,
pub device_id: Option<String>,
pub rb_local_folder_path: Option<String>,
pub src_id: Option<String>,
pub src_title: Option<String>,
pub src_artist_name: Option<String>,
pub src_album_name: Option<String>,
pub src_length: Option<i32>,
}
impl ModelInsert for NewDjmdContent {
type Model = DjmdContent;
fn insert(self, conn: &mut SqliteConnection) -> QueryResult<Self::Model> {
let path = PathBuf::from(&self.folder_path);
let rb_path_str = path
.normalize_unix_string()
.expect("Failed to convert path to string");
if !path.is_file() || !path.exists() {
let e = Error::DatabaseError(
DatabaseErrorKind::UniqueViolation,
Box::new(format!("File does not exist: {}", rb_path_str)),
);
return Err(e);
}
if Self::Model::path_exists(conn, &rb_path_str)? {
let e = Error::DatabaseError(
DatabaseErrorKind::UniqueViolation,
Box::new(format!("Path is not unique: {}", rb_path_str)),
);
return Err(e);
}
let invalid_input = |message: String| {
Error::SerializationError(Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
message,
)))
};
let file_type = match self.file_type {
Some(file_type) => file_type,
None => {
let ext = path
.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| {
invalid_input(format!(
"Missing or invalid file extension: {}",
rb_path_str
))
})?;
FileType::try_from_extension(ext)
.map_err(invalid_input)?
.into()
}
};
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| invalid_input(format!("Invalid file name: {}", rb_path_str)))?
.to_string();
let meta =
std::fs::metadata(&path).map_err(|err| Error::SerializationError(Box::new(err)))?;
let file_size =
i32::try_from(meta.len()).map_err(|err| Error::SerializationError(Box::new(err)))?;
let (db_id, library_device_id): (String, String) = djmdProperty::table
.select((djmdProperty::db_id, djmdProperty::device_id))
.first(conn)?;
let content_link = self.content_link;
let id = Self::Model::generate_id(conn)?;
let file_id = Self::Model::generate_file_id(conn)?;
let uuid = Uuid::new_v4().to_string();
let usn = AgentRegistry::increment_local_usn(conn)?;
let now = Utc::now();
let item = Self::Model {
id: id.clone(),
uuid,
rb_local_usn: Some(usn),
created_at: now,
updated_at: now,
folder_path: Some(rb_path_str.to_string()),
file_name_l: Some(file_name.clone()),
file_name_s: Some("".to_string()),
file_type: file_type as i32,
master_db_id: Some(db_id),
master_song_id: Some(id),
file_size: Some(file_size),
content_link,
hot_cue_auto_load: Some("on".to_string()),
delivery_control: Some(String::new()),
modified_by_rbm: Some(String::new()),
service_id: Some(0),
rb_file_id: Some(file_id),
title: self.title,
artist_id: self.artist_id,
album_id: self.album_id,
genre_id: self.genre_id,
bpm: self.bpm,
length: self.length,
track_no: self.track_no,
bit_rate: self.bit_rate,
bit_depth: self.bit_depth,
commnt: self.commnt,
rating: self.rating.or(Some(0)),
release_year: self.release_year,
remixer_id: self.remixer_id,
label_id: self.label_id,
org_artist_id: self.org_artist_id,
key_id: self.key_id,
stock_date: self.stock_date,
color_id: Some(self.color_id.unwrap_or_else(|| "0".into())),
dj_play_count: self.dj_play_count.or(Some(0)),
image_path: self.image_path,
analysis_data_path: self.analysis_data_path,
search_str: self.search_str,
disc_no: self.disc_no.or(Some(0)),
composer_id: self.composer_id,
subtitle: Some(self.subtitle.unwrap_or_default()),
sample_rate: self.sample_rate,
release_date: self.release_date,
date_created: self.date_created,
tag: self.tag,
delivery_comment: Some(self.delivery_comment.unwrap_or_default()),
lyricist: Some(self.lyricist.unwrap_or_default()),
isrc: self.isrc,
sampler_track_info: self.sampler_track_info.or(Some(0)),
sampler_play_offset: self.sampler_play_offset.or(Some(0)),
sampler_gain: self.sampler_gain.or(Some(0.0)),
video_associate: Some(self.video_associate.unwrap_or_else(|| "0".into())),
lyric_status: self.lyric_status.or(Some(0)),
org_folder_path: Some(self.org_folder_path.unwrap_or_default()),
reserved1: Some(self.reserved1.unwrap_or_default()),
reserved2: self.reserved2,
reserved3: self.reserved3,
reserved4: self.reserved4,
ext_info: Some(self.ext_info.unwrap_or_else(|| "null".into())),
device_id: Some(self.device_id.unwrap_or(library_device_id)),
rb_local_folder_path: self.rb_local_folder_path,
src_id: self.src_id,
src_title: self.src_title,
src_artist_name: self.src_artist_name,
src_album_name: self.src_album_name,
src_length: self.src_length,
analysed: Some(Analyzed::NotAnalyzed as i32),
..Default::default()
};
diesel::insert_into(djmdContent::table)
.values(item)
.get_result(conn)
}
}
impl NewDjmdContent {
pub fn new<S: Into<String>>(path: S) -> Self {
Self {
folder_path: path.into(),
..Default::default()
}
}
pub fn file_type(mut self, file_type: i32) -> Self {
self.file_type = Some(file_type);
self
}
pub fn title<S: Into<String>>(mut self, title: S) -> Self {
self.title = Some(title.into());
self
}
pub fn artist_id<S: Into<String>>(mut self, artist_id: S) -> Self {
self.artist_id = Some(artist_id.into());
self
}
pub fn album_id<S: Into<String>>(mut self, album_id: S) -> Self {
self.album_id = Some(album_id.into());
self
}
pub fn genre_id<S: Into<String>>(mut self, genre_id: S) -> Self {
self.genre_id = Some(genre_id.into());
self
}
pub fn bpm(mut self, bpm: i32) -> Self {
self.bpm = Some(bpm);
self
}
pub fn length(mut self, length: i32) -> Self {
self.length = Some(length);
self
}
pub fn track_no(mut self, track_no: i32) -> Self {
self.track_no = Some(track_no);
self
}
pub fn bit_rate(mut self, bit_rate: i32) -> Self {
self.bit_rate = Some(bit_rate);
self
}
pub fn bit_depth(mut self, bit_depth: i32) -> Self {
self.bit_depth = Some(bit_depth);
self
}
pub fn commnt<S: Into<String>>(mut self, commnt: S) -> Self {
self.commnt = Some(commnt.into());
self
}
pub fn rating(mut self, rating: i32) -> Self {
self.rating = Some(rating);
self
}
pub fn release_year(mut self, release_year: i32) -> Self {
self.release_year = Some(release_year);
self
}
pub fn remixer_id<S: Into<String>>(mut self, remixer_id: S) -> Self {
self.remixer_id = Some(remixer_id.into());
self
}
pub fn label_id<S: Into<String>>(mut self, label_id: S) -> Self {
self.label_id = Some(label_id.into());
self
}
pub fn org_artist_id<S: Into<String>>(mut self, org_artist_id: S) -> Self {
self.org_artist_id = Some(org_artist_id.into());
self
}
pub fn key_id<S: Into<String>>(mut self, key_id: S) -> Self {
self.key_id = Some(key_id.into());
self
}
pub fn stock_date<S: Into<String>>(mut self, stock_date: S) -> Self {
self.stock_date = Some(stock_date.into());
self
}
pub fn search_str<S: Into<String>>(mut self, search_str: S) -> Self {
self.search_str = Some(search_str.into());
self
}
pub fn disc_no(mut self, disc_no: i32) -> Self {
self.disc_no = Some(disc_no);
self
}
pub fn composer_id<S: Into<String>>(mut self, composer_id: S) -> Self {
self.composer_id = Some(composer_id.into());
self
}
pub fn subtitle<S: Into<String>>(mut self, subtitle: S) -> Self {
self.subtitle = Some(subtitle.into());
self
}
pub fn sample_rate(mut self, sample_rate: i32) -> Self {
self.sample_rate = Some(sample_rate);
self
}
pub fn release_date<S: Into<String>>(mut self, release_date: S) -> Self {
self.release_date = Some(release_date.into());
self
}
pub fn date_created<S: Into<String>>(mut self, date_created: S) -> Self {
self.date_created = Some(date_created.into());
self
}
pub fn tag<S: Into<String>>(mut self, tag: S) -> Self {
self.tag = Some(tag.into());
self
}
pub fn lyricist<S: Into<String>>(mut self, lyricist: S) -> Self {
self.lyricist = Some(lyricist.into());
self
}
pub fn isrc<S: Into<String>>(mut self, isrc: S) -> Self {
self.isrc = Some(isrc.into());
self
}
}
impl TryFrom<&DjmdContentView> for Track {
type Error = MasterDbError;
fn try_from(view: &DjmdContentView) -> Result<Self, Self::Error> {
let input = &view.content;
if input.rating.is_some_and(|value| !(0..=5).contains(&value)) {
return Err(MasterDbError::InvalidOperation(
"rating must be between 0 and 5".into(),
));
}
if input.bpm.is_some_and(|value| value < 0) || input.length.is_some_and(|value| value < 0) {
return Err(MasterDbError::InvalidOperation(
"bpm and length must be nonnegative".into(),
));
}
Ok(Self {
id: input.id.clone(),
uuid: input.uuid.clone(),
path: input
.folder_path
.as_ref()
.map(PathBuf::from)
.unwrap_or_default(),
file_type: Some(input.file_type),
bpm: input.bpm.map(|value| f64::from(value) / 100.0),
length: input.length.map(f64::from),
title: input.title.clone(),
search_str: input.search_str.clone(),
tag: input.tag.clone(),
commnt: input.commnt.clone(),
stock_date: input.stock_date.clone(),
subtitle: input.subtitle.clone(),
release_date: input.release_date.clone(),
date_created: input.date_created.clone(),
lyricist: input.lyricist.clone(),
isrc: input.isrc.clone(),
rating: input.rating.map(u8::try_from).transpose().map_err(|_| {
MasterDbError::InvalidOperation("rating is outside the Track u8 range".into())
})?,
track_no: input.track_no.map(u32::try_from).transpose().map_err(|_| {
MasterDbError::InvalidOperation("track_no is outside the Track u32 range".into())
})?,
bit_rate: input.bit_rate.map(u32::try_from).transpose().map_err(|_| {
MasterDbError::InvalidOperation("bit_rate is outside the Track u32 range".into())
})?,
bit_depth: input
.bit_depth
.map(u16::try_from)
.transpose()
.map_err(|_| {
MasterDbError::InvalidOperation(
"bit_depth is outside the Track u16 range".into(),
)
})?,
release_year: input
.release_year
.map(u16::try_from)
.transpose()
.map_err(|_| {
MasterDbError::InvalidOperation(
"release_year is outside the Track u16 range".into(),
)
})?,
file_size: input
.file_size
.map(u64::try_from)
.transpose()
.map_err(|_| {
MasterDbError::InvalidOperation(
"file_size is outside the Track u64 range".into(),
)
})?,
disc_no: input.disc_no.map(u32::try_from).transpose().map_err(|_| {
MasterDbError::InvalidOperation("disc_no is outside the Track u32 range".into())
})?,
sample_rate: input
.sample_rate
.map(u32::try_from)
.transpose()
.map_err(|_| {
MasterDbError::InvalidOperation(
"sample_rate is outside the Track u32 range".into(),
)
})?,
artist: view.artist.as_ref().map(|row| row.name.clone()),
album: view.album.as_ref().map(|row| row.name.clone()),
genre: view.genre.as_ref().map(|row| row.name.clone()),
remixer: view.remixer.as_ref().map(|row| row.name.clone()),
org_artist: view.original_artist.as_ref().map(|row| row.name.clone()),
composer: view.composer.as_ref().map(|row| row.name.clone()),
key: view.key.as_ref().map(|row| row.name.clone()),
label: view.label.as_ref().map(|row| row.name.clone()),
album_artist: view.album_artist.as_ref().map(|row| row.name.clone()),
})
}
}
impl TryFrom<&TrackMetadata> for NewDjmdContent {
type Error = MasterDbError;
fn try_from(input: &TrackMetadata) -> Result<Self, Self::Error> {
if input.rating.is_some_and(|value| value > 5) {
return Err(MasterDbError::InvalidOperation(
"rating must be between 0 and 5".into(),
));
}
Ok(Self {
folder_path: input
.path
.normalize_unix_string()
.ok_or_else(|| MasterDbError::InvalidOperation("Track path is not UTF-8".into()))?,
file_type: input.file_type,
title: input.title.clone(),
search_str: input.search_str.clone(),
tag: input.tag.clone(),
commnt: input.commnt.clone(),
stock_date: input.stock_date.clone(),
subtitle: input.subtitle.clone(),
release_date: input.release_date.clone(),
date_created: input.date_created.clone(),
lyricist: input.lyricist.clone(),
isrc: input.isrc.clone(),
bpm: input
.bpm
.map(|value| {
let rounded = (value * 100.0).round();
if !value.is_finite() || value < 0.0 || rounded > f64::from(i32::MAX) {
return Err(MasterDbError::InvalidOperation(
"bpm must be finite, nonnegative, and fit the database's i32 range"
.into(),
));
}
Ok(rounded as i32)
})
.transpose()?,
length: input
.length
.map(|value| {
let rounded = (value * 1.0).round();
if !value.is_finite() || value < 0.0 || rounded > f64::from(i32::MAX) {
return Err(MasterDbError::InvalidOperation(
"length must be finite, nonnegative, and fit the database's i32 range"
.into(),
));
}
Ok(rounded as i32)
})
.transpose()?,
rating: input.rating.map(i32::from),
bit_depth: input.bit_depth.map(i32::from),
release_year: input.release_year.map(i32::from),
track_no: input.track_no.map(i32::try_from).transpose().map_err(|_| {
MasterDbError::InvalidOperation("track_no exceeds the database i32 range".into())
})?,
bit_rate: input.bit_rate.map(i32::try_from).transpose().map_err(|_| {
MasterDbError::InvalidOperation("bit_rate exceeds the database i32 range".into())
})?,
disc_no: input.disc_no.map(i32::try_from).transpose().map_err(|_| {
MasterDbError::InvalidOperation("disc_no exceeds the database i32 range".into())
})?,
sample_rate: input
.sample_rate
.map(i32::try_from)
.transpose()
.map_err(|_| {
MasterDbError::InvalidOperation(
"sample_rate exceeds the database i32 range".into(),
)
})?,
..Default::default()
})
}
}
impl TryFrom<DjmdContentView> for Track {
type Error = MasterDbError;
fn try_from(input: DjmdContentView) -> Result<Self, Self::Error> {
Self::try_from(&input)
}
}
impl TryFrom<TrackMetadata> for NewDjmdContent {
type Error = MasterDbError;
fn try_from(input: TrackMetadata) -> Result<Self, Self::Error> {
Self::try_from(&input)
}
}