Track media server songs list

This commit is contained in:
jude 2023-09-09 20:03:01 +01:00
parent d217dd0b81
commit 9c689d73d6
16 changed files with 361 additions and 25 deletions

12
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="postgres@localhost" uuid="84798454-83c1-4f0a-870a-8e649a07b493">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/navidrome-playlists.iml" filepath="$PROJECT_DIR$/.idea/navidrome-playlists.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="CPP_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/navidrome/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/navidrome/target" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -11,18 +11,20 @@
<component name="CargoProjects">
<cargoProject FILE="$PROJECT_DIR$/Cargo.toml" />
<cargoProject FILE="$PROJECT_DIR$/navidrome/Cargo.toml" />
<cargoProject FILE="$PROJECT_DIR$/listenbrainz/Cargo.toml" />
</component>
<component name="ChangeListManager">
<list default="true" id="52900e09-9584-4b6c-95ff-fbd4ed5d8b2c" name="Changes" comment="Structure">
<change afterPath="$PROJECT_DIR$/src/daemon.rs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/listenbrainz.rs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/models.rs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/navidrome/src/client/library.rs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/daemon/mod.rs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/daemon/update_tracks.rs" 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.toml" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/listenbrainz/Cargo.toml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/listenbrainz/src/lib.rs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/navidrome/src/client/builder.rs" beforeDir="false" afterPath="$PROJECT_DIR$/navidrome/src/client/builder.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/navidrome/src/client/mod.rs" beforeDir="false" afterPath="$PROJECT_DIR$/navidrome/src/client/mod.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/navidrome/src/client/playlists.rs" beforeDir="false" afterPath="$PROJECT_DIR$/navidrome/src/client/playlists.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/navidrome/src/models/navidrome.rs" beforeDir="false" afterPath="$PROJECT_DIR$/navidrome/src/models/navidrome.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/daemon.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" />
</list>
<option name="SHOW_DIALOG" value="false" />
@ -56,12 +58,13 @@
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.cidr.known.project.marker": "true",
"WebServerToolWindowFactoryState": "false",
"cf.first.check.clang-format": "false",
"cidr.known.project.marker": "true",
"last_opened_file_path": "/home/jude/listenbrainz-rs",
"last_opened_file_path": "/home/jude/Documents/navidrome-playlists/src/daemon",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
@ -72,18 +75,50 @@
"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",
"settings.editor.selected.configurable": "language.rust.rustfmt",
"settings.editor.selected.configurable": "language.rust.cargo.check",
"vue.rearranger.settings.migration": "true"
},
"keyToStringList": {
"DatabaseDriversLRU": [
"postgresql"
]
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/daemon" />
<recent name="$PROJECT_DIR$/navidrome/src/client" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/daemon" />
<recent name="$PROJECT_DIR$/navidrome/src/models" />
</key>
</component>
<component name="RunManager">
<configuration name="Run" type="CargoCommandRunConfiguration" factoryName="Cargo Command" nameIsGenerated="true">
<option name="command" value="run" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="emulateTerminal" value="false" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs>
<env name="DATABASE_URL" value="postgres:///navidrome-playlists" />
<env name="NAVIDROME_BASE" value="https://navidrome.jellypro.xyz" />
<env name="NAVIDROME_TOKEN" value="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG0iOnRydWUsImV4cCI6MTY5NDM2MjkyNywiaWF0IjoxNjk0MjcyNzE1LCJpc3MiOiJORCIsInN1YiI6Imp1ZGUiLCJ1aWQiOiIyZjljNTFiMi03MThmLTRiZmYtYjhkYi03MzE0ODdmZmFhYmIifQ.XNpfkjJI7wXq4EzGM-s7dQAXyCBAyN4Dmy4frbRMXPU" />
</envs>
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
<component name="RustProjectSettings">
<option name="toolchainHomeDirectory" value="$PROJECT_DIR$/../.cargo/bin" />
<option name="version" value="2" />
<option name="toolchainHomeDirectory" value="$USER_HOME$/.cargo/bin" />
</component>
<component name="RustfmtProjectSettings">
<option name="runRustfmtOnSave" value="true" />
@ -100,6 +135,8 @@
<workItem from="1691949248703" duration="4525000" />
<workItem from="1692003763542" duration="5103000" />
<workItem from="1692042976149" duration="10055000" />
<workItem from="1694271920428" duration="9295000" />
<workItem from="1694285959491" duration="137000" />
</task>
<task id="LOCAL-00001" summary="Structure">
<created>1692008860369</created>
@ -118,4 +155,8 @@
<MESSAGE value="Structure" />
<option name="LAST_COMMIT_MESSAGE" value="Structure" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
</project>

73
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<String>,
token: Option<String>,
}
#[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<Navidrome, NavidromeBuilderError> {
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)?,
});
}
}

View File

@ -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<Vec<NavidromeTrack>>;
}
#[async_trait]
impl Library for Navidrome {
async fn tracks(&self) -> Result<Vec<NavidromeTrack>> {
self.http
.get(self.path("api/song"))
.header("X-Nd-Authorization", format!("Bearer {}", self.token))
.send()
.await?
.json::<Vec<NavidromeTrack>>()
.await
}
}

View File

@ -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<U> = Result<U, reqwest::Error>;
@ -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)
}
}

View File

@ -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<Vec<Playlist>>;
async fn playlist(&self, id: String) -> Result<Playlist>;
async fn tracks(&self, id: String) -> Result<Vec<NavidromeTrack>>;
async fn create_playlist(&self) -> Result<Playlist>;
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<Playlist> {
todo!()
self.http
.get(self.path(format!("api/playlist/{}", id)))
.header("X-Nd-Authorization", format!("Bearer {}", self.token))
.send()
.await?
.json::<NavidromePlaylist>()
.await
.map(|pv| (&pv).into())
}
async fn tracks(&self, id: String) -> Result<Vec<NavidromeTrack>> {
self.http
.get(self.path(format!("api/playlist/{}/tracks", id)))
.header("X-Nd-Authorization", format!("Bearer {}", self.token))
.send()
.await?
.json::<Vec<NavidromeTrack>>()
.await
}
async fn create_playlist(&self) -> Result<Playlist> {

View File

@ -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<NavidromePlaylistRule>,
#[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<String>,
#[serde(rename = "mbzReleaseTrackId")]
musicbrainz_release_track_id: Option<String>,
}

5
src/daemon/mod.rs Normal file
View File

@ -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;

View File

@ -5,6 +5,8 @@ use std::time::Duration;
use tokio::time::{interval, MissedTickBehavior};
pub async fn update_playlists_daemon() -> Result<(), Box<dyn std::error::Error>> {
println!("Playlist daemon starting...");
let database = PgPool::connect(&env::var("DATABASE_URL").unwrap())
.await
.unwrap();

View File

@ -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<RwLock<HashMap<String, NavidromeTrack>>>,
) -> Result<(), Box<dyn std::error::Error>> {
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;
}
}

View File

@ -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<RwLock<HashMap<String, NavidromeTrack>>>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
.serve(app.into_make_service())
.await?;
tokio::spawn(async move {
update_playlists_daemon().await.unwrap();
});
Ok(())
}