Add playlist building daemon
This creates playlists when the name would change
This commit is contained in:
100
src/daemon/create_playlists.rs
Normal file
100
src/daemon/create_playlists.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use crate::models::CreateTrackedPlaylist;
|
||||
use crate::subsonic::Subsonic;
|
||||
use crate::CONFIG;
|
||||
use chrono::Local;
|
||||
use log::error;
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tokio::time::{interval, MissedTickBehavior};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn create_playlists_daemon(
|
||||
media_client: impl AsRef<Subsonic> + Clone,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Create playlist daemon starting...");
|
||||
|
||||
let database = PgPool::connect(
|
||||
CONFIG
|
||||
.get()
|
||||
.unwrap()
|
||||
.get::<String>("database.url")?
|
||||
.as_str(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut ticker = interval(Duration::from_secs(
|
||||
CONFIG.get().unwrap().get::<u64>("daemon.interval")?,
|
||||
));
|
||||
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
loop {
|
||||
let records_res = sqlx::query_as!(
|
||||
CreateTrackedPlaylist,
|
||||
r#"SELECT * FROM "create_tracked_playlist""#
|
||||
)
|
||||
.fetch_all(&database)
|
||||
.await;
|
||||
|
||||
match records_res {
|
||||
Ok(records) => {
|
||||
for record in records {
|
||||
// todo handle me
|
||||
create_playlist(media_client.clone(), record, database.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
println!("Could not fetch create_tracked_playlist: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
ticker.tick().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_playlist(
|
||||
media_client: impl AsRef<Subsonic>,
|
||||
playlist_rule: CreateTrackedPlaylist,
|
||||
database: PgPool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let now = Local::now();
|
||||
let new_name = now.format(&playlist_rule.name_template).to_string();
|
||||
|
||||
if Some(&new_name) != playlist_rule.last_name.as_ref() {
|
||||
// Create playlist
|
||||
let playlist = media_client.as_ref().create_playlist(&new_name).await?;
|
||||
let uuid = Uuid::new_v4();
|
||||
let playlist_uuid = Uuid::from_str(&playlist.id)?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO "tracked_playlist" (id, playlist_id, playlist_size, tracking_user, tracking_type)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
uuid,
|
||||
playlist_uuid,
|
||||
playlist_rule.playlist_size,
|
||||
playlist_rule.tracking_user,
|
||||
playlist_rule.tracking_type,
|
||||
)
|
||||
.execute(&database)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE "create_tracked_playlist" SET last_name = $1 WHERE id = $2
|
||||
"#,
|
||||
new_name,
|
||||
playlist_rule.id,
|
||||
)
|
||||
.execute(&database)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
pub mod create_playlists;
|
||||
pub mod update_playlists;
|
||||
|
||||
pub use create_playlists::create_playlists_daemon;
|
||||
pub use update_playlists::update_playlists_daemon;
|
||||
|
@ -19,7 +19,7 @@ pub enum UpdatePlaylistError {
|
||||
pub async fn update_playlists_daemon(
|
||||
media_client: impl AsRef<Subsonic> + Clone,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Playlist daemon starting...");
|
||||
println!("Track playlist daemon starting...");
|
||||
|
||||
let database = PgPool::connect(
|
||||
CONFIG
|
||||
@ -67,9 +67,10 @@ async fn update_playlist(
|
||||
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, playlist.playlist_size)
|
||||
.await?
|
||||
.recordings;
|
||||
|
||||
let mut tracks = vec![];
|
||||
for recording in recordings {
|
||||
@ -77,7 +78,7 @@ async fn update_playlist(
|
||||
.as_ref()
|
||||
.song_search(format!(
|
||||
"{} {}",
|
||||
recording.track_name, recording.artist_name
|
||||
recording.track_name, recording.artists[0].artist_credit_name
|
||||
))
|
||||
.await?;
|
||||
|
||||
|
@ -82,11 +82,17 @@ pub struct RecordingsPayload {
|
||||
pub recordings: Vec<RecordingsEntry>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Artist {
|
||||
pub artist_credit_name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RecordingsEntry {
|
||||
pub track_name: String,
|
||||
pub release_name: String,
|
||||
pub artist_name: String,
|
||||
pub artists: Vec<Artist>,
|
||||
pub recording_mbid: String,
|
||||
pub release_mbid: String,
|
||||
}
|
||||
@ -94,13 +100,15 @@ pub struct RecordingsEntry {
|
||||
pub async fn recordings(
|
||||
user: impl Display,
|
||||
range: StatsRange,
|
||||
count: i32,
|
||||
) -> Result<RecordingsPayload, reqwest::Error> {
|
||||
let url = format!(
|
||||
"{}/stats/user/{}/{}?range={}",
|
||||
"{}/stats/user/{}/{}?range={}&count={}",
|
||||
BASE,
|
||||
user,
|
||||
StatsType::Recordings.stub(),
|
||||
range.param()
|
||||
range.param(),
|
||||
count
|
||||
);
|
||||
|
||||
Ok(reqwest::get(url)
|
||||
|
39
src/main.rs
39
src/main.rs
@ -5,13 +5,13 @@ mod playlist;
|
||||
mod subsonic;
|
||||
mod track;
|
||||
|
||||
use crate::daemon::update_playlists_daemon;
|
||||
use crate::models::{CreateTrackedPlaylist, PartialTrackedPlaylist, TrackedPlaylist};
|
||||
use crate::daemon::{create_playlists_daemon, update_playlists_daemon};
|
||||
use crate::models::{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 serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
@ -49,16 +49,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.build()?,
|
||||
);
|
||||
|
||||
let _media_client = media_client.clone();
|
||||
tokio::spawn(async move {
|
||||
update_playlists_daemon(_media_client).await.unwrap();
|
||||
});
|
||||
{
|
||||
let _media_client = media_client.clone();
|
||||
tokio::spawn(async move {
|
||||
update_playlists_daemon(_media_client).await.unwrap();
|
||||
});
|
||||
}
|
||||
{
|
||||
let _media_client = media_client.clone();
|
||||
tokio::spawn(async move {
|
||||
create_playlists_daemon(_media_client).await.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
rocket::build()
|
||||
.manage(media_client)
|
||||
.manage(database)
|
||||
.mount(
|
||||
"/",
|
||||
"/api/playlist",
|
||||
routes![
|
||||
create_tracking_playlist,
|
||||
add_tracking_playlist,
|
||||
@ -98,11 +106,20 @@ enum Response<T> {
|
||||
Error(JsonError),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreatePlaylist {
|
||||
pub playlist_name: String,
|
||||
pub playlist_size: i32,
|
||||
pub tracking_user: Option<String>,
|
||||
pub tracking_type: Option<String>,
|
||||
}
|
||||
|
||||
/// Create a new playlist and attach a tracker
|
||||
#[post("/create", data = "<create_playlist>")]
|
||||
async fn create_tracking_playlist(
|
||||
media_client: &State<Arc<Subsonic>>,
|
||||
pool: &State<PgPool>,
|
||||
create_playlist: Json<CreateTrackedPlaylist>,
|
||||
create_playlist: Json<CreatePlaylist>,
|
||||
) -> Json<Response<TrackedPlaylist>> {
|
||||
match media_client
|
||||
.create_playlist(create_playlist.playlist_name.clone())
|
||||
@ -132,7 +149,8 @@ async fn create_tracking_playlist(
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/add", data = "<playlist>")]
|
||||
/// Add tracking to an existing playlist
|
||||
#[post("/track", data = "<playlist>")]
|
||||
async fn add_tracking_playlist(
|
||||
pool: &State<PgPool>,
|
||||
playlist: Json<PartialTrackedPlaylist>,
|
||||
@ -143,6 +161,7 @@ async fn add_tracking_playlist(
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all tracked playlists
|
||||
#[get("/")]
|
||||
async fn all_playlists(pool: &State<PgPool>) -> JsonValue {
|
||||
match TrackedPlaylist::all(pool.inner()).await {
|
||||
|
@ -4,14 +4,6 @@ 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: Uuid,
|
||||
@ -75,3 +67,34 @@ impl TrackedPlaylist {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CreateTrackedPlaylist {
|
||||
pub id: Uuid,
|
||||
pub name_template: String,
|
||||
pub last_name: Option<String>,
|
||||
pub playlist_size: i32,
|
||||
pub tracking_user: String,
|
||||
pub tracking_type: String,
|
||||
}
|
||||
|
||||
impl CreateTrackedPlaylist {
|
||||
pub async fn all(pool: impl Executor<'_, Database = Postgres>) -> Result<Vec<Self>, Error> {
|
||||
sqlx::query_as!(Self, r#"SELECT * FROM "create_tracked_playlist""#)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn rule(
|
||||
pool: impl Executor<'_, Database = Postgres>,
|
||||
uuid: Uuid,
|
||||
) -> Result<Self, Error> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
r#"SELECT * FROM "create_tracked_playlist" WHERE id = $1"#,
|
||||
uuid
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,10 @@ use std::fmt::{Display, Formatter};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn default_vec() -> Vec<Track> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Subsonic {
|
||||
username: String,
|
||||
@ -28,6 +32,7 @@ struct SearchResponse {
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SearchResponseSongs {
|
||||
#[serde(default = "default_vec")]
|
||||
song: Vec<Track>,
|
||||
}
|
||||
|
||||
@ -121,7 +126,10 @@ impl Subsonic {
|
||||
.playlist)
|
||||
}
|
||||
|
||||
pub async fn create_playlist(&self, playlist_name: String) -> Result<Playlist, SubsonicError> {
|
||||
pub async fn create_playlist(
|
||||
&self,
|
||||
playlist_name: impl ToString,
|
||||
) -> Result<Playlist, SubsonicError> {
|
||||
let auth_params = self.auth_params();
|
||||
|
||||
let get_params = [
|
||||
@ -131,7 +139,7 @@ impl Subsonic {
|
||||
("v", String::from("16")),
|
||||
("c", String::from("playlistd")),
|
||||
("f", String::from("json")),
|
||||
("name", playlist_name),
|
||||
("name", playlist_name.to_string()),
|
||||
];
|
||||
|
||||
Ok(self
|
||||
|
Reference in New Issue
Block a user