diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..187f63f
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://localhost:5432/postgres
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..ecd7bc0
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/navidrome-playlists.iml b/.idea/navidrome-playlists.iml
new file mode 100644
index 0000000..008063f
--- /dev/null
+++ b/.idea/navidrome-playlists.iml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 41bb99f..12f22b9 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -11,18 +11,20 @@
-
-
-
-
+
+
+
-
-
+
+
+
+
+
@@ -56,12 +58,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
@@ -100,6 +135,8 @@
+
+
1692008860369
@@ -118,4 +155,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 6ee0d7c..fb7463f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -35,6 +35,21 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "async-trait"
version = "0.1.73"
@@ -194,6 +209,21 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "chrono"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-targets",
+]
+
[[package]]
name = "const-oid"
version = "0.9.5"
@@ -670,6 +700,29 @@ dependencies = [
"tokio-native-tls",
]
+[[package]]
+name = "iana-time-zone"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "idna"
version = "0.4.0"
@@ -855,11 +908,22 @@ dependencies = [
"tempfile",
]
+[[package]]
+name = "navidrome"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "chrono",
+ "reqwest",
+ "serde",
+]
+
[[package]]
name = "navidrome-playlists"
version = "0.1.0"
dependencies = [
"axum",
+ "navidrome",
"reqwest",
"serde",
"sqlx",
@@ -2062,6 +2126,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets",
+]
+
[[package]]
name = "windows-sys"
version = "0.48.0"
diff --git a/Cargo.toml b/Cargo.toml
index 33a4320..bcb5c74 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ tokio = { version = "1.0", features = ["full"] }
sqlx = { version = "0.7.1", features = ["runtime-tokio", "postgres", "uuid"] }
reqwest = { version = "0.11.18", features = ["json"] }
serde = { version = "1.0.183", features = ["derive"] }
+navidrome = { path = "navidrome" }
[package.metadata.deb]
depends = "$auto"
diff --git a/navidrome/src/client/builder.rs b/navidrome/src/client/builder.rs
index d558d4c..3512b98 100644
--- a/navidrome/src/client/builder.rs
+++ b/navidrome/src/client/builder.rs
@@ -1,2 +1,59 @@
-struct NavidromeBuilder {
+use crate::client::Navidrome;
+use std::error::Error;
+use std::fmt::{Debug, Display, Formatter};
+
+pub struct NavidromeBuilder {
+ base: Option,
+ token: Option,
+}
+
+#[derive(Debug)]
+pub enum NavidromeBuilderError {
+ MissingParam,
+ Reqwest(reqwest::Error),
+}
+
+impl Display for NavidromeBuilderError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl Error for NavidromeBuilderError {}
+
+impl NavidromeBuilder {
+ pub fn new() -> NavidromeBuilder {
+ return NavidromeBuilder {
+ base: None,
+ token: None,
+ };
+ }
+
+ pub fn base(&mut self, base: impl ToString) -> &mut NavidromeBuilder {
+ self.base = Some(base.to_string());
+ return self;
+ }
+
+ pub fn token(&mut self, token: impl ToString) -> &mut NavidromeBuilder {
+ self.token = Some(token.to_string());
+ return self;
+ }
+
+ pub fn build(&self) -> Result {
+ let client = reqwest::ClientBuilder::new()
+ .build()
+ .map_err(|e| NavidromeBuilderError::Reqwest(e))?;
+
+ return Ok(Navidrome {
+ base: self
+ .base
+ .clone()
+ .ok_or(NavidromeBuilderError::MissingParam)?,
+ http: client,
+ token: self
+ .token
+ .clone()
+ .ok_or(NavidromeBuilderError::MissingParam)?,
+ });
+ }
}
diff --git a/navidrome/src/client/library.rs b/navidrome/src/client/library.rs
new file mode 100644
index 0000000..f0ff030
--- /dev/null
+++ b/navidrome/src/client/library.rs
@@ -0,0 +1,22 @@
+use crate::client::{MediaResult as Result, Navidrome, QualifyPath};
+use crate::models::generic::Playlist;
+use crate::models::navidrome::{NavidromePlaylist, NavidromeTrack};
+use async_trait::async_trait;
+
+#[async_trait]
+pub trait Library {
+ async fn tracks(&self) -> Result>;
+}
+
+#[async_trait]
+impl Library for Navidrome {
+ async fn tracks(&self) -> Result> {
+ self.http
+ .get(self.path("api/song"))
+ .header("X-Nd-Authorization", format!("Bearer {}", self.token))
+ .send()
+ .await?
+ .json::>()
+ .await
+ }
+}
diff --git a/navidrome/src/client/mod.rs b/navidrome/src/client/mod.rs
index a87ca6d..599801a 100644
--- a/navidrome/src/client/mod.rs
+++ b/navidrome/src/client/mod.rs
@@ -1,8 +1,11 @@
+use std::fmt::Display;
+
pub mod builder;
+pub mod library;
pub mod playlists;
trait QualifyPath {
- fn path(&self, path: &str) -> String;
+ fn path(&self, path: impl Display) -> String;
}
pub type MediaResult = Result;
@@ -14,7 +17,7 @@ pub struct Navidrome {
}
impl QualifyPath for Navidrome {
- fn path(&self, path: &str) -> String {
+ fn path(&self, path: impl Display) -> String {
format!("{}/{}", self.base, path)
}
}
diff --git a/navidrome/src/client/playlists.rs b/navidrome/src/client/playlists.rs
index 90f24bd..3fc5326 100644
--- a/navidrome/src/client/playlists.rs
+++ b/navidrome/src/client/playlists.rs
@@ -1,12 +1,13 @@
use crate::client::{MediaResult as Result, Navidrome, QualifyPath};
use crate::models::generic::Playlist;
-use crate::models::navidrome::NavidromePlaylist;
+use crate::models::navidrome::{NavidromePlaylist, NavidromeTrack};
use async_trait::async_trait;
#[async_trait]
pub trait Playlists {
async fn playlists(&self) -> Result>;
async fn playlist(&self, id: String) -> Result;
+ async fn tracks(&self, id: String) -> Result>;
async fn create_playlist(&self) -> Result;
async fn delete_playlist(&self) -> Result<()>;
async fn update_playlists(&self) -> Result<()>;
@@ -26,7 +27,24 @@ impl Playlists for Navidrome {
}
async fn playlist(&self, id: String) -> Result {
- todo!()
+ self.http
+ .get(self.path(format!("api/playlist/{}", id)))
+ .header("X-Nd-Authorization", format!("Bearer {}", self.token))
+ .send()
+ .await?
+ .json::()
+ .await
+ .map(|pv| (&pv).into())
+ }
+
+ async fn tracks(&self, id: String) -> Result> {
+ self.http
+ .get(self.path(format!("api/playlist/{}/tracks", id)))
+ .header("X-Nd-Authorization", format!("Bearer {}", self.token))
+ .send()
+ .await?
+ .json::>()
+ .await
}
async fn create_playlist(&self) -> Result {
diff --git a/navidrome/src/models/navidrome.rs b/navidrome/src/models/navidrome.rs
index 64478fa..ae641da 100644
--- a/navidrome/src/models/navidrome.rs
+++ b/navidrome/src/models/navidrome.rs
@@ -1,7 +1,6 @@
use crate::models::generic::Playlist;
use chrono::NaiveDateTime;
use serde::Deserialize;
-use std::iter::Iterator;
#[derive(Deserialize)]
struct LoginResponse {
@@ -34,8 +33,6 @@ pub(crate) struct NavidromePlaylist {
owner_id: String,
#[serde(rename = "ownerName")]
owner_name: String,
- #[serde(skip)] // todo fix
- rules: Option,
#[serde(rename = "songCount")]
song_count: u64,
sync: bool,
@@ -54,5 +51,20 @@ impl From<&NavidromePlaylist> for Playlist {
}
}
-#[derive(Deserialize)]
-struct NavidromePlaylistRule {}
+#[derive(Deserialize, Clone)]
+pub struct NavidromeTrack {
+ title: String,
+ artist: String,
+ #[serde(rename = "albumArtist")]
+ album_artist: String,
+ album: String,
+ #[serde(rename = "albumId")]
+ album_id: String,
+ #[serde(rename = "artistId")]
+ artist_id: String,
+ id: String,
+ #[serde(rename = "mbzTrackId")]
+ pub musicbrainz_track_id: Option,
+ #[serde(rename = "mbzReleaseTrackId")]
+ musicbrainz_release_track_id: Option,
+}
diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs
new file mode 100644
index 0000000..644aa22
--- /dev/null
+++ b/src/daemon/mod.rs
@@ -0,0 +1,5 @@
+pub mod update_playlists;
+pub mod update_tracks;
+
+pub use update_playlists::update_playlists_daemon;
+pub use update_tracks::update_tracks_daemon;
diff --git a/src/daemon.rs b/src/daemon/update_playlists.rs
similarity index 95%
rename from src/daemon.rs
rename to src/daemon/update_playlists.rs
index b2ec1e2..545fbe8 100644
--- a/src/daemon.rs
+++ b/src/daemon/update_playlists.rs
@@ -5,6 +5,8 @@ use std::time::Duration;
use tokio::time::{interval, MissedTickBehavior};
pub async fn update_playlists_daemon() -> Result<(), Box> {
+ println!("Playlist daemon starting...");
+
let database = PgPool::connect(&env::var("DATABASE_URL").unwrap())
.await
.unwrap();
diff --git a/src/daemon/update_tracks.rs b/src/daemon/update_tracks.rs
new file mode 100644
index 0000000..da5b9cc
--- /dev/null
+++ b/src/daemon/update_tracks.rs
@@ -0,0 +1,33 @@
+use navidrome::{
+ client::{library::Library, Navidrome},
+ models::navidrome::NavidromeTrack,
+};
+
+use std::collections::HashMap;
+use std::sync::Arc;
+use std::time::Duration;
+use tokio::sync::RwLock;
+use tokio::time::{interval, MissedTickBehavior};
+
+pub async fn update_tracks_daemon(
+ media_client: &Navidrome,
+ tracks_map: &mut Arc>>,
+) -> Result<(), Box> {
+ println!("Tracks daemon starting...");
+
+ let mut ticker = interval(Duration::from_secs(30));
+ ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
+
+ loop {
+ let mut tracks_map = tracks_map.write().await;
+ media_client.tracks().await?.iter().for_each(|t| {
+ if let Some(mb_id) = &t.musicbrainz_track_id {
+ tracks_map.insert(mb_id.clone(), t.clone());
+ }
+ });
+
+ println!("Found {} tracks", tracks_map.len());
+
+ ticker.tick().await;
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 63683a0..bf586b0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,20 +2,54 @@ mod daemon;
mod listenbrainz;
mod models;
-use crate::daemon::update_playlists_daemon;
+use navidrome::{
+ client::{builder::NavidromeBuilder, library::Library},
+ models::navidrome::NavidromeTrack,
+};
+
+use crate::daemon::{update_playlists_daemon, update_tracks_daemon};
use crate::listenbrainz::StatsRange;
use axum::extract::State;
use axum::routing::get;
use axum::Router;
use sqlx::postgres::PgPool;
+use std::collections::HashMap;
use std::env;
use std::net::SocketAddr;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+
+#[derive(Clone)]
+struct TrackState {
+ tracks: Arc>>,
+}
#[tokio::main]
async fn main() -> Result<(), Box> {
let database = PgPool::connect(&env::var("DATABASE_URL")?).await?;
sqlx::migrate!().run(&database).await?;
+ let media_client = NavidromeBuilder::new()
+ .base(env::var("NAVIDROME_BASE")?)
+ .token(env::var("NAVIDROME_TOKEN")?)
+ .build()?;
+
+ let mut tracks = Arc::new(RwLock::new(HashMap::new()));
+
+ let state = TrackState {
+ tracks: tracks.clone(),
+ };
+
+ tokio::spawn(async move {
+ update_playlists_daemon().await.unwrap();
+ });
+
+ tokio::spawn(async move {
+ update_tracks_daemon(&media_client, &mut tracks)
+ .await
+ .unwrap();
+ });
+
let app = Router::new().route("/", get(index)).with_state(database);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
@@ -23,10 +57,6 @@ async fn main() -> Result<(), Box> {
.serve(app.into_make_service())
.await?;
- tokio::spawn(async move {
- update_playlists_daemon().await.unwrap();
- });
-
Ok(())
}