use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use std::sync::Arc;
use vibelang_core::{
traits::SequenceConfig, types::Beat, Clip, MelodyId, PatternId, SequenceId, SequenceMessage,
};
use crate::{
models::{
ErrorResponse, Sequence, SequenceClip, SequenceCreate, SequenceStartRequest, SequenceUpdate,
},
AppState,
};
async fn resolve_sequence_id(
state: &Arc<AppState>,
identifier: &str,
) -> Result<SequenceId, (StatusCode, Json<ErrorResponse>)> {
if let Ok(num_id) = identifier.parse::<u32>() {
let sequence_id = SequenceId::new(num_id);
let exists = state
.with_state(|s| s.sequences.contains_key(&sequence_id))
.await;
if exists {
return Ok(sequence_id);
}
}
let found = state
.with_state(|s| {
s.sequences
.iter()
.find(|(_, ss)| ss.config.name == identifier)
.map(|(id, _)| *id)
})
.await;
match found {
Some(id) => Ok(id),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse::not_found(&format!(
"Sequence '{}' not found",
identifier
))),
)),
}
}
fn clip_to_api(clip: &Clip) -> SequenceClip {
match clip {
Clip::Pattern { id, start, end } => SequenceClip {
clip_type: "pattern".to_string(),
name: id.raw().to_string(),
start_beat: start.to_f64(),
end_beat: Some(end.to_f64()),
duration_beats: Some((end.to_f64() - start.to_f64()).max(0.0)),
once: None,
},
Clip::Melody { id, start, end } => SequenceClip {
clip_type: "melody".to_string(),
name: id.raw().to_string(),
start_beat: start.to_f64(),
end_beat: Some(end.to_f64()),
duration_beats: Some((end.to_f64() - start.to_f64()).max(0.0)),
once: None,
},
Clip::Fade { start, .. } => SequenceClip {
clip_type: "fade".to_string(),
name: String::new(),
start_beat: start.to_f64(),
end_beat: None,
duration_beats: None,
once: Some(true),
},
Clip::Sequence { id, start } => SequenceClip {
clip_type: "sequence".to_string(),
name: id.raw().to_string(),
start_beat: start.to_f64(),
end_beat: None,
duration_beats: None,
once: None,
},
}
}
fn sequence_to_api(_id: &SequenceId, state: &vibelang_core::SequenceState) -> Sequence {
Sequence {
name: state.config.name.clone(),
loop_beats: state.config.length.to_f64(),
clips: state.config.clips.iter().map(clip_to_api).collect(),
play_once: Some(!state.looping),
active: Some(state.playing),
source_location: None,
}
}
pub async fn list_sequences(State(state): State<Arc<AppState>>) -> Json<Vec<Sequence>> {
let sequences = state
.with_state(|s| {
s.sequences
.iter()
.map(|(id, ss)| sequence_to_api(id, ss))
.collect::<Vec<_>>()
})
.await;
Json(sequences)
}
pub async fn get_sequence(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Sequence>, (StatusCode, Json<ErrorResponse>)> {
let sequence_id = resolve_sequence_id(&state, &id).await?;
let sequence = state
.with_state(|s| {
s.sequences
.get(&sequence_id)
.map(|ss| sequence_to_api(&sequence_id, ss))
})
.await;
match sequence {
Some(seq) => Ok(Json(seq)),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse::not_found(&format!(
"Sequence '{}' not found",
id
))),
)),
}
}
pub async fn update_sequence(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(update): Json<SequenceUpdate>,
) -> Result<Json<Sequence>, (StatusCode, Json<ErrorResponse>)> {
let _sequence_id = resolve_sequence_id(&state, &id).await?;
if update.clips.is_some() || update.loop_beats.is_some() {
tracing::warn!(
"Sequence {} update requested clips or loop_beats change, which requires script reload",
id
);
}
get_sequence(State(state), Path(id)).await
}
pub async fn start_sequence(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(req): Json<Option<SequenceStartRequest>>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let sequence_id = resolve_sequence_id(&state, &id).await?;
let looping = !req.map(|r| r.play_once).unwrap_or(false);
if let Err(e) = state
.send(
SequenceMessage::Start {
id: sequence_id,
looping,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to start sequence: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn stop_sequence(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let sequence_id = resolve_sequence_id(&state, &id).await?;
if let Err(e) = state
.send(SequenceMessage::Stop { id: sequence_id }.into())
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to stop sequence: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn pause_sequence(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let sequence_id = resolve_sequence_id(&state, &id).await?;
if let Err(e) = state
.send(SequenceMessage::Pause { id: sequence_id }.into())
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to pause sequence: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
fn api_clip_to_clip(clip: &SequenceClip) -> Option<Clip> {
let start = Beat::from_f64(clip.start_beat);
let end = clip
.end_beat
.or(clip.duration_beats.map(|d| clip.start_beat + d))
.map(Beat::from_f64)
.unwrap_or_else(|| Beat::from_f64(clip.start_beat + 4.0));
match clip.clip_type.to_lowercase().as_str() {
"pattern" => {
let id = clip.name.parse::<u32>().ok().map(PatternId::new)?;
Some(Clip::Pattern { id, start, end })
}
"melody" => {
let id = clip.name.parse::<u32>().ok().map(MelodyId::new)?;
Some(Clip::Melody { id, start, end })
}
"sequence" => {
let id = clip.name.parse::<u32>().ok().map(SequenceId::new)?;
Some(Clip::Sequence { id, start })
}
_ => None,
}
}
pub async fn create_sequence(
State(state): State<Arc<AppState>>,
Json(req): Json<SequenceCreate>,
) -> Result<(StatusCode, Json<Sequence>), (StatusCode, Json<ErrorResponse>)> {
let sequence_id = state
.with_state(|s| {
let id = req
.name
.bytes()
.fold(1u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
let mut id = id % 10000 + 1;
while s.sequences.contains_key(&SequenceId::new(id)) {
id += 1;
}
SequenceId::new(id)
})
.await;
let clips: Vec<Clip> = req.clips.iter().filter_map(api_clip_to_clip).collect();
let config = SequenceConfig {
name: req.name.clone(),
length: Beat::from_f64(req.loop_beats),
clips,
};
if let Err(e) = state
.send(
SequenceMessage::Create {
id: sequence_id,
config,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to create sequence: {}",
e
))),
));
}
let sequence = state
.with_state(|s| {
s.sequences
.get(&sequence_id)
.map(|ss| sequence_to_api(&sequence_id, ss))
})
.await;
match sequence {
Some(seq) => Ok((StatusCode::CREATED, Json(seq))),
None => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(
"Sequence created but not found in state",
)),
)),
}
}
pub async fn delete_sequence(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let sequence_id = resolve_sequence_id(&state, &id).await?;
if let Err(e) = state
.send(SequenceMessage::Delete { id: sequence_id }.into())
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to delete sequence: {}",
e
))),
));
}
Ok(StatusCode::NO_CONTENT)
}