use mineral_channel_core::store::AlbumCacheEntry;
use mineral_model::MediaUrl;
use sea_orm::{DbErr, EntityTrait, Set, TransactionTrait};
use crate::persistence::db::NamespaceStore;
use crate::persistence::entity::album_cache::{albums, artists, track_artists, tracks};
use crate::persistence::{Error, Result};
const INSERT_BATCH_SIZE: usize = 50;
impl NamespaceStore {
pub async fn put_album_cache(&self, entry: &AlbumCacheEntry) -> Result<()> {
let album = &entry.album;
self.check_source(album.source())?;
for artist in &album.artists {
self.check_source(artist.id.namespace())?;
}
for track in &album.tracks {
self.check_song(&track.song)?;
}
let Some(db) = self.pool() else {
return Ok(());
};
let namespace = self.namespace();
let album_id = album.id.value();
let header = albums::ActiveModel {
namespace: Set(namespace.to_owned()),
album_id: Set(album_id.to_owned()),
name: Set(album.name.clone()),
description: Set(album.description.clone()),
company: Set(album.company.clone()),
publish_time_ms: Set(album.publish_time_ms),
track_count: Set(album.track_count.map(i64::try_from).transpose()?),
cover_url: Set(album.cover_url.as_ref().map(MediaUrl::to_string)),
expired_at: Set(entry.expired_at),
};
let album_artists = album
.artists
.iter()
.enumerate()
.map(|(position, artist)| {
Ok(artists::ActiveModel {
namespace: Set(namespace.to_owned()),
album_id: Set(album_id.to_owned()),
position: Set(i64::try_from(position)?),
artist_id: Set(artist.id.value().to_owned()),
name: Set(artist.name.clone()),
})
})
.collect::<Result<Vec<_>>>()?;
let mut album_tracks = Vec::with_capacity(album.tracks.len());
let mut song_artists = Vec::new();
for track in &album.tracks {
let collection_index = i64::try_from(track.index.get())?;
let song = &track.song;
album_tracks.push(tracks::ActiveModel {
namespace: Set(namespace.to_owned()),
album_id: Set(album_id.to_owned()),
collection_index: Set(collection_index),
song_value: Set(song.id.value().to_owned()),
name: Set(song.name.clone()),
alias: Set(song.alias.clone()),
song_album_id: Set(song.album.as_ref().map(|a| a.id.value().to_owned())),
song_album_name: Set(song.album.as_ref().map(|a| a.name.clone())),
duration_ms: Set(song.duration_ms.map(i64::try_from).transpose()?),
cover_url: Set(song.cover_url.as_ref().map(MediaUrl::to_string)),
source_url: Set(song.source_url.as_ref().map(MediaUrl::to_string)),
unavailable: Set(song.unavailable),
});
for (position, artist) in song.artists.iter().enumerate() {
song_artists.push(track_artists::ActiveModel {
namespace: Set(namespace.to_owned()),
album_id: Set(album_id.to_owned()),
collection_index: Set(collection_index),
position: Set(i64::try_from(position)?),
artist_id: Set(artist.id.value().to_owned()),
name: Set(artist.name.clone()),
});
}
}
let tx = db.begin().await.map_err(database_error)?;
albums::Entity::delete_by_id((namespace.to_owned(), album_id.to_owned()))
.exec(&tx)
.await
.map_err(database_error)?;
albums::Entity::insert(header)
.exec_without_returning(&tx)
.await
.map_err(database_error)?;
for batch in album_artists.chunks(INSERT_BATCH_SIZE) {
artists::Entity::insert_many(batch.to_vec())
.exec_without_returning(&tx)
.await
.map_err(database_error)?;
}
for batch in album_tracks.chunks(INSERT_BATCH_SIZE) {
tracks::Entity::insert_many(batch.to_vec())
.exec_without_returning(&tx)
.await
.map_err(database_error)?;
}
for batch in song_artists.chunks(INSERT_BATCH_SIZE) {
track_artists::Entity::insert_many(batch.to_vec())
.exec_without_returning(&tx)
.await
.map_err(database_error)?;
}
tx.commit().await.map_err(database_error)?;
mineral_log::debug!(target: "persist", album = %album.id, tracks = album.tracks.len(),
expired_at = %entry.expired_at, "saved album cache snapshot");
Ok(())
}
}
fn database_error(source: DbErr) -> Error {
Error::Database {
operation: "replace album cache",
source,
}
}