Replace axum with Rocket

Axum is shit and doesn't work
This commit is contained in:
jude 2024-04-18 20:15:07 +01:00
parent ace0f4927a
commit 996504373e
7 changed files with 982 additions and 467 deletions

View File

@ -16,6 +16,10 @@
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Cargo.lock" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.lock" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Cargo.lock" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Cargo.toml" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.toml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Cargo.toml" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/daemon/update_playlists.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/daemon/update_playlists.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/main.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/models.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/models.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/subsonic.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/subsonic.rs" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -52,42 +56,42 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"Cargo.Run.executor": "Run", &quot;Cargo.Run.executor&quot;: &quot;Run&quot;,
"RunOnceActivity.OpenProjectViewOnStart": "true", &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.cidr.known.project.marker": "true", &quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,
"RunOnceActivity.rust.reset.selective.auto.import": "true", &quot;RunOnceActivity.rust.reset.selective.auto.import&quot;: &quot;true&quot;,
"WebServerToolWindowFactoryState": "false", &quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
"cf.first.check.clang-format": "false", &quot;cf.first.check.clang-format&quot;: &quot;false&quot;,
"cidr.known.project.marker": "true", &quot;cidr.known.project.marker&quot;: &quot;true&quot;,
"git-widget-placeholder": "master", &quot;git-widget-placeholder&quot;: &quot;master&quot;,
"last_opened_file_path": "/home/jude/Documents/navidrome-playlists", &quot;last_opened_file_path&quot;: &quot;/home/jude/Documents/navidrome-playlists&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"org.rust.cargo.project.model.PROJECT_DISCOVERY": "true", &quot;org.rust.cargo.project.model.PROJECT_DISCOVERY&quot;: &quot;true&quot;,
"org.rust.cargo.project.model.impl.CargoExternalSystemProjectAware.subscribe.first.balloon": "", &quot;org.rust.cargo.project.model.impl.CargoExternalSystemProjectAware.subscribe.first.balloon&quot;: &quot;&quot;,
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/mod.rs": "true", &quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/mod.rs&quot;: &quot;true&quot;,
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/playlists.rs": "true", &quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/playlists.rs&quot;: &quot;true&quot;,
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/lib.rs": "true", &quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/lib.rs&quot;: &quot;true&quot;,
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/models.rs": "true", &quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/models.rs&quot;: &quot;true&quot;,
"org.rust.first.attach.projects": "true", &quot;org.rust.first.attach.projects&quot;: &quot;true&quot;,
"settings.editor.selected.configurable": "language.rust.cargo.check", &quot;settings.editor.selected.configurable&quot;: &quot;language.rust.cargo.check&quot;,
"vue.rearranger.settings.migration": "true" &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}, },
"keyToStringList": { &quot;keyToStringList&quot;: {
"DatabaseDriversLRU": [ &quot;DatabaseDriversLRU&quot;: [
"postgresql" &quot;postgresql&quot;
], ],
"com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect": [ &quot;com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect&quot;: [
"PostgreSQL" &quot;PostgreSQL&quot;
] ]
} }
}]]></component> }</component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS"> <key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/daemon" /> <recent name="$PROJECT_DIR$/src/daemon" />
@ -154,7 +158,10 @@
<workItem from="1710677603495" duration="13803000" /> <workItem from="1710677603495" duration="13803000" />
<workItem from="1710872266453" duration="225000" /> <workItem from="1710872266453" duration="225000" />
<workItem from="1713283733149" duration="180000" /> <workItem from="1713283733149" duration="180000" />
<workItem from="1713283935337" duration="12890000" /> <workItem from="1713283935337" duration="13390000" />
<workItem from="1713372315382" duration="4924000" />
<workItem from="1713385358427" duration="2705000" />
<workItem from="1713465993328" duration="1489000" />
</task> </task>
<task id="LOCAL-00001" summary="Structure"> <task id="LOCAL-00001" summary="Structure">
<created>1692008860369</created> <created>1692008860369</created>

1166
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ license = "AGPL-3.0 only"
description = "Subsonic playlist daemon" description = "Subsonic playlist daemon"
[dependencies] [dependencies]
axum = { version = "0.7", features = ["json"] } rocket = { version = "0.5.0", features = ["json"] }
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid"] } sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid"] }
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
@ -17,6 +17,7 @@ md5 = "0.7.0"
getrandom = "0.2.12" getrandom = "0.2.12"
thiserror = "1.0.58" thiserror = "1.0.58"
config = "0.14.0" config = "0.14.0"
log = "0.4.21"
[package.metadata.deb] [package.metadata.deb]
depends = "$auto" depends = "$auto"

