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

73
.idea/workspace.xml generated
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$/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$/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>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -52,42 +56,42 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Cargo.Run.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.cidr.known.project.marker": "true",
"RunOnceActivity.rust.reset.selective.auto.import": "true",
"WebServerToolWindowFactoryState": "false",
"cf.first.check.clang-format": "false",
"cidr.known.project.marker": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "/home/jude/Documents/navidrome-playlists",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"org.rust.cargo.project.model.PROJECT_DISCOVERY": "true",
"org.rust.cargo.project.model.impl.CargoExternalSystemProjectAware.subscribe.first.balloon": "",
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/mod.rs": "true",
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/playlists.rs": "true",
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/lib.rs": "true",
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/models.rs": "true",
"org.rust.first.attach.projects": "true",
"settings.editor.selected.configurable": "language.rust.cargo.check",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Cargo.Run.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;RunOnceActivity.rust.reset.selective.auto.import&quot;: &quot;true&quot;,
&quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
&quot;cf.first.check.clang-format&quot;: &quot;false&quot;,
&quot;cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/jude/Documents/navidrome-playlists&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;org.rust.cargo.project.model.PROJECT_DISCOVERY&quot;: &quot;true&quot;,
&quot;org.rust.cargo.project.model.impl.CargoExternalSystemProjectAware.subscribe.first.balloon&quot;: &quot;&quot;,
&quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/mod.rs&quot;: &quot;true&quot;,
&quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/playlists.rs&quot;: &quot;true&quot;,
&quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/lib.rs&quot;: &quot;true&quot;,
&quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/models.rs&quot;: &quot;true&quot;,
&quot;org.rust.first.attach.projects&quot;: &quot;true&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;language.rust.cargo.check&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
},
"keyToStringList": {
"DatabaseDriversLRU": [
"postgresql"
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;postgresql&quot;
],
"com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect": [
"PostgreSQL"
&quot;com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect&quot;: [
&quot;PostgreSQL&quot;
]
}
}]]></component>
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/daemon" />
@ -154,7 +158,10 @@
<workItem from="1710677603495" duration="13803000" />
<workItem from="1710872266453" duration="225000" />
<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 id="LOCAL-00001" summary="Structure">
<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"
[dependencies]
axum = { version = "0.7", features = ["json"] }
rocket = { version = "0.5.0", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid"] }
reqwest = { version = "0.11", features = ["json"] }
@ -17,6 +17,7 @@ md5 = "0.7.0"
getrandom = "0.2.12"
thiserror = "1.0.58"
config = "0.14.0"
log = "0.4.21"
[package.metadata.deb]
depends = "$auto"

View File

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

View File

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

View File

@ -4,9 +4,17 @@ use sqlx::types::Uuid as UuidType;
use sqlx::{Executor, Postgres};
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)]
pub struct PartialTrackedPlaylist {
pub playlist_id: Option<Uuid>,
pub playlist_id: Uuid,
pub playlist_size: i32,
pub tracking_user: Option<String>,
pub tracking_type: Option<String>,

View File

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