Initial commit

This commit is contained in:
Connor Johnstone
2026-03-17 21:56:12 -04:00
commit 50a0ddcdbc
16 changed files with 1110 additions and 0 deletions

147
src/routes/system.rs Normal file
View File

@@ -0,0 +1,147 @@
use actix_web::{web, HttpResponse};
use shanty_db::entities::download_queue::DownloadStatus;
use shanty_db::queries;
use crate::error::ApiError;
use crate::state::AppState;
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("/status").route(web::get().to(get_status)))
.service(web::resource("/index").route(web::post().to(trigger_index)))
.service(web::resource("/tag").route(web::post().to(trigger_tag)))
.service(web::resource("/organize").route(web::post().to(trigger_organize)))
.service(web::resource("/tasks/{id}").route(web::get().to(get_task)))
.service(web::resource("/watchlist").route(web::get().to(list_watchlist)))
.service(web::resource("/watchlist/{id}").route(web::delete().to(remove_watchlist)))
.service(web::resource("/config").route(web::get().to(get_config)));
}
async fn get_status(
state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
let summary = shanty_watch::library_summary(state.db.conn()).await?;
let pending = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Pending))
.await?
.len();
let downloading = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Downloading))
.await?
.len();
let tasks = state.tasks.list();
Ok(HttpResponse::Ok().json(serde_json::json!({
"library": summary,
"queue": {
"pending": pending,
"downloading": downloading,
},
"tasks": tasks,
})))
}
async fn trigger_index(
state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
let task_id = state.tasks.register("index");
let state = state.clone();
let tid = task_id.clone();
tokio::spawn(async move {
let scan_config = shanty_index::ScanConfig {
root: state.config.library_path.clone(),
dry_run: false,
concurrency: 4,
};
match shanty_index::run_scan(state.db.conn(), &scan_config).await {
Ok(stats) => state.tasks.complete(&tid, format!("{stats}")),
Err(e) => state.tasks.fail(&tid, e.to_string()),
}
});
Ok(HttpResponse::Accepted().json(serde_json::json!({ "task_id": task_id })))
}
async fn trigger_tag(
state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
let task_id = state.tasks.register("tag");
let state = state.clone();
let tid = task_id.clone();
tokio::spawn(async move {
let mb = match shanty_tag::MusicBrainzClient::new() {
Ok(c) => c,
Err(e) => {
state.tasks.fail(&tid, e.to_string());
return;
}
};
let tag_config = shanty_tag::TagConfig {
dry_run: false,
write_tags: state.config.tagging.write_tags,
confidence: state.config.tagging.confidence,
};
match shanty_tag::run_tagging(state.db.conn(), &mb, &tag_config, None).await {
Ok(stats) => state.tasks.complete(&tid, format!("{stats}")),
Err(e) => state.tasks.fail(&tid, e.to_string()),
}
});
Ok(HttpResponse::Accepted().json(serde_json::json!({ "task_id": task_id })))
}
async fn trigger_organize(
state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
let task_id = state.tasks.register("organize");
let state = state.clone();
let tid = task_id.clone();
tokio::spawn(async move {
let org_config = shanty_org::OrgConfig {
target_dir: state.config.library_path.clone(),
format: state.config.organization_format.clone(),
dry_run: false,
copy: false,
};
match shanty_org::organize_from_db(state.db.conn(), &org_config).await {
Ok(stats) => state.tasks.complete(&tid, format!("{stats}")),
Err(e) => state.tasks.fail(&tid, e.to_string()),
}
});
Ok(HttpResponse::Accepted().json(serde_json::json!({ "task_id": task_id })))
}
async fn get_task(
state: web::Data<AppState>,
path: web::Path<String>,
) -> Result<HttpResponse, ApiError> {
let id = path.into_inner();
match state.tasks.get(&id) {
Some(task) => Ok(HttpResponse::Ok().json(task)),
None => Err(ApiError::NotFound(format!("task {id}"))),
}
}
async fn list_watchlist(
state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
let items = shanty_watch::list_items(state.db.conn(), None, None).await?;
Ok(HttpResponse::Ok().json(items))
}
async fn remove_watchlist(
state: web::Data<AppState>,
path: web::Path<i32>,
) -> Result<HttpResponse, ApiError> {
let id = path.into_inner();
shanty_watch::remove_item(state.db.conn(), id).await?;
Ok(HttpResponse::NoContent().finish())
}
async fn get_config(
state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
Ok(HttpResponse::Ok().json(&state.config))
}