diff --git a/crates/playlists/src/core.rs b/crates/playlists/src/core.rs index 5cbbd921..64d9cff1 100644 --- a/crates/playlists/src/core.rs +++ b/crates/playlists/src/core.rs @@ -4,6 +4,7 @@ use anyhow::Error; use duckdb::{params, Connection}; use owo_colors::OwoColorize; use reqwest::Client; +use serde_json::json; use sha2::Digest; use sqlx::{Pool, Postgres}; @@ -152,7 +153,7 @@ pub async fn find_spotify_users( Ok(user_tokens) } -pub async fn save_playlists(pool: &Pool, conn: Arc>, playlists: Vec, user_id: &str, did: &str) -> Result<(), Error> { +pub async fn save_playlists(pool: &Pool, conn: Arc>, nc: Arc>, playlists: Vec, user_id: &str, did: &str) -> Result<(), Error> { let token = generate_token(did)?; for playlist in playlists { println!("Saving playlist: {} - {} tracks", playlist.name.bright_green(), playlist.tracks.total); @@ -182,8 +183,20 @@ pub async fn save_playlists(pool: &Pool, conn: Arc>, let new_playlist = new_playlist.first().unwrap(); + let nc = nc.lock().unwrap(); + nc.publish("rocksky.playlist", + serde_json::to_string(&json!({ + "id": new_playlist.xata_id.clone(), + "did": did, + }) + ).unwrap().into() + ).await?; + drop(nc); + + let mut i = 1; for track in playlist.tracks.items.unwrap_or_default() { - println!("Saving track: {}", track.track.name.bright_green()); + println!("Saving track: {} - {}/{}", track.track.name.bright_green(), i, playlist.tracks.total); + i += 1; match save_track(track.track, &token).await? { Some(track) => { println!("Saved track: {}", track.xata_id.bright_green()); diff --git a/crates/playlists/src/main.rs b/crates/playlists/src/main.rs index 8006aa3c..e054bea2 100644 --- a/crates/playlists/src/main.rs +++ b/crates/playlists/src/main.rs @@ -2,8 +2,10 @@ use core::{create_tables, find_spotify_users, load_users, save_playlists}; use std::{env, sync::{Arc, Mutex}}; use anyhow::Error; +use async_nats::connect; use dotenv::dotenv; use duckdb::Connection; +use owo_colors::OwoColorize; use playlists::subscriber::subscribe; use spotify::get_user_playlists; use sqlx::postgres::PgPoolOptions; @@ -38,12 +40,17 @@ async fn main() -> Result<(), Error> { .await?; let conn = conn.clone(); + let addr = env::var("NATS_URL").unwrap_or_else(|_| "nats://localhost:4222".to_string()); + let nc = connect(&addr).await?; + let nc = Arc::new(Mutex::new(nc)); + println!("Connected to NATS server at {}", addr.bright_green()); + for user in users { let token = user.1.clone(); let did = user.2.clone(); let user_id = user.3.clone(); let playlists = get_user_playlists(token).await?; - save_playlists(&pool, conn.clone(), playlists, &user_id, &did).await?; + save_playlists(&pool, conn.clone(), nc.clone(),playlists, &user_id, &did).await?; } println!("Done!"); diff --git a/crates/playlists/src/types/playlist.rs b/crates/playlists/src/types/playlist.rs index b9472c33..ff6d7d2c 100644 --- a/crates/playlists/src/types/playlist.rs +++ b/crates/playlists/src/types/playlist.rs @@ -11,7 +11,7 @@ pub struct SpotifyResponse { pub items: Vec, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Playlist { pub collaborative: bool, pub description: String, @@ -29,19 +29,19 @@ pub struct Playlist { pub uri: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ExternalUrls { pub spotify: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Image { pub height: u32, pub url: String, pub width: u32, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Owner { pub display_name: Option, pub external_urls: ExternalUrls, @@ -51,7 +51,7 @@ pub struct Owner { pub uri: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Tracks { pub href: String, pub limit: Option, @@ -62,7 +62,7 @@ pub struct Tracks { pub items: Option>, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct TrackItem { pub added_at: String, pub added_by: Owner, @@ -70,7 +70,7 @@ pub struct TrackItem { pub track: Track, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Track { pub album: Album, pub artists: Vec, @@ -94,7 +94,7 @@ pub struct Track { pub is_local: bool, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Album { pub album_type: String, pub total_tracks: u32, @@ -112,7 +112,7 @@ pub struct Album { pub artists: Vec, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Artist { pub external_urls: ExternalUrls, pub href: String, @@ -122,17 +122,17 @@ pub struct Artist { pub uri: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ExternalIds { pub isrc: String, pub ean: Option, pub upc: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct LinkedFrom {} -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Restrictions { pub reason: String, } diff --git a/rockskyapi/rocksky-auth/.xata/migrations/.ledger b/rockskyapi/rocksky-auth/.xata/migrations/.ledger index 66e7fefd..07ccddda 100644 --- a/rockskyapi/rocksky-auth/.xata/migrations/.ledger +++ b/rockskyapi/rocksky-auth/.xata/migrations/.ledger @@ -163,3 +163,22 @@ mig_cuq27fufvuvgoi6asfog mig_cuq2866fvuvgoi6asfpg mig_cuq28kefvuvgoi6asfqg mig_cusvclmfvuvgoi6atb2g +mig_cuvv6k1dsuem1m1rk3l0 +mig_cuvvadtv5omnnkb7cs40 +mig_cv3tgjqtb4rhvnj37il0 +mig_cv3tib2tb4rhvnj37io0 +mig_cv3tipd2t0po8jv9t7u0 +mig_cv3tj3itb4rhvnj37ip0 +mig_cv55loqtb4rhvnj37rng +mig_cv55m5o66mqp0v06gpbg +mig_cv55mat2t0po8jv9tkgg +mig_cv55mi066mqp0v06gpcg +mig_cv55mql2t0po8jv9tkhg +mig_cv55n3066mqp0v06gpeg +mig_cv55n7d2t0po8jv9tkig +mig_cv55nf52t0po8jv9tkjg +mig_cv55njg66mqp0v06gpfg +mig_cv55nu2tb4rhvnj37rog +mig_cv55o3d2t0po8jv9tkkg +mig_cv55o952t0po8jv9tklg +sql_7fbe41bfacc536 diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cuvv6k1dsuem1m1rk3l0.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cuvv6k1dsuem1m1rk3l0.json new file mode 100644 index 00000000..a81ac569 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cuvv6k1dsuem1m1rk3l0.json @@ -0,0 +1,30 @@ +{ + "done": true, + "migration": { + "name": "mig_cuvv6k1dsuem1m1rk3l0", + "operations": [ + { + "add_column": { + "up": "''", + "table": "playlists", + "column": { + "name": "created_by", + "type": "text", + "comment": "{\"xata.link\":\"users\"}", + "references": { + "name": "created_by_link", + "table": "users", + "column": "xata_id", + "on_delete": "SET NULL" + } + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cuvv6k1dsuem1m1rk3l0", + "parent": "mig_cusvclmfvuvgoi6atb2g", + "schema": "public", + "startedAt": "2025-02-27T05:08:32.597847Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cuvvadtv5omnnkb7cs40.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cuvvadtv5omnnkb7cs40.json new file mode 100644 index 00000000..91a7979f --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cuvvadtv5omnnkb7cs40.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cuvvadtv5omnnkb7cs40", + "operations": [ + { + "add_column": { + "table": "playlists", + "column": { + "name": "picture", + "type": "text", + "comment": "", + "nullable": true + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cuvvadtv5omnnkb7cs40", + "parent": "mig_cuvv6k1dsuem1m1rk3l0", + "schema": "public", + "startedAt": "2025-02-27T05:16:39.402596Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tgjqtb4rhvnj37il0.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tgjqtb4rhvnj37il0.json new file mode 100644 index 00000000..47ee2360 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tgjqtb4rhvnj37il0.json @@ -0,0 +1,25 @@ +{ + "done": true, + "migration": { + "name": "mig_cv3tgjqtb4rhvnj37il0", + "operations": [ + { + "add_column": { + "table": "playlists", + "column": { + "name": "spotify_link", + "type": "text", + "unique": true, + "comment": "", + "nullable": true + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv3tgjqtb4rhvnj37il0", + "parent": "mig_cuvvadtv5omnnkb7cs40", + "schema": "public", + "startedAt": "2025-03-05T04:51:28.013325Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tib2tb4rhvnj37io0.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tib2tb4rhvnj37io0.json new file mode 100644 index 00000000..0fcdb31f --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tib2tb4rhvnj37io0.json @@ -0,0 +1,21 @@ +{ + "done": true, + "migration": { + "name": "mig_cv3tib2tb4rhvnj37io0", + "operations": [ + { + "drop_constraint": { + "up": "\"spotify_link\"", + "down": "\"spotify_link\"", + "name": "playlists__pgroll_new_spotify_link_key", + "table": "playlists" + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv3tib2tb4rhvnj37io0", + "parent": "mig_cv3tgjqtb4rhvnj37il0", + "schema": "public", + "startedAt": "2025-03-05T04:55:09.2563Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tipd2t0po8jv9t7u0.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tipd2t0po8jv9t7u0.json new file mode 100644 index 00000000..9f742ec6 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tipd2t0po8jv9t7u0.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv3tipd2t0po8jv9t7u0", + "operations": [ + { + "add_column": { + "table": "playlists", + "column": { + "name": "tidal_link", + "type": "text", + "comment": "", + "nullable": true + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv3tipd2t0po8jv9t7u0", + "parent": "mig_cv3tib2tb4rhvnj37io0", + "schema": "public", + "startedAt": "2025-03-05T04:56:06.432882Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tj3itb4rhvnj37ip0.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tj3itb4rhvnj37ip0.json new file mode 100644 index 00000000..a3ff0116 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tj3itb4rhvnj37ip0.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv3tj3itb4rhvnj37ip0", + "operations": [ + { + "add_column": { + "table": "playlists", + "column": { + "name": "apple_music_link", + "type": "text", + "comment": "", + "nullable": true + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv3tj3itb4rhvnj37ip0", + "parent": "mig_cv3tipd2t0po8jv9t7u0", + "schema": "public", + "startedAt": "2025-03-05T04:56:47.661823Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55loqtb4rhvnj37rng.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55loqtb4rhvnj37rng.json new file mode 100644 index 00000000..c550785b --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55loqtb4rhvnj37rng.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55loqtb4rhvnj37rng", + "operations": [ + { + "alter_column": { + "up": "\"spotify_link\"", + "down": "\"spotify_link\"", + "table": "playlists", + "column": "spotify_link", + "unique": { + "name": "playlists_spotify_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55loqtb4rhvnj37rng", + "parent": "mig_cv3tj3itb4rhvnj37ip0", + "schema": "public", + "startedAt": "2025-03-07T02:33:08.663091Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55m5o66mqp0v06gpbg.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55m5o66mqp0v06gpbg.json new file mode 100644 index 00000000..3943fbab --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55m5o66mqp0v06gpbg.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55m5o66mqp0v06gpbg", + "operations": [ + { + "alter_column": { + "up": "\"tidal_link\"", + "down": "\"tidal_link\"", + "table": "playlists", + "column": "tidal_link", + "unique": { + "name": "playlists_tidal_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55m5o66mqp0v06gpbg", + "parent": "mig_cv55loqtb4rhvnj37rng", + "schema": "public", + "startedAt": "2025-03-07T02:34:00.176168Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mat2t0po8jv9tkgg.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mat2t0po8jv9tkgg.json new file mode 100644 index 00000000..dbca5317 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mat2t0po8jv9tkgg.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55mat2t0po8jv9tkgg", + "operations": [ + { + "alter_column": { + "up": "\"apple_music_link\"", + "down": "\"apple_music_link\"", + "table": "playlists", + "column": "apple_music_link", + "unique": { + "name": "playlists_apple_music_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55mat2t0po8jv9tkgg", + "parent": "mig_cv55m5o66mqp0v06gpbg", + "schema": "public", + "startedAt": "2025-03-07T02:34:19.996787Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mi066mqp0v06gpcg.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mi066mqp0v06gpcg.json new file mode 100644 index 00000000..5e75967d --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mi066mqp0v06gpcg.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55mi066mqp0v06gpcg", + "operations": [ + { + "alter_column": { + "up": "\"spotify_link\"", + "down": "\"spotify_link\"", + "table": "albums", + "column": "spotify_link", + "unique": { + "name": "albums_spotify_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55mi066mqp0v06gpcg", + "parent": "mig_cv55mat2t0po8jv9tkgg", + "schema": "public", + "startedAt": "2025-03-07T02:34:49.528087Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mql2t0po8jv9tkhg.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mql2t0po8jv9tkhg.json new file mode 100644 index 00000000..3f0981e7 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mql2t0po8jv9tkhg.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55mql2t0po8jv9tkhg", + "operations": [ + { + "alter_column": { + "up": "\"apple_music_link\"", + "down": "\"apple_music_link\"", + "table": "albums", + "column": "apple_music_link", + "unique": { + "name": "albums_apple_music_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55mql2t0po8jv9tkhg", + "parent": "mig_cv55mi066mqp0v06gpcg", + "schema": "public", + "startedAt": "2025-03-07T02:35:23.576723Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55n3066mqp0v06gpeg.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55n3066mqp0v06gpeg.json new file mode 100644 index 00000000..4d55b724 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55n3066mqp0v06gpeg.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55n3066mqp0v06gpeg", + "operations": [ + { + "alter_column": { + "up": "\"spotify_link\"", + "down": "\"spotify_link\"", + "table": "artists", + "column": "spotify_link", + "unique": { + "name": "artists_spotify_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55n3066mqp0v06gpeg", + "parent": "mig_cv55mql2t0po8jv9tkhg", + "schema": "public", + "startedAt": "2025-03-07T02:35:56.764132Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55n7d2t0po8jv9tkig.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55n7d2t0po8jv9tkig.json new file mode 100644 index 00000000..9ae56071 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55n7d2t0po8jv9tkig.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55n7d2t0po8jv9tkig", + "operations": [ + { + "alter_column": { + "up": "\"tidal_link\"", + "down": "\"tidal_link\"", + "table": "artists", + "column": "tidal_link", + "unique": { + "name": "artists_tidal_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55n7d2t0po8jv9tkig", + "parent": "mig_cv55n3066mqp0v06gpeg", + "schema": "public", + "startedAt": "2025-03-07T02:36:13.866904Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55nf52t0po8jv9tkjg.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55nf52t0po8jv9tkjg.json new file mode 100644 index 00000000..0818998a --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55nf52t0po8jv9tkjg.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55nf52t0po8jv9tkjg", + "operations": [ + { + "alter_column": { + "up": "\"youtube_link\"", + "down": "\"youtube_link\"", + "table": "artists", + "column": "youtube_link", + "unique": { + "name": "artists_youtube_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55nf52t0po8jv9tkjg", + "parent": "mig_cv55n7d2t0po8jv9tkig", + "schema": "public", + "startedAt": "2025-03-07T02:36:44.990769Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55njg66mqp0v06gpfg.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55njg66mqp0v06gpfg.json new file mode 100644 index 00000000..47f0b430 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55njg66mqp0v06gpfg.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55njg66mqp0v06gpfg", + "operations": [ + { + "alter_column": { + "up": "\"apple_music_link\"", + "down": "\"apple_music_link\"", + "table": "artists", + "column": "apple_music_link", + "unique": { + "name": "artists_apple_music_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55njg66mqp0v06gpfg", + "parent": "mig_cv55nf52t0po8jv9tkjg", + "schema": "public", + "startedAt": "2025-03-07T02:37:03.149101Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55nu2tb4rhvnj37rog.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55nu2tb4rhvnj37rog.json new file mode 100644 index 00000000..9a89a05d --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55nu2tb4rhvnj37rog.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55nu2tb4rhvnj37rog", + "operations": [ + { + "alter_column": { + "up": "\"spotify_link\"", + "down": "\"spotify_link\"", + "table": "tracks", + "column": "spotify_link", + "unique": { + "name": "tracks_spotify_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55nu2tb4rhvnj37rog", + "parent": "mig_cv55njg66mqp0v06gpfg", + "schema": "public", + "startedAt": "2025-03-07T02:37:44.390222Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55o3d2t0po8jv9tkkg.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55o3d2t0po8jv9tkkg.json new file mode 100644 index 00000000..2246bbc0 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55o3d2t0po8jv9tkkg.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55o3d2t0po8jv9tkkg", + "operations": [ + { + "alter_column": { + "up": "\"tidal_link\"", + "down": "\"tidal_link\"", + "table": "tracks", + "column": "tidal_link", + "unique": { + "name": "tracks_tidal_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55o3d2t0po8jv9tkkg", + "parent": "mig_cv55nu2tb4rhvnj37rog", + "schema": "public", + "startedAt": "2025-03-07T02:38:06.483745Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55o952t0po8jv9tklg.json b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55o952t0po8jv9tklg.json new file mode 100644 index 00000000..76b422d0 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/mig_cv55o952t0po8jv9tklg.json @@ -0,0 +1,24 @@ +{ + "done": true, + "migration": { + "name": "mig_cv55o952t0po8jv9tklg", + "operations": [ + { + "alter_column": { + "up": "\"youtube_link\"", + "down": "\"youtube_link\"", + "table": "tracks", + "column": "youtube_link", + "unique": { + "name": "tracks_youtube_link_unique" + } + } + } + ] + }, + "migrationType": "pgroll", + "name": "mig_cv55o952t0po8jv9tklg", + "parent": "mig_cv55o3d2t0po8jv9tkkg", + "schema": "public", + "startedAt": "2025-03-07T02:38:28.708265Z" +} diff --git a/rockskyapi/rocksky-auth/.xata/migrations/sql_7fbe41bfacc536.json b/rockskyapi/rocksky-auth/.xata/migrations/sql_7fbe41bfacc536.json new file mode 100644 index 00000000..76bc7e27 --- /dev/null +++ b/rockskyapi/rocksky-auth/.xata/migrations/sql_7fbe41bfacc536.json @@ -0,0 +1,18 @@ +{ + "done": true, + "migration": { + "name": "sql_7fbe41bfacc536", + "operations": [ + { + "sql": { + "up": "CREATE UNIQUE INDEX IF NOT EXISTS user_playlists_unique_index ON user_playlists USING btree (user_id, playlist_id)" + } + } + ] + }, + "migrationType": "inferred", + "name": "sql_7fbe41bfacc536", + "parent": "mig_cv55o952t0po8jv9tklg", + "schema": "public", + "startedAt": "2025-03-07T04:52:51.842079Z" +} diff --git a/rockskyapi/rocksky-auth/bun.lock b/rockskyapi/rocksky-auth/bun.lock index 3147ce3f..99df6ee8 100644 --- a/rockskyapi/rocksky-auth/bun.lock +++ b/rockskyapi/rocksky-auth/bun.lock @@ -18,6 +18,7 @@ "assert": "^2.1.0", "axios": "^1.7.9", "better-sqlite3": "^11.8.1", + "chalk": "^5.4.1", "chanfana": "^2.0.2", "dotenv": "^16.4.7", "drizzle-orm": "^0.39.3", @@ -309,7 +310,7 @@ "cborg": ["cborg@1.10.2", "", { "bin": { "cborg": "cli.js" } }, "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "chanfana": ["chanfana@2.6.3", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.2.0", "js-yaml": "^4.1.0", "openapi3-ts": "^4.4.0", "zod": "^3.23.8" } }, "sha512-Wb3Mc+xte4NzCwZsNgP1TmTunkkIZwmkyP2Ph+JfzypE4UYMf1oppNKGUvWuAEAWH9U17V4QMlRxTfWc6uWyrw=="], @@ -825,6 +826,8 @@ "zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], + "@atproto/lex-cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], diff --git a/rockskyapi/rocksky-auth/lexicons/playlist.json b/rockskyapi/rocksky-auth/lexicons/playlist.json index f6577d92..9a758318 100644 --- a/rockskyapi/rocksky-auth/lexicons/playlist.json +++ b/rockskyapi/rocksky-auth/lexicons/playlist.json @@ -10,7 +10,6 @@ "type": "object", "required": [ "name", - "tracks", "createdAt" ], "properties": { diff --git a/rockskyapi/rocksky-auth/package.json b/rockskyapi/rocksky-auth/package.json index 9bfd23ec..1c1b8d70 100644 --- a/rockskyapi/rocksky-auth/package.json +++ b/rockskyapi/rocksky-auth/package.json @@ -24,6 +24,7 @@ "assert": "^2.1.0", "axios": "^1.7.9", "better-sqlite3": "^11.8.1", + "chalk": "^5.4.1", "chanfana": "^2.0.2", "dotenv": "^16.4.7", "drizzle-orm": "^0.39.3", diff --git a/rockskyapi/rocksky-auth/src/index.ts b/rockskyapi/rocksky-auth/src/index.ts index 3dd33551..c8905339 100644 --- a/rockskyapi/rocksky-auth/src/index.ts +++ b/rockskyapi/rocksky-auth/src/index.ts @@ -11,6 +11,7 @@ import { unLikeTrack, } from "lovedtracks/lovedtracks.service"; import { scrobbleTrack } from "nowplaying/nowplaying.service"; +import subscribe from "subscribers"; import { saveTrack } from "tracks/tracks.service"; import { trackSchema } from "types/track"; import bsky from "./bsky/app"; @@ -19,6 +20,8 @@ import search from "./search/app"; import spotify from "./spotify/app"; import users from "./users/app"; +subscribe(ctx); + const app = new Hono(); app.use(cors()); diff --git a/rockskyapi/rocksky-auth/src/lexicon/lexicons.ts b/rockskyapi/rocksky-auth/src/lexicon/lexicons.ts index bcf6c134..a3eb5c99 100644 --- a/rockskyapi/rocksky-auth/src/lexicon/lexicons.ts +++ b/rockskyapi/rocksky-auth/src/lexicon/lexicons.ts @@ -329,7 +329,7 @@ export const schemaDict = { description: 'A declaration of a playlist.', record: { type: 'object', - required: ['name', 'tracks', 'createdAt'], + required: ['name', 'createdAt'], properties: { name: { type: 'string', diff --git a/rockskyapi/rocksky-auth/src/lexicon/types/app/rocksky/playlist.ts b/rockskyapi/rocksky-auth/src/lexicon/types/app/rocksky/playlist.ts index c609b279..c7cc5407 100644 --- a/rockskyapi/rocksky-auth/src/lexicon/types/app/rocksky/playlist.ts +++ b/rockskyapi/rocksky-auth/src/lexicon/types/app/rocksky/playlist.ts @@ -15,7 +15,7 @@ export interface Record { /** The picture of the playlist. */ picture?: BlobRef /** The tracks in the playlist. */ - tracks: AppRockskySong.Record[] + tracks?: AppRockskySong.Record[] /** The date the playlist was created. */ createdAt: string [k: string]: unknown diff --git a/rockskyapi/rocksky-auth/src/schema/index.ts b/rockskyapi/rocksky-auth/src/schema/index.ts index adc95f16..a52aad2c 100644 --- a/rockskyapi/rocksky-auth/src/schema/index.ts +++ b/rockskyapi/rocksky-auth/src/schema/index.ts @@ -1,5 +1,6 @@ import albums from "./albums"; import artists from "./artists"; +import playlists from "./playlists"; import profileShouts from "./profile-shouts"; import scrobbles from "./scrobbles"; import shoutLikes from "./shout-likes"; @@ -18,4 +19,5 @@ export default { tracks, scrobbles, shoutReports, + playlists, }; diff --git a/rockskyapi/rocksky-auth/src/schema/playlists.ts b/rockskyapi/rocksky-auth/src/schema/playlists.ts new file mode 100644 index 00000000..1e568329 --- /dev/null +++ b/rockskyapi/rocksky-auth/src/schema/playlists.ts @@ -0,0 +1,20 @@ +import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import users from "./users"; + +const playlists = pgTable("playlists", { + id: text("xata_id").primaryKey(), + name: text("name").notNull(), + picture: text("picture"), + description: text("description"), + uri: text("uri").unique(), + spotifyLink: text("spotify_link"), + tidalLink: text("tidal_link"), + appleMusicLink: text("apple_music_link"), + createdBy: text("created_by") + .notNull() + .references(() => users.id), + createdAt: timestamp("xata_createdat").defaultNow().notNull(), + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), +}); + +export default playlists; diff --git a/rockskyapi/rocksky-auth/src/subscribers/index.ts b/rockskyapi/rocksky-auth/src/subscribers/index.ts new file mode 100644 index 00000000..8410f27c --- /dev/null +++ b/rockskyapi/rocksky-auth/src/subscribers/index.ts @@ -0,0 +1,6 @@ +import { Context } from "context"; +import { onNewPlaylist } from "./playlist"; + +export default function subscribe(ctx: Context) { + onNewPlaylist(ctx); +} diff --git a/rockskyapi/rocksky-auth/src/subscribers/playlist.ts b/rockskyapi/rocksky-auth/src/subscribers/playlist.ts new file mode 100644 index 00000000..c521d136 --- /dev/null +++ b/rockskyapi/rocksky-auth/src/subscribers/playlist.ts @@ -0,0 +1,90 @@ +import { TID } from "@atproto/common"; +import { BlobRef } from "@atproto/lexicon"; +import chalk from "chalk"; +import { Context } from "context"; +import * as Playlist from "lexicon/types/app/rocksky/playlist"; +import { createAgent } from "lib/agent"; +import downloadImage, { getContentType } from "lib/downloadImage"; +import { StringCodec } from "nats"; + +export function onNewPlaylist(ctx: Context) { + const sc = StringCodec(); + const sub = ctx.nc.subscribe("rocksky.playlist"); + (async () => { + for await (const m of sub) { + const payload: { + id: string; + did: string; + } = JSON.parse(sc.decode(m.data)); + console.log( + `New playlist: ${chalk.cyan(payload.did)} - ${chalk.greenBright(payload.id)}` + ); + await putPlaylistRecord(ctx, payload); + } + })(); +} + +async function putPlaylistRecord( + ctx: Context, + payload: { id: string; did: string } +) { + const agent = await createAgent(ctx.oauthClient, payload.did); + + if (!agent) { + console.error( + `Failed to create agent, skipping playlist: ${chalk.cyan(payload.id)} for ${chalk.greenBright(payload.did)}` + ); + return; + } + + const playlist = await ctx.client.db.playlists + .filter("xata_id", payload.id) + .getFirst(); + + if (!playlist.uri) { + const rkey = TID.nextStr(); + const record: { + $type: string; + name: string; + description?: string; + createdAt: string; + picture?: BlobRef; + } = { + $type: "app.rocksky.playlist", + name: playlist.name, + description: playlist.description, + createdAt: new Date().toISOString(), + }; + + if (playlist.picture) { + const imageBuffer = await downloadImage(playlist.picture); + const encoding = await getContentType(playlist.picture); + const uploadResponse = await agent.uploadBlob(imageBuffer, { + encoding, + }); + record.picture = uploadResponse.data.blob; + } + + if (!Playlist.validateRecord(record)) { + console.error( + `Invalid record: ${chalk.redBright(JSON.stringify(record))}` + ); + return; + } + + try { + const res = await agent.com.atproto.repo.putRecord({ + repo: agent.assertDid, + collection: "app.rocksky.playlist", + rkey, + record, + validate: false, + }); + const uri = res.data.uri; + console.log(`Playlist record created: ${chalk.greenBright(uri)}`); + await ctx.client.db.playlists.update(payload.id, { uri }); + } catch (e) { + console.error(`Failed to put record: ${chalk.redBright(e.message)}`); + } + } +} diff --git a/rockskyapi/rocksky-auth/src/xata.ts b/rockskyapi/rocksky-auth/src/xata.ts index d5fddb85..dc8de11f 100644 --- a/rockskyapi/rocksky-auth/src/xata.ts +++ b/rockskyapi/rocksky-auth/src/xata.ts @@ -201,6 +201,14 @@ const tables = [ name: "albums__pgroll_new_uri_key", columns: ["uri"], }, + albums_apple_music_link_unique: { + name: "albums_apple_music_link_unique", + columns: ["apple_music_link"], + }, + albums_spotify_link_unique: { + name: "albums_spotify_link_unique", + columns: ["spotify_link"], + }, }, columns: [ { @@ -215,7 +223,7 @@ const tables = [ name: "apple_music_link", type: "text", notNull: false, - unique: false, + unique: true, defaultValue: null, comment: "", }, @@ -255,7 +263,7 @@ const tables = [ name: "spotify_link", type: "text", notNull: false, - unique: false, + unique: true, defaultValue: null, comment: "", }, @@ -612,13 +620,29 @@ const tables = [ name: "artists__pgroll_new_uri_key", columns: ["uri"], }, + artists_apple_music_link_unique: { + name: "artists_apple_music_link_unique", + columns: ["apple_music_link"], + }, + artists_spotify_link_unique: { + name: "artists_spotify_link_unique", + columns: ["spotify_link"], + }, + artists_tidal_link_unique: { + name: "artists_tidal_link_unique", + columns: ["tidal_link"], + }, + artists_youtube_link_unique: { + name: "artists_youtube_link_unique", + columns: ["youtube_link"], + }, }, columns: [ { name: "apple_music_link", type: "text", notNull: false, - unique: false, + unique: true, defaultValue: null, comment: "", }, @@ -682,7 +706,7 @@ const tables = [ name: "spotify_link", type: "text", notNull: false, - unique: false, + unique: true, defaultValue: null, comment: "", }, @@ -690,7 +714,7 @@ const tables = [ name: "tidal_link", type: "text", notNull: false, - unique: false, + unique: true, defaultValue: null, comment: "", }, @@ -738,7 +762,7 @@ const tables = [ name: "youtube_link", type: "text", notNull: false, - unique: false, + unique: true, defaultValue: null, comment: "", }, @@ -935,7 +959,15 @@ const tables = [ definition: "CHECK ((length(xata_id) < 256))", }, }, - foreignKeys: {}, + foreignKeys: { + created_by_link: { + name: "created_by_link", + columns: ["created_by"], + referencedTable: "users", + referencedColumns: ["xata_id"], + onDelete: "SET NULL", + }, + }, primaryKey: [], uniqueConstraints: { _pgroll_new_playlists_xata_id_key: { @@ -946,8 +978,37 @@ const tables = [ name: "playlists__pgroll_new_uri_key", columns: ["uri"], }, + playlists_apple_music_link_unique: { + name: "playlists_apple_music_link_unique", + columns: ["apple_music_link"], + }, + playlists_spotify_link_unique: { + name: "playlists_spotify_link_unique", + columns: ["spotify_link"], + }, + playlists_tidal_link_unique: { + name: "playlists_tidal_link_unique", + columns: ["tidal_link"], + }, }, columns: [ + { + name: "apple_music_link", + type: "text", + notNull: false, + unique: true, + defaultValue: null, + comment: "", + }, + { + name: "created_by", + type: "link", + link: { table: "users" }, + notNull: true, + unique: false, + defaultValue: null, + comment: '{"xata.link":"users"}', + }, { name: "description", type: "text", @@ -964,6 +1025,30 @@ const tables = [ defaultValue: null, comment: "", }, + { + name: "picture", + type: "text", + notNull: false, + unique: false, + defaultValue: null, + comment: "", + }, + { + name: "spotify_link", + type: "text", + notNull: false, + unique: true, + defaultValue: null, + comment: "", + }, + { + name: "tidal_link", + type: "text", + notNull: false, + unique: true, + defaultValue: null, + comment: "", + }, { name: "uri", type: "text", @@ -2038,6 +2123,18 @@ const tables = [ name: "tracks__pgroll_new_uri_key", columns: ["uri"], }, + tracks_spotify_link_unique: { + name: "tracks_spotify_link_unique", + columns: ["spotify_link"], + }, + tracks_tidal_link_unique: { + name: "tracks_tidal_link_unique", + columns: ["tidal_link"], + }, + tracks_youtube_link_unique: { + name: "tracks_youtube_link_unique", + columns: ["youtube_link"], + }, }, columns: [ { @@ -2172,7 +2269,7 @@ const tables = [ name: "spotify_link", type: "text", notNull: false, - unique: false, + unique: true, defaultValue: null, comment: "", }, @@ -2180,7 +2277,7 @@ const tables = [ name: "tidal_link", type: "text", notNull: false, - unique: false, + unique: true, defaultValue: null, comment: "", }, @@ -2244,7 +2341,7 @@ const tables = [ name: "youtube_link", type: "text", notNull: false, - unique: false, + unique: true, defaultValue: null, comment: "", },