song upload
This commit is contained in:
@@ -215,11 +215,14 @@ async fn read_multipart_file(mut multipart: Multipart, max_bytes: usize) -> Resu
|
||||
Err(Error::BadRequest("multipart field `file` is required".to_string()))
|
||||
}
|
||||
|
||||
async fn read_track_upload(mut multipart: Multipart) -> Result<(Vec<u8>, Option<String>, Option<i32>, bool)> {
|
||||
async fn read_track_upload(
|
||||
mut multipart: Multipart,
|
||||
) -> Result<(Vec<u8>, Option<String>, Option<i32>, bool, bool)> {
|
||||
let mut data = None;
|
||||
let mut title = None;
|
||||
let mut track_number = None;
|
||||
let mut featured = false;
|
||||
let mut published = false;
|
||||
|
||||
while let Some(mut field) = multipart
|
||||
.next_field()
|
||||
@@ -251,6 +254,7 @@ async fn read_track_upload(mut multipart: Multipart) -> Result<(Vec<u8>, Option<
|
||||
track_number = value.trim().parse::<i32>().ok().filter(|number| *number > 0)
|
||||
}
|
||||
"featured" => featured = value == "on" || value == "true" || value == "1",
|
||||
"published" => published = value == "on" || value == "true" || value == "1",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -261,7 +265,7 @@ async fn read_track_upload(mut multipart: Multipart) -> Result<(Vec<u8>, Option<
|
||||
return Err(Error::BadRequest("empty file upload".to_string()));
|
||||
}
|
||||
|
||||
Ok((data, title, track_number, featured))
|
||||
Ok((data, title, track_number, featured, published))
|
||||
}
|
||||
|
||||
async fn unique_album_slug(ctx: &AppContext, title: &str) -> Result<String> {
|
||||
@@ -282,18 +286,22 @@ async fn unique_album_slug(ctx: &AppContext, title: &str) -> Result<String> {
|
||||
Ok(slug)
|
||||
}
|
||||
|
||||
async fn unique_track_slug(ctx: &AppContext, album_id: Uuid, title: &str) -> Result<String> {
|
||||
async fn unique_track_slug(ctx: &AppContext, album_id: Option<Uuid>, title: &str) -> Result<String> {
|
||||
let base = slugify(title);
|
||||
let mut slug = base.clone();
|
||||
let mut suffix = 2;
|
||||
|
||||
while audio_tracks::Entity::find()
|
||||
.filter(audio_tracks::Column::AlbumId.eq(album_id))
|
||||
.filter(audio_tracks::Column::Slug.eq(&slug))
|
||||
.count(&ctx.db)
|
||||
.await?
|
||||
> 0
|
||||
{
|
||||
loop {
|
||||
let mut query = audio_tracks::Entity::find().filter(audio_tracks::Column::Slug.eq(&slug));
|
||||
query = if let Some(album_id) = album_id {
|
||||
query.filter(audio_tracks::Column::AlbumId.eq(album_id))
|
||||
} else {
|
||||
query.filter(audio_tracks::Column::AlbumId.is_null())
|
||||
};
|
||||
|
||||
if query.count(&ctx.db).await? == 0 {
|
||||
break;
|
||||
}
|
||||
slug = format!("{base}-{suffix}");
|
||||
suffix += 1;
|
||||
}
|
||||
@@ -452,6 +460,7 @@ async fn public_album(
|
||||
|
||||
let tracks = audio_tracks::Entity::find()
|
||||
.filter(audio_tracks::Column::AlbumId.eq(album.id))
|
||||
.filter(audio_tracks::Column::Published.eq(true))
|
||||
.order_by_asc(audio_tracks::Column::TrackNumber)
|
||||
.order_by_asc(audio_tracks::Column::Title)
|
||||
.all(&ctx.db)
|
||||
@@ -468,6 +477,26 @@ async fn public_album(
|
||||
)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn public_tracks(
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let tracks = audio_tracks::Entity::find()
|
||||
.filter(audio_tracks::Column::Published.eq(true))
|
||||
.order_by_desc(audio_tracks::Column::PublishedAt)
|
||||
.order_by_desc(audio_tracks::Column::CreatedAt)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
|
||||
format::view(
|
||||
&v,
|
||||
"audio/tracks.html",
|
||||
json!({ "tracks": tracks, "logged_in_admin": logged_in_admin(&ctx, &jar).await }),
|
||||
)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_albums(
|
||||
auth: auth::JWT,
|
||||
@@ -491,6 +520,21 @@ async fn admin_albums(
|
||||
format::view(&v, "admin/audio/albums.html", json!({ "albums": rows }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_tracks(
|
||||
auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
let tracks = audio_tracks::Entity::find()
|
||||
.order_by_desc(audio_tracks::Column::CreatedAt)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
|
||||
format::view(&v, "admin/audio/songs.html", json!({ "tracks": tracks }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_album_new(auth: auth::JWT, ViewEngine(v): ViewEngine<TeraView>, State(ctx): State<AppContext>) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
@@ -566,6 +610,45 @@ async fn admin_track_upload_form(
|
||||
)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_song_upload_form(
|
||||
auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
format::view(&v, "admin/audio/upload_track.html", json!({ "album": null }))
|
||||
}
|
||||
|
||||
async fn create_uploaded_track(
|
||||
ctx: &AppContext,
|
||||
album_id: Option<Uuid>,
|
||||
multipart: Multipart,
|
||||
) -> Result<audio_tracks::Model> {
|
||||
let (data, title, track_number, featured, published) = read_track_upload(multipart).await?;
|
||||
let extension = detect_audio_extension(&data)?;
|
||||
let filename = store_upload(ctx, AUDIO_STORAGE_DIR, extension, data).await?;
|
||||
let title = title.unwrap_or_else(|| filename.trim_end_matches(&format!(".{extension}")).to_string());
|
||||
|
||||
audio_tracks::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
album_id: Set(album_id),
|
||||
title: Set(title.clone()),
|
||||
slug: Set(unique_track_slug(ctx, album_id, &title).await?),
|
||||
audio_file_id: Set(filename),
|
||||
track_number: Set(track_number),
|
||||
duration: Set(None),
|
||||
featured: Set(featured),
|
||||
published: Set(published),
|
||||
play_count: Set(0),
|
||||
published_at: Set(published.then(|| Utc::now().into())),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&ctx.db)
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_track_upload(
|
||||
auth: auth::JWT,
|
||||
@@ -576,29 +659,22 @@ async fn admin_track_upload(
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
album_by_id(&ctx, album_id).await?;
|
||||
|
||||
let (data, title, track_number, featured) = read_track_upload(multipart).await?;
|
||||
let extension = detect_audio_extension(&data)?;
|
||||
let filename = store_upload(&ctx, AUDIO_STORAGE_DIR, extension, data).await?;
|
||||
let title = title.unwrap_or_else(|| filename.trim_end_matches(&format!(".{extension}")).to_string());
|
||||
|
||||
audio_tracks::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
album_id: Set(album_id),
|
||||
title: Set(title.clone()),
|
||||
slug: Set(unique_track_slug(&ctx, album_id, &title).await?),
|
||||
audio_file_id: Set(filename),
|
||||
track_number: Set(track_number),
|
||||
duration: Set(None),
|
||||
featured: Set(featured),
|
||||
play_count: Set(0),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&ctx.db)
|
||||
.await?;
|
||||
create_uploaded_track(&ctx, Some(album_id), multipart).await?;
|
||||
|
||||
format::redirect(&format!("/admin/audio/albums/{album_id}/tracks"))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_song_upload(
|
||||
auth: auth::JWT,
|
||||
State(ctx): State<AppContext>,
|
||||
multipart: Multipart,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
create_uploaded_track(&ctx, None, multipart).await?;
|
||||
format::redirect("/admin/audio/tracks")
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_track_delete(
|
||||
auth: auth::JWT,
|
||||
@@ -613,7 +689,53 @@ async fn admin_track_delete(
|
||||
.delete(StdPath::new(&format!("{AUDIO_STORAGE_DIR}/{}", track.audio_file_id)))
|
||||
.await;
|
||||
track.delete(&ctx.db).await?;
|
||||
format::redirect(&format!("/admin/audio/albums/{album_id}/tracks"))
|
||||
if let Some(album_id) = album_id {
|
||||
format::redirect(&format!("/admin/audio/albums/{album_id}/tracks"))
|
||||
} else {
|
||||
format::redirect("/admin/audio/tracks")
|
||||
}
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_track_publish(
|
||||
auth: auth::JWT,
|
||||
Path(id): Path<Uuid>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
let track = track_by_id(&ctx, id).await?;
|
||||
let album_id = track.album_id;
|
||||
let mut active = track.into_active_model();
|
||||
active.published = Set(true);
|
||||
active.published_at = Set(Some(Utc::now().into()));
|
||||
active.update(&ctx.db).await?;
|
||||
|
||||
if let Some(album_id) = album_id {
|
||||
format::redirect(&format!("/admin/audio/albums/{album_id}/tracks"))
|
||||
} else {
|
||||
format::redirect("/admin/audio/tracks")
|
||||
}
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_track_unpublish(
|
||||
auth: auth::JWT,
|
||||
Path(id): Path<Uuid>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
let track = track_by_id(&ctx, id).await?;
|
||||
let album_id = track.album_id;
|
||||
let mut active = track.into_active_model();
|
||||
active.published = Set(false);
|
||||
active.published_at = Set(None);
|
||||
active.update(&ctx.db).await?;
|
||||
|
||||
if let Some(album_id) = album_id {
|
||||
format::redirect(&format!("/admin/audio/albums/{album_id}/tracks"))
|
||||
} else {
|
||||
format::redirect("/admin/audio/tracks")
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_audio_file(config: &Config, filename: &str, headers: &HeaderMap) -> Result<Response> {
|
||||
@@ -704,8 +826,7 @@ async fn track_stream(
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let track = track_by_id(&ctx, id).await?;
|
||||
let album = album_by_id(&ctx, track.album_id).await?;
|
||||
if !album.published {
|
||||
if !track.published {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
@@ -724,14 +845,20 @@ pub fn routes() -> Routes {
|
||||
.add("/audio/stream/{filename}", get(raw_audio_stream))
|
||||
.add("/audio/albums", get(public_albums))
|
||||
.add("/audio/albums/{slug}", get(public_album))
|
||||
.add("/audio/tracks", get(public_tracks))
|
||||
.add("/audio/tracks/{id}/stream", get(track_stream))
|
||||
.add("/admin/images", get(admin_images))
|
||||
.add("/admin/images/upload", post(admin_image_upload).layer(DefaultBodyLimit::max(IMAGE_MAX_BYTES + 1024 * 1024)))
|
||||
.add("/admin/audio/albums", get(admin_albums))
|
||||
.add("/admin/audio/albums/create", get(admin_album_new))
|
||||
.add("/admin/audio/albums/create", post(admin_album_create))
|
||||
.add("/admin/audio/tracks", get(admin_tracks))
|
||||
.add("/admin/audio/tracks/upload", get(admin_song_upload_form))
|
||||
.add("/admin/audio/tracks/upload-file", post(admin_song_upload).layer(DefaultBodyLimit::max(AUDIO_MAX_BYTES + 1024 * 1024)))
|
||||
.add("/admin/audio/albums/{album_id}/tracks", get(admin_album_tracks))
|
||||
.add("/admin/audio/albums/{album_id}/tracks/upload", get(admin_track_upload_form))
|
||||
.add("/admin/audio/albums/{album_id}/tracks/upload-file", post(admin_track_upload).layer(DefaultBodyLimit::max(AUDIO_MAX_BYTES + 1024 * 1024)))
|
||||
.add("/admin/audio/tracks/{id}/publish", post(admin_track_publish))
|
||||
.add("/admin/audio/tracks/{id}/unpublish", post(admin_track_unpublish))
|
||||
.add("/admin/audio/tracks/{id}/delete", post(admin_track_delete))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user