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::schema::{djmdAlbum, djmdArtist, djmdColor, djmdContent, djmdGenre, djmdKey, djmdLabel};
use super::{Date, DateString, RandomIdGenerator};
use crate::enums::{Analyzed, FileType};
use crate::model_traits::{load_related, Model, ModelInsert, ModelRelations, ModelUpdate};
#[cfg(feature = "pyo3")]
use crate::util::{PyItemsIter, PyObjectIter, PyStrIter};
use crate::PathExt;
#[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(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(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 genre: Option<DjmdGenre>,
pub label: Option<DjmdLabel>,
pub key: Option<DjmdKey>,
pub color: Option<DjmdColor>,
}
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 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()
}),
|conn, ids| {
DjmdArtist::query()
.filter(djmdArtist::id.eq_any(ids))
.load(conn)
},
|row| row.id.clone(),
)?;
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 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: 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 by_ids(conn: &mut SqliteConnection, ids: &[&str]) -> QueryResult<Vec<Self>> {
Self::query().filter(djmdContent::id.eq_any(ids)).load(conn)
}
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 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(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 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 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 tag: Option<String>,
pub lyricist: Option<String>,
pub isrc: Option<String>,
}
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 ext = path
.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| {
invalid_input(format!(
"Missing or invalid file extension: {}",
rb_path_str
))
})?;
let file_type = FileType::try_from_extension(ext).map_err(invalid_input)?;
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: u64 = meta.len();
let db_id = DjmdDevice::master_db_id(conn)?;
let content_link = djmdContent::table
.select(djmdContent::content_link)
.order(djmdContent::rb_local_usn.desc())
.limit(1)
.first(conn)?;
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 as i32),
content_link,
hot_cue_auto_load: Some("on".to_string()),
delivery_control: Some("on".to_string()),
sampler_track_info: Some(0),
sampler_play_offset: Some(0),
sampler_gain: Some(0.0),
lyric_status: Some(0),
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,
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,
search_str: self.search_str,
disc_no: self.disc_no,
composer_id: self.composer_id,
subtitle: self.subtitle,
sample_rate: self.sample_rate,
release_date: self.release_date,
date_created: self.date_created,
tag: self.tag,
lyricist: self.lyricist,
isrc: self.isrc,
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 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
}
}