View File

@ -2,6 +2,7 @@ use crate::listenbrainz::StatsRange;
use crate::models::TrackedPlaylist; use crate::models::TrackedPlaylist;
use crate::subsonic::Subsonic; use crate::subsonic::Subsonic;
use crate::{listenbrainz, CONFIG}; use crate::{listenbrainz, CONFIG};
use log::error;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use std::time::Duration; use std::time::Duration;
use thiserror::Error; use thiserror::Error;
@ -16,7 +17,7 @@ pub enum UpdatePlaylistError {
} }
pub async fn update_playlists_daemon( pub async fn update_playlists_daemon(
media_client: &Subsonic, media_client: impl AsRef<Subsonic> + Clone,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
println!("Playlist daemon starting..."); println!("Playlist daemon starting...");
@ -43,7 +44,8 @@ pub async fn update_playlists_daemon(
match records_res { match records_res {
Ok(records) => { Ok(records) => {
for record in records { for record in records {
update_playlist(media_client, record).await.unwrap(); // todo handle me // todo handle me
update_playlist(media_client.clone(), record).await.unwrap();
} }
} }
@ -57,7 +59,7 @@ pub async fn update_playlists_daemon(
} }
async fn update_playlist( async fn update_playlist(
media_client: &Subsonic, media_client: impl AsRef<Subsonic>,
playlist: TrackedPlaylist, playlist: TrackedPlaylist,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
match playlist.playlist_id { match playlist.playlist_id {
@ -72,6 +74,7 @@ async fn update_playlist(
let mut tracks = vec![]; let mut tracks = vec![];
for recording in recordings { for recording in recordings {
let search_results = media_client let search_results = media_client
.as_ref()
.song_search(format!( .song_search(format!(
"{} {}", "{} {}",
recording.track_name, recording.artist_name recording.track_name, recording.artist_name
@ -91,7 +94,7 @@ async fn update_playlist(
tracks.push(track.id); tracks.push(track.id);
} }
None => { None => {
println!( error!(
"Couldn't find matching track for {} - {}", "Couldn't find matching track for {} - {}",
recording.track_name, recording.artist_name recording.track_name, recording.artist_name
) )
@ -99,8 +102,12 @@ async fn update_playlist(
} }
} }
let playlist = media_client.get_playlist(playlist_id.to_string()).await?; let playlist = media_client
.as_ref()
.get_playlist(playlist_id.to_string())
.await?;
media_client media_client
.as_ref()
.update_playlist( .update_playlist(
playlist_id.to_string(), playlist_id.to_string(),
tracks, tracks,

View File

@ -6,16 +6,16 @@ mod subsonic;
mod track; mod track;
use crate::daemon::update_playlists_daemon; use crate::daemon::update_playlists_daemon;
use crate::models::{PartialTrackedPlaylist, TrackedPlaylist}; use crate::models::{CreateTrackedPlaylist, PartialTrackedPlaylist, TrackedPlaylist};
use crate::subsonic::SubsonicBuilder; use crate::subsonic::{Subsonic, SubsonicBuilder};
use axum::extract::State;
use axum::routing::{get, put};
use axum::{extract, Json, Router};
use config::Config; use config::Config;
use rocket::serde::json::{json, Json};
use rocket::{get, post, routes, serde::json::Value as JsonValue, State};
use serde::Serialize; use serde::Serialize;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use sqlx::Error; use std::str::FromStr;
use std::sync::OnceLock; use std::sync::{Arc, OnceLock};
use uuid::Uuid;
pub static CONFIG: OnceLock<Config> = OnceLock::new(); pub static CONFIG: OnceLock<Config> = OnceLock::new();
@ -41,23 +41,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?; .await?;
sqlx::migrate!().run(&database).await?; sqlx::migrate!().run(&database).await?;
let media_client = SubsonicBuilder::new() let media_client = Arc::new(
.base(CONFIG.get().unwrap().get::<String>("subsonic.base")?) SubsonicBuilder::new()
.username(CONFIG.get().unwrap().get::<String>("subsonic.username")?) .base(CONFIG.get().unwrap().get::<String>("subsonic.base")?)
.password(CONFIG.get().unwrap().get::<String>("subsonic.password")?) .username(CONFIG.get().unwrap().get::<String>("subsonic.username")?)
.build()?; .password(CONFIG.get().unwrap().get::<String>("subsonic.password")?)
.build()?,
);
let _media_client = media_client.clone();
tokio::spawn(async move { tokio::spawn(async move {
update_playlists_daemon(&media_client).await.unwrap(); update_playlists_daemon(_media_client).await.unwrap();
}); });
let app = Router::new() rocket::build()
.route("/playlists", get(all_playlists)) .manage(media_client)
.route("/playlists", put(create_tracking_playlist)) .manage(database)
.with_state(database); .mount(
"/",
let listener = tokio::net::TcpListener::bind("localhost:3000").await?; routes![
axum::serve(listener, app).await?; create_tracking_playlist,
add_tracking_playlist,
all_playlists,
],
)
.launch()
.await?;
Ok(()) Ok(())
} }
@ -67,8 +76,16 @@ struct JsonError {
error: String, error: String,
} }
impl From<Error> for JsonError { impl From<sqlx::Error> for JsonError {
fn from(value: Error) -> Self { fn from(value: sqlx::Error) -> Self {
Self {
error: value.to_string(),
}
}
}
impl From<uuid::Error> for JsonError {
fn from(value: uuid::Error) -> Self {
Self { Self {
error: value.to_string(), error: value.to_string(),
} }
@ -81,19 +98,55 @@ enum Response<T> {
Error(JsonError), Error(JsonError),
} }
#[post("/create", data = "<create_playlist>")]
async fn create_tracking_playlist( async fn create_tracking_playlist(
State(pool): State<PgPool>, media_client: &State<Arc<Subsonic>>,
extract::Json(partial): extract::Json<PartialTrackedPlaylist>, pool: &State<PgPool>,
create_playlist: Json<CreateTrackedPlaylist>,
) -> Json<Response<TrackedPlaylist>> { ) -> Json<Response<TrackedPlaylist>> {
match partial.record(&pool).await { match media_client
.create_playlist(create_playlist.playlist_name.clone())
.await
{
Ok(created_playlist) => match Uuid::from_str(&created_playlist.id) {
Ok(playlist_id) => {
let partial = PartialTrackedPlaylist {
playlist_id,
playlist_size: create_playlist.playlist_size,
tracking_user: create_playlist.tracking_user.clone(),
tracking_type: create_playlist.tracking_type.clone(),
};
match partial.record(pool.inner()).await {
Ok(playlist) => Json(Response::Success(playlist)),
Err(e) => Json(Response::Error(e.into())),
}
}
Err(e) => Json(Response::Error(e.into())),
},
Err(_) => Json(Response::Error(JsonError {
error: "Couldn't create Subsonic playlist".to_string(),
})),
}
}
#[post("/add", data = "<playlist>")]
async fn add_tracking_playlist(
pool: &State<PgPool>,
playlist: Json<PartialTrackedPlaylist>,
) -> Json<Response<TrackedPlaylist>> {
match playlist.record(pool.inner()).await {
Ok(playlist) => Json(Response::Success(playlist)), Ok(playlist) => Json(Response::Success(playlist)),
Err(e) => Json(Response::Error(e.into())), Err(e) => Json(Response::Error(e.into())),
} }
} }
async fn all_playlists(State(pool): State<PgPool>) -> Json<Vec<TrackedPlaylist>> { #[get("/")]
match TrackedPlaylist::all(&pool).await { async fn all_playlists(pool: &State<PgPool>) -> JsonValue {
Ok(playlists) => Json(playlists), match TrackedPlaylist::all(pool.inner()).await {
Err(_) => Json(vec![]), Ok(playlists) => json!(playlists),
Err(_) => json!([]),
} }
} }

View File

@ -4,9 +4,17 @@ use sqlx::types::Uuid as UuidType;
use sqlx::{Executor, Postgres}; use sqlx::{Executor, Postgres};
use uuid::Uuid; use uuid::Uuid;
#[derive(Deserialize)]
pub struct CreateTrackedPlaylist {
pub playlist_name: String,
pub playlist_size: i32,
pub tracking_user: Option<String>,
pub tracking_type: Option<String>,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct PartialTrackedPlaylist { pub struct PartialTrackedPlaylist {
pub playlist_id: Option<Uuid>, pub playlist_id: Uuid,
pub playlist_size: i32, pub playlist_size: i32,
pub tracking_user: Option<String>, pub tracking_user: Option<String>,
pub tracking_type: Option<String>, pub tracking_type: Option<String>,

View File

@ -2,9 +2,11 @@ use crate::playlist::Playlist;
use crate::track::Track; use crate::track::Track;
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone)]
pub struct Subsonic { pub struct Subsonic {
username: String, username: String,
password: String, password: String,
@ -34,11 +36,24 @@ struct GetPlaylistResponse {
playlist: Playlist, playlist: Playlist,
} }
type CreatePlaylistResponse = GetPlaylistResponse;
struct AuthParams { struct AuthParams {
salt: String, salt: String,
hash: String, hash: String,
} }
#[derive(Debug, Error)]
pub enum SubsonicError {
Reqwest(reqwest::Error),
}
impl Display for SubsonicError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Subsonic { impl Subsonic {
fn auth_params(&self) -> AuthParams { fn auth_params(&self) -> AuthParams {
let salt = Uuid::new_v4().simple().to_string(); let salt = Uuid::new_v4().simple().to_string();
@ -50,10 +65,7 @@ impl Subsonic {
AuthParams { salt, hash } AuthParams { salt, hash }
} }
pub async fn song_search( pub async fn song_search(&self, query: String) -> Result<Vec<Track>, SubsonicError> {
&self,
query: String,
) -> Result<Vec<Track>, Box<dyn std::error::Error>> {
let auth_params = self.auth_params(); let auth_params = self.auth_params();
let query_params = [ let query_params = [
("query", query.as_str()), ("query", query.as_str()),
@ -72,18 +84,17 @@ impl Subsonic {
.get(format!("{}/search3", self.base)) .get(format!("{}/search3", self.base))
.query(&query_params) .query(&query_params)
.send() .send()
.await? .await
.map_err(|e| SubsonicError::Reqwest(e))?
.json::<SubsonicResponse<SearchResponse>>() .json::<SubsonicResponse<SearchResponse>>()
.await? .await
.map_err(|e| SubsonicError::Reqwest(e))?
.subsonic_response .subsonic_response
.search_results .search_results
.song) .song)
} }
pub async fn get_playlist( pub async fn get_playlist(&self, playlist_id: String) -> Result<Playlist, SubsonicError> {
&self,
playlist_id: String,
) -> Result<Playlist, Box<dyn std::error::Error>> {
let auth_params = self.auth_params(); let auth_params = self.auth_params();
let get_params = [ let get_params = [
@ -101,9 +112,38 @@ impl Subsonic {
.get(format!("{}/getPlaylist", self.base)) .get(format!("{}/getPlaylist", self.base))
.query(&get_params) .query(&get_params)
.send() .send()
.await? .await
.map_err(|e| SubsonicError::Reqwest(e))?
.json::<SubsonicResponse<GetPlaylistResponse>>() .json::<SubsonicResponse<GetPlaylistResponse>>()
.await? .await
.map_err(|e| SubsonicError::Reqwest(e))?
.subsonic_response
.playlist)
}
pub async fn create_playlist(&self, playlist_name: String) -> Result<Playlist, SubsonicError> {
let auth_params = self.auth_params();
let get_params = [
("u", self.username.to_string()),
("s", auth_params.salt.to_string()),
("t", auth_params.hash.to_string()),
("v", String::from("16")),
("c", String::from("playlistd")),
("f", String::from("json")),
("name", playlist_name),
];
Ok(self
.client
.get(format!("{}/createPlaylist", self.base))
.query(&get_params)
.send()
.await
.map_err(|e| SubsonicError::Reqwest(e))?
.json::<SubsonicResponse<CreatePlaylistResponse>>()
.await
.map_err(|e| SubsonicError::Reqwest(e))?
.subsonic_response .subsonic_response
.playlist) .playlist)
} }
@ -113,7 +153,7 @@ impl Subsonic {
playlist_id: String, playlist_id: String,
add: Vec<String>, add: Vec<String>,
remove: Vec<usize>, remove: Vec<usize>,
) -> Result<Playlist, Box<dyn std::error::Error>> { ) -> Result<Playlist, SubsonicError> {
let auth_params = self.auth_params(); let auth_params = self.auth_params();
let mut update_query = vec![ let mut update_query = vec![
@ -138,7 +178,8 @@ impl Subsonic {
.get(format!("{}/updatePlaylist", self.base)) .get(format!("{}/updatePlaylist", self.base))
.query(&update_query) .query(&update_query)
.send() .send()
.await?; .await
.map_err(|e| SubsonicError::Reqwest(e))?;
self.get_playlist(playlist_id).await self.get_playlist(playlist_id).await
} }