Get the daemon to actually work
This commit is contained in:
parent
587f82932b
commit
19345e771c
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||
|
14
.idea/workspace.xml
generated
14
.idea/workspace.xml
generated
@ -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>
|
||||
|
8
migrations/20240416175952_drop_useless_columns.sql
Normal file
8
migrations/20240416175952_drop_useless_columns.sql
Normal 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;
|
@ -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,45 +53,58 @@ 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?
|
||||
.recordings;
|
||||
let recordings = listenbrainz::recordings(playlist.tracking_user, range)
|
||||
.await?
|
||||
.recordings;
|
||||
|
||||
let mut tracks = vec![];
|
||||
for recording in recordings {
|
||||
let search_results = media_client
|
||||
.song_search(format!(
|
||||
"{} {}",
|
||||
recording.track_name, recording.artist_name
|
||||
))
|
||||
.await?;
|
||||
let mut tracks = vec![];
|
||||
for recording in recordings {
|
||||
let search_results = media_client
|
||||
.song_search(format!(
|
||||
"{} {}",
|
||||
recording.track_name, recording.artist_name
|
||||
))
|
||||
.await?;
|
||||
|
||||
let filtered = search_results.iter().find(|s| {
|
||||
alpha_compare(&s.title, &recording.track_name)
|
||||
&& alpha_compare(&s.album, &recording.release_name)
|
||||
});
|
||||
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());
|
||||
match filtered {
|
||||
Some(track) => {
|
||||
tracks.push(track.id);
|
||||
}
|
||||
None => {
|
||||
println!(
|
||||
"Couldn't find matching track for {} - {}",
|
||||
recording.track_name, recording.artist_name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!(
|
||||
"Couldn't find matching track for {} - {}",
|
||||
recording.track_name, recording.artist_name
|
||||
|
||||
let playlist = media_client.get_playlist(playlist_id.to_string()).await?;
|
||||
media_client
|
||||
.update_playlist(
|
||||
playlist_id.to_string(),
|
||||
tracks,
|
||||
(0..playlist.song_count).collect(),
|
||||
)
|
||||
}
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(Box::new(UpdatePlaylistError::NoPlaylistId)),
|
||||
}
|
||||
|
||||
media_client
|
||||
.update_playlist(playlist.playlist_id.unwrap().to_string(), vec![], vec![])
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn alpha_compare(a: &str, b: &str) -> bool {
|
||||
|
@ -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>> {
|
||||
|
@ -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
16
src/playlist.rs
Normal 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>,
|
||||
}
|
121
src/subsonic.rs
121
src/subsonic.rs
@ -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>> {
|
||||
let auth_params = self.auth_params();
|
||||
let query_params = [
|
||||
("query", query.as_str()),
|
||||
("artistCount", "0"),
|
||||
("albumCount", "0"),
|
||||
("u", self.username.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", query.as_str()),
|
||||
("artistCount", "0"),
|
||||
("albumCount", "0"),
|
||||
("u", self.username.as_str()),
|
||||
("s", self.salt.as_str()),
|
||||
("t", self.hash.as_str()),
|
||||
("v", "16"),
|
||||
("c", "playlistd"),
|
||||
("f", "json"),
|
||||
])
|
||||
.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(),
|
||||
})
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user