Get the daemon to actually work

This commit is contained in:
jude 2024-04-16 19:10:05 +01:00
parent 587f82932b
commit 19345e771c
9 changed files with 175 additions and 92 deletions

3
.gitignore vendored
View File

@ -1,6 +1,5 @@
/target
/navidrome/target
/listenbrainz/target
.env
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

View File

@ -10,14 +10,18 @@
</component>
<component name="CargoProjects">
<cargoProject FILE="$PROJECT_DIR$/Cargo.toml" />
<cargoProject FILE="$PROJECT_DIR$/navidrome/Cargo.toml" />
</component>
<component name="ChangeListManager">
<list default="true" id="52900e09-9584-4b6c-95ff-fbd4ed5d8b2c" name="Changes" comment="Add interface package. Start adding auth stuff for refreshing tokens.">
<change afterPath="$PROJECT_DIR$/migrations/20240416175952_drop_useless_columns.sql" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/playlist.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/daemon/mod.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/daemon/mod.rs" 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" />
<change beforePath="$PROJECT_DIR$/src/track.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/track.rs" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -77,6 +81,7 @@
"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"
},
@ -152,7 +157,10 @@
<workItem from="1694621211523" duration="3713000" />
<workItem from="1708962769688" duration="6050000" />
<workItem from="1710605458078" duration="1011000" />
<workItem from="1710677603495" duration="13610000" />
<workItem from="1710677603495" duration="13803000" />
<workItem from="1710872266453" duration="225000" />
<workItem from="1713283733149" duration="180000" />
<workItem from="1713283935337" duration="7066000" />
</task>
<task id="LOCAL-00001" summary="Structure">
<created>1692008860369</created>

View File

@ -0,0 +1,8 @@
ALTER TABLE "tracked_playlist"
RENAME COLUMN "rule_id" TO "id";
ALTER TABLE "tracked_playlist"
DROP COLUMN playlist_name;
ALTER TABLE "tracked_playlist"
DROP COLUMN reduce_duplication_on;

View File

@ -2,7 +2,6 @@ use crate::listenbrainz;
use crate::listenbrainz::StatsRange;
use crate::models::TrackedPlaylist;
use crate::subsonic::Subsonic;
use crate::track::Track;
use sqlx::postgres::PgPool;
use std::env;
use std::time::Duration;
@ -13,6 +12,8 @@ use tokio::time::{interval, MissedTickBehavior};
pub enum UpdatePlaylistError {
#[error("Bad range specified")]
BadRange,
#[error("Operation requires a playlist ID")]
NoPlaylistId,
}
pub async fn update_playlists_daemon(
@ -52,8 +53,10 @@ async fn update_playlist(
media_client: &Subsonic,
playlist: TrackedPlaylist,
) -> Result<(), Box<dyn std::error::Error>> {
let range =
StatsRange::from_param(&playlist.tracking_type).ok_or(UpdatePlaylistError::BadRange)?;
match playlist.playlist_id {
Some(playlist_id) => {
let range = StatsRange::from_param(&playlist.tracking_type)
.ok_or(UpdatePlaylistError::BadRange)?;
let recordings = listenbrainz::recordings(playlist.tracking_user, range)
.await?
@ -68,14 +71,17 @@ async fn update_playlist(
))
.await?;
let filtered = search_results.iter().find(|s| {
let filtered = search_results
.iter()
.find(|s| {
alpha_compare(&s.title, &recording.track_name)
&& alpha_compare(&s.album, &recording.release_name)
});
})
.cloned();
match filtered {
Some(track) => {
tracks.push(track.clone());
tracks.push(track.id);
}
None => {
println!(
@ -86,11 +92,19 @@ async fn update_playlist(
}
}
let playlist = media_client.get_playlist(playlist_id.to_string()).await?;
media_client
.update_playlist(playlist.playlist_id.unwrap().to_string(), vec![], vec![])
.update_playlist(
playlist_id.to_string(),
tracks,
(0..playlist.song_count).collect(),
)
.await?;
Ok(())
}
None => Err(Box::new(UpdatePlaylistError::NoPlaylistId)),
}
}
fn alpha_compare(a: &str, b: &str) -> bool {

View File

@ -1,6 +1,7 @@
mod daemon;
mod listenbrainz;
mod models;
mod playlist;
mod subsonic;
mod track;
@ -40,7 +41,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new()
.route("/playlists", get(all_playlists))
.route("/playlists", put(create_playlist))
.route("/playlists", put(create_tracking_playlist))
.with_state(database);
let listener = tokio::net::TcpListener::bind("localhost:3000").await?;
@ -68,7 +69,7 @@ enum Response<T> {
Error(JsonError),
}
async fn create_playlist(
async fn create_tracking_playlist(
State(pool): State<PgPool>,
extract::Json(partial): extract::Json<PartialTrackedPlaylist>,
) -> Json<Response<TrackedPlaylist>> {

View File

@ -6,11 +6,10 @@ use uuid::Uuid;
#[derive(Deserialize)]
pub struct PartialTrackedPlaylist {
pub playlist_name: Option<String>,
pub playlist_id: Option<Uuid>,
pub playlist_size: i32,
pub tracking_user: Option<String>,
pub tracking_type: Option<String>,
pub reduce_duplication_on: Option<String>,
}
impl PartialTrackedPlaylist {
@ -21,15 +20,16 @@ impl PartialTrackedPlaylist {
let uuid = Uuid::new_v4();
sqlx::query!(
r#"INSERT INTO "tracked_playlist"
(rule_id, playlist_name, playlist_size, tracking_user, tracking_type, reduce_duplication_on)
VALUES ($1, $2, $3, $4, $5, $6)"#,
r#"
INSERT INTO "tracked_playlist"
(id, playlist_id, playlist_size, tracking_user, tracking_type)
VALUES ($1, $2, $3, $4, $5)
"#,
uuid,
self.playlist_name,
self.playlist_id,
self.playlist_size,
self.tracking_user,
self.tracking_type,
self.reduce_duplication_on,
)
.execute(pool)
.await?;
@ -40,13 +40,11 @@ impl PartialTrackedPlaylist {
#[derive(Serialize)]
pub struct TrackedPlaylist {
pub rule_id: Uuid,
pub id: Uuid,
pub playlist_id: Option<UuidType>,
pub playlist_name: Option<String>,
pub playlist_size: i32,
pub tracking_user: String,
pub tracking_type: String,
pub reduce_duplication_on: Option<String>,
}
impl TrackedPlaylist {
@ -62,7 +60,7 @@ impl TrackedPlaylist {
) -> Result<Self, Error> {
sqlx::query_as!(
Self,
r#"SELECT * FROM "tracked_playlist" WHERE rule_id = $1"#,
r#"SELECT * FROM "tracked_playlist" WHERE id = $1"#,
uuid
)
.fetch_one(pool)

16
src/playlist.rs Normal file
View File

@ -0,0 +1,16 @@
use crate::track::Track;
use serde::{Deserialize, Serialize};
fn default_vec() -> Vec<Track> {
Vec::new()
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Playlist {
pub id: String,
pub name: String,
#[serde(rename = "songCount")]
pub song_count: usize,
#[serde(default = "default_vec")]
pub entry: Vec<Track>,
}

View File

@ -1,3 +1,4 @@
use crate::playlist::Playlist;
use crate::track::Track;
use reqwest::Client;
use serde::{Deserialize, Serialize};
@ -6,8 +7,7 @@ use uuid::Uuid;
pub struct Subsonic {
username: String,
salt: String,
hash: String,
password: String,
base: String,
client: Client,
}
@ -29,25 +29,48 @@ struct SearchResponseSongs {
song: Vec<Track>,
}
#[derive(Serialize, Deserialize)]
struct GetPlaylistResponse {
playlist: Playlist,
}
struct AuthParams {
salt: String,
hash: String,
}
impl Subsonic {
fn auth_params(&self) -> AuthParams {
let salt = Uuid::new_v4().simple().to_string();
let hash = format!(
"{:?}",
md5::compute(format!("{}{}", self.password, salt).into_bytes())
);
AuthParams { salt, hash }
}
pub async fn song_search(
&self,
query: String,
) -> Result<Vec<Track>, Box<dyn std::error::Error>> {
Ok(self
.client
.get(format!("{}/search3", self.base))
.query(&[
let auth_params = self.auth_params();
let query_params = [
("query", query.as_str()),
("artistCount", "0"),
("albumCount", "0"),
("u", self.username.as_str()),
("s", self.salt.as_str()),
("t", self.hash.as_str()),
("s", auth_params.salt.as_str()),
("t", auth_params.hash.as_str()),
("v", "16"),
("c", "playlistd"),
("f", "json"),
])
];
Ok(self
.client
.get(format!("{}/search3", self.base))
.query(&query_params)
.send()
.await?
.json::<SubsonicResponse<SearchResponse>>()
@ -57,37 +80,67 @@ impl Subsonic {
.song)
}
pub async fn get_playlist(
&self,
playlist_id: String,
) -> Result<Playlist, Box<dyn std::error::Error>> {
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")),
("id", playlist_id),
];
Ok(self
.client
.get(format!("{}/getPlaylist", self.base))
.query(&get_params)
.send()
.await?
.json::<SubsonicResponse<GetPlaylistResponse>>()
.await?
.subsonic_response
.playlist)
}
pub async fn update_playlist(
&self,
playlist_id: String,
add: Vec<String>,
remove: Vec<usize>,
) -> Result<Vec<Track>, Box<dyn std::error::Error>> {
let mut query_params = vec![
) -> Result<Playlist, Box<dyn std::error::Error>> {
let auth_params = self.auth_params();
let mut update_query = vec![
("u", self.username.to_string()),
("s", self.salt.to_string()),
("t", self.hash.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")),
("playlistId", playlist_id.clone()),
];
for id in add {
query_params.push(("songIdToAdd", id));
update_query.push(("songIdToAdd", id));
}
for index in remove {
query_params.push(("songIndexToRemove", index.to_string()));
update_query.push(("songIndexToRemove", index.to_string()));
}
Ok(self
.client
.get(format!("{}/search3", self.base))
.query(&query_params)
self.client
.get(format!("{}/updatePlaylist", self.base))
.query(&update_query)
.send()
.await?
.json::<Vec<Track>>()
.await?)
.await?;
self.get_playlist(playlist_id).await
}
}
@ -127,23 +180,9 @@ impl SubsonicBuilder {
}
pub fn build(self) -> Result<Subsonic, Box<dyn std::error::Error>> {
let salt = Uuid::new_v4().simple().to_string();
let hash = format!(
"{:?}",
md5::compute(
format!(
"{}{}",
self.password.ok_or(SubsonicBuilderError::MissingField)?,
salt
)
.as_bytes()
)
);
Ok(Subsonic {
username: self.username.ok_or(SubsonicBuilderError::MissingField)?,
salt,
hash,
password: self.password.ok_or(SubsonicBuilderError::MissingField)?,
base: self.base.ok_or(SubsonicBuilderError::MissingField)?,
client: Default::default(),
})

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Track {
pub id: String,
pub title: String,