Replace axum with Rocket
Axum is shit and doesn't work
This commit is contained in:
parent
ace0f4927a
commit
996504373e
@ -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">{
|
||||
"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"
|
||||
},
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
],
|
||||
"com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect": [
|
||||
"PostgreSQL"
|
||||
"com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect": [
|
||||
"PostgreSQL"
|
||||
]
|
||||
}
|
||||
}]]></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
1166
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
@ -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,
|
||||
|
111
src/main.rs
111
src/main.rs
@ -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!([]),
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user