A decentralized music tracking and discovery platform built on AT Protocol 🎵

publish playlist to atprotocol

+743 -29
+15 -2
crates/playlists/src/core.rs
··· 4 4 use duckdb::{params, Connection}; 5 5 use owo_colors::OwoColorize; 6 6 use reqwest::Client; 7 + use serde_json::json; 7 8 use sha2::Digest; 8 9 use sqlx::{Pool, Postgres}; 9 10 ··· 152 153 Ok(user_tokens) 153 154 } 154 155 155 - pub async fn save_playlists(pool: &Pool<Postgres>, conn: Arc<Mutex<Connection>>, playlists: Vec<types::playlist::Playlist>, user_id: &str, did: &str) -> Result<(), Error> { 156 + pub async fn save_playlists(pool: &Pool<Postgres>, conn: Arc<Mutex<Connection>>, nc: Arc<Mutex<async_nats::Client>>, playlists: Vec<types::playlist::Playlist>, user_id: &str, did: &str) -> Result<(), Error> { 156 157 let token = generate_token(did)?; 157 158 for playlist in playlists { 158 159 println!("Saving playlist: {} - {} tracks", playlist.name.bright_green(), playlist.tracks.total); ··· 182 183 183 184 let new_playlist = new_playlist.first().unwrap(); 184 185 186 + let nc = nc.lock().unwrap(); 187 + nc.publish("rocksky.playlist", 188 + serde_json::to_string(&json!({ 189 + "id": new_playlist.xata_id.clone(), 190 + "did": did, 191 + }) 192 + ).unwrap().into() 193 + ).await?; 194 + drop(nc); 195 + 196 + let mut i = 1; 185 197 for track in playlist.tracks.items.unwrap_or_default() { 186 - println!("Saving track: {}", track.track.name.bright_green()); 198 + println!("Saving track: {} - {}/{}", track.track.name.bright_green(), i, playlist.tracks.total); 199 + i += 1; 187 200 match save_track(track.track, &token).await? { 188 201 Some(track) => { 189 202 println!("Saved track: {}", track.xata_id.bright_green());
+8 -1
crates/playlists/src/main.rs
··· 2 2 use std::{env, sync::{Arc, Mutex}}; 3 3 4 4 use anyhow::Error; 5 + use async_nats::connect; 5 6 use dotenv::dotenv; 6 7 use duckdb::Connection; 8 + use owo_colors::OwoColorize; 7 9 use playlists::subscriber::subscribe; 8 10 use spotify::get_user_playlists; 9 11 use sqlx::postgres::PgPoolOptions; ··· 38 40 .await?; 39 41 let conn = conn.clone(); 40 42 43 + let addr = env::var("NATS_URL").unwrap_or_else(|_| "nats://localhost:4222".to_string()); 44 + let nc = connect(&addr).await?; 45 + let nc = Arc::new(Mutex::new(nc)); 46 + println!("Connected to NATS server at {}", addr.bright_green()); 47 + 41 48 for user in users { 42 49 let token = user.1.clone(); 43 50 let did = user.2.clone(); 44 51 let user_id = user.3.clone(); 45 52 let playlists = get_user_playlists(token).await?; 46 - save_playlists(&pool, conn.clone(), playlists, &user_id, &did).await?; 53 + save_playlists(&pool, conn.clone(), nc.clone(),playlists, &user_id, &did).await?; 47 54 } 48 55 49 56 println!("Done!");
+12 -12
crates/playlists/src/types/playlist.rs
··· 11 11 pub items: Vec<Playlist>, 12 12 } 13 13 14 - #[derive(Debug, Serialize, Deserialize)] 14 + #[derive(Debug, Serialize, Deserialize, Clone)] 15 15 pub struct Playlist { 16 16 pub collaborative: bool, 17 17 pub description: String, ··· 29 29 pub uri: String, 30 30 } 31 31 32 - #[derive(Debug, Serialize, Deserialize)] 32 + #[derive(Debug, Serialize, Deserialize, Clone)] 33 33 pub struct ExternalUrls { 34 34 pub spotify: String, 35 35 } 36 36 37 - #[derive(Debug, Serialize, Deserialize)] 37 + #[derive(Debug, Serialize, Deserialize, Clone)] 38 38 pub struct Image { 39 39 pub height: u32, 40 40 pub url: String, 41 41 pub width: u32, 42 42 } 43 43 44 - #[derive(Debug, Serialize, Deserialize)] 44 + #[derive(Debug, Serialize, Deserialize, Clone)] 45 45 pub struct Owner { 46 46 pub display_name: Option<String>, 47 47 pub external_urls: ExternalUrls, ··· 51 51 pub uri: String, 52 52 } 53 53 54 - #[derive(Debug, Serialize, Deserialize)] 54 + #[derive(Debug, Serialize, Deserialize, Clone)] 55 55 pub struct Tracks { 56 56 pub href: String, 57 57 pub limit: Option<u32>, ··· 62 62 pub items: Option<Vec<TrackItem>>, 63 63 } 64 64 65 - #[derive(Debug, Serialize, Deserialize)] 65 + #[derive(Debug, Serialize, Deserialize, Clone)] 66 66 pub struct TrackItem { 67 67 pub added_at: String, 68 68 pub added_by: Owner, ··· 70 70 pub track: Track, 71 71 } 72 72 73 - #[derive(Debug, Serialize, Deserialize)] 73 + #[derive(Debug, Serialize, Deserialize, Clone)] 74 74 pub struct Track { 75 75 pub album: Album, 76 76 pub artists: Vec<Artist>, ··· 94 94 pub is_local: bool, 95 95 } 96 96 97 - #[derive(Debug, Serialize, Deserialize)] 97 + #[derive(Debug, Serialize, Deserialize, Clone)] 98 98 pub struct Album { 99 99 pub album_type: String, 100 100 pub total_tracks: u32, ··· 112 112 pub artists: Vec<Artist>, 113 113 } 114 114 115 - #[derive(Debug, Serialize, Deserialize)] 115 + #[derive(Debug, Serialize, Deserialize, Clone)] 116 116 pub struct Artist { 117 117 pub external_urls: ExternalUrls, 118 118 pub href: String, ··· 122 122 pub uri: String, 123 123 } 124 124 125 - #[derive(Debug, Serialize, Deserialize)] 125 + #[derive(Debug, Serialize, Deserialize, Clone)] 126 126 pub struct ExternalIds { 127 127 pub isrc: String, 128 128 pub ean: Option<String>, 129 129 pub upc: Option<String>, 130 130 } 131 131 132 - #[derive(Debug, Serialize, Deserialize)] 132 + #[derive(Debug, Serialize, Deserialize, Clone)] 133 133 pub struct LinkedFrom {} 134 134 135 - #[derive(Debug, Serialize, Deserialize)] 135 + #[derive(Debug, Serialize, Deserialize, Clone)] 136 136 pub struct Restrictions { 137 137 pub reason: String, 138 138 }
+19
rockskyapi/rocksky-auth/.xata/migrations/.ledger
··· 163 163 mig_cuq2866fvuvgoi6asfpg 164 164 mig_cuq28kefvuvgoi6asfqg 165 165 mig_cusvclmfvuvgoi6atb2g 166 + mig_cuvv6k1dsuem1m1rk3l0 167 + mig_cuvvadtv5omnnkb7cs40 168 + mig_cv3tgjqtb4rhvnj37il0 169 + mig_cv3tib2tb4rhvnj37io0 170 + mig_cv3tipd2t0po8jv9t7u0 171 + mig_cv3tj3itb4rhvnj37ip0 172 + mig_cv55loqtb4rhvnj37rng 173 + mig_cv55m5o66mqp0v06gpbg 174 + mig_cv55mat2t0po8jv9tkgg 175 + mig_cv55mi066mqp0v06gpcg 176 + mig_cv55mql2t0po8jv9tkhg 177 + mig_cv55n3066mqp0v06gpeg 178 + mig_cv55n7d2t0po8jv9tkig 179 + mig_cv55nf52t0po8jv9tkjg 180 + mig_cv55njg66mqp0v06gpfg 181 + mig_cv55nu2tb4rhvnj37rog 182 + mig_cv55o3d2t0po8jv9tkkg 183 + mig_cv55o952t0po8jv9tklg 184 + sql_7fbe41bfacc536
+30
rockskyapi/rocksky-auth/.xata/migrations/mig_cuvv6k1dsuem1m1rk3l0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cuvv6k1dsuem1m1rk3l0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "playlists", 10 + "column": { 11 + "name": "created_by", 12 + "type": "text", 13 + "comment": "{\"xata.link\":\"users\"}", 14 + "references": { 15 + "name": "created_by_link", 16 + "table": "users", 17 + "column": "xata_id", 18 + "on_delete": "SET NULL" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_cuvv6k1dsuem1m1rk3l0", 27 + "parent": "mig_cusvclmfvuvgoi6atb2g", 28 + "schema": "public", 29 + "startedAt": "2025-02-27T05:08:32.597847Z" 30 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cuvvadtv5omnnkb7cs40.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cuvvadtv5omnnkb7cs40", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "playlists", 9 + "column": { 10 + "name": "picture", 11 + "type": "text", 12 + "comment": "", 13 + "nullable": true 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cuvvadtv5omnnkb7cs40", 21 + "parent": "mig_cuvv6k1dsuem1m1rk3l0", 22 + "schema": "public", 23 + "startedAt": "2025-02-27T05:16:39.402596Z" 24 + }
+25
rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tgjqtb4rhvnj37il0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv3tgjqtb4rhvnj37il0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "playlists", 9 + "column": { 10 + "name": "spotify_link", 11 + "type": "text", 12 + "unique": true, 13 + "comment": "", 14 + "nullable": true 15 + } 16 + } 17 + } 18 + ] 19 + }, 20 + "migrationType": "pgroll", 21 + "name": "mig_cv3tgjqtb4rhvnj37il0", 22 + "parent": "mig_cuvvadtv5omnnkb7cs40", 23 + "schema": "public", 24 + "startedAt": "2025-03-05T04:51:28.013325Z" 25 + }
+21
rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tib2tb4rhvnj37io0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv3tib2tb4rhvnj37io0", 5 + "operations": [ 6 + { 7 + "drop_constraint": { 8 + "up": "\"spotify_link\"", 9 + "down": "\"spotify_link\"", 10 + "name": "playlists__pgroll_new_spotify_link_key", 11 + "table": "playlists" 12 + } 13 + } 14 + ] 15 + }, 16 + "migrationType": "pgroll", 17 + "name": "mig_cv3tib2tb4rhvnj37io0", 18 + "parent": "mig_cv3tgjqtb4rhvnj37il0", 19 + "schema": "public", 20 + "startedAt": "2025-03-05T04:55:09.2563Z" 21 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tipd2t0po8jv9t7u0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv3tipd2t0po8jv9t7u0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "playlists", 9 + "column": { 10 + "name": "tidal_link", 11 + "type": "text", 12 + "comment": "", 13 + "nullable": true 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv3tipd2t0po8jv9t7u0", 21 + "parent": "mig_cv3tib2tb4rhvnj37io0", 22 + "schema": "public", 23 + "startedAt": "2025-03-05T04:56:06.432882Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv3tj3itb4rhvnj37ip0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv3tj3itb4rhvnj37ip0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "playlists", 9 + "column": { 10 + "name": "apple_music_link", 11 + "type": "text", 12 + "comment": "", 13 + "nullable": true 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv3tj3itb4rhvnj37ip0", 21 + "parent": "mig_cv3tipd2t0po8jv9t7u0", 22 + "schema": "public", 23 + "startedAt": "2025-03-05T04:56:47.661823Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55loqtb4rhvnj37rng.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55loqtb4rhvnj37rng", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"spotify_link\"", 9 + "down": "\"spotify_link\"", 10 + "table": "playlists", 11 + "column": "spotify_link", 12 + "unique": { 13 + "name": "playlists_spotify_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55loqtb4rhvnj37rng", 21 + "parent": "mig_cv3tj3itb4rhvnj37ip0", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:33:08.663091Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55m5o66mqp0v06gpbg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55m5o66mqp0v06gpbg", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"tidal_link\"", 9 + "down": "\"tidal_link\"", 10 + "table": "playlists", 11 + "column": "tidal_link", 12 + "unique": { 13 + "name": "playlists_tidal_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55m5o66mqp0v06gpbg", 21 + "parent": "mig_cv55loqtb4rhvnj37rng", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:34:00.176168Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mat2t0po8jv9tkgg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55mat2t0po8jv9tkgg", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"apple_music_link\"", 9 + "down": "\"apple_music_link\"", 10 + "table": "playlists", 11 + "column": "apple_music_link", 12 + "unique": { 13 + "name": "playlists_apple_music_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55mat2t0po8jv9tkgg", 21 + "parent": "mig_cv55m5o66mqp0v06gpbg", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:34:19.996787Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mi066mqp0v06gpcg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55mi066mqp0v06gpcg", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"spotify_link\"", 9 + "down": "\"spotify_link\"", 10 + "table": "albums", 11 + "column": "spotify_link", 12 + "unique": { 13 + "name": "albums_spotify_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55mi066mqp0v06gpcg", 21 + "parent": "mig_cv55mat2t0po8jv9tkgg", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:34:49.528087Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55mql2t0po8jv9tkhg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55mql2t0po8jv9tkhg", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"apple_music_link\"", 9 + "down": "\"apple_music_link\"", 10 + "table": "albums", 11 + "column": "apple_music_link", 12 + "unique": { 13 + "name": "albums_apple_music_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55mql2t0po8jv9tkhg", 21 + "parent": "mig_cv55mi066mqp0v06gpcg", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:35:23.576723Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55n3066mqp0v06gpeg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55n3066mqp0v06gpeg", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"spotify_link\"", 9 + "down": "\"spotify_link\"", 10 + "table": "artists", 11 + "column": "spotify_link", 12 + "unique": { 13 + "name": "artists_spotify_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55n3066mqp0v06gpeg", 21 + "parent": "mig_cv55mql2t0po8jv9tkhg", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:35:56.764132Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55n7d2t0po8jv9tkig.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55n7d2t0po8jv9tkig", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"tidal_link\"", 9 + "down": "\"tidal_link\"", 10 + "table": "artists", 11 + "column": "tidal_link", 12 + "unique": { 13 + "name": "artists_tidal_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55n7d2t0po8jv9tkig", 21 + "parent": "mig_cv55n3066mqp0v06gpeg", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:36:13.866904Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55nf52t0po8jv9tkjg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55nf52t0po8jv9tkjg", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"youtube_link\"", 9 + "down": "\"youtube_link\"", 10 + "table": "artists", 11 + "column": "youtube_link", 12 + "unique": { 13 + "name": "artists_youtube_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55nf52t0po8jv9tkjg", 21 + "parent": "mig_cv55n7d2t0po8jv9tkig", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:36:44.990769Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55njg66mqp0v06gpfg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55njg66mqp0v06gpfg", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"apple_music_link\"", 9 + "down": "\"apple_music_link\"", 10 + "table": "artists", 11 + "column": "apple_music_link", 12 + "unique": { 13 + "name": "artists_apple_music_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55njg66mqp0v06gpfg", 21 + "parent": "mig_cv55nf52t0po8jv9tkjg", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:37:03.149101Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55nu2tb4rhvnj37rog.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55nu2tb4rhvnj37rog", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"spotify_link\"", 9 + "down": "\"spotify_link\"", 10 + "table": "tracks", 11 + "column": "spotify_link", 12 + "unique": { 13 + "name": "tracks_spotify_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55nu2tb4rhvnj37rog", 21 + "parent": "mig_cv55njg66mqp0v06gpfg", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:37:44.390222Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55o3d2t0po8jv9tkkg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55o3d2t0po8jv9tkkg", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"tidal_link\"", 9 + "down": "\"tidal_link\"", 10 + "table": "tracks", 11 + "column": "tidal_link", 12 + "unique": { 13 + "name": "tracks_tidal_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55o3d2t0po8jv9tkkg", 21 + "parent": "mig_cv55nu2tb4rhvnj37rog", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:38:06.483745Z" 24 + }
+24
rockskyapi/rocksky-auth/.xata/migrations/mig_cv55o952t0po8jv9tklg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_cv55o952t0po8jv9tklg", 5 + "operations": [ 6 + { 7 + "alter_column": { 8 + "up": "\"youtube_link\"", 9 + "down": "\"youtube_link\"", 10 + "table": "tracks", 11 + "column": "youtube_link", 12 + "unique": { 13 + "name": "tracks_youtube_link_unique" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_cv55o952t0po8jv9tklg", 21 + "parent": "mig_cv55o3d2t0po8jv9tkkg", 22 + "schema": "public", 23 + "startedAt": "2025-03-07T02:38:28.708265Z" 24 + }
+18
rockskyapi/rocksky-auth/.xata/migrations/sql_7fbe41bfacc536.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "sql_7fbe41bfacc536", 5 + "operations": [ 6 + { 7 + "sql": { 8 + "up": "CREATE UNIQUE INDEX IF NOT EXISTS user_playlists_unique_index ON user_playlists USING btree (user_id, playlist_id)" 9 + } 10 + } 11 + ] 12 + }, 13 + "migrationType": "inferred", 14 + "name": "sql_7fbe41bfacc536", 15 + "parent": "mig_cv55o952t0po8jv9tklg", 16 + "schema": "public", 17 + "startedAt": "2025-03-07T04:52:51.842079Z" 18 + }
+4 -1
rockskyapi/rocksky-auth/bun.lock
··· 18 18 "assert": "^2.1.0", 19 19 "axios": "^1.7.9", 20 20 "better-sqlite3": "^11.8.1", 21 + "chalk": "^5.4.1", 21 22 "chanfana": "^2.0.2", 22 23 "dotenv": "^16.4.7", 23 24 "drizzle-orm": "^0.39.3", ··· 309 310 310 311 "cborg": ["cborg@1.10.2", "", { "bin": { "cborg": "cli.js" } }, "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug=="], 311 312 312 - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 313 + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], 313 314 314 315 "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=="], 315 316 ··· 824 825 "yesno": ["yesno@0.4.0", "", {}, "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA=="], 825 826 826 827 "zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], 828 + 829 + "@atproto/lex-cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 827 830 828 831 "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 829 832
-1
rockskyapi/rocksky-auth/lexicons/playlist.json
··· 10 10 "type": "object", 11 11 "required": [ 12 12 "name", 13 - "tracks", 14 13 "createdAt" 15 14 ], 16 15 "properties": {
+1
rockskyapi/rocksky-auth/package.json
··· 24 24 "assert": "^2.1.0", 25 25 "axios": "^1.7.9", 26 26 "better-sqlite3": "^11.8.1", 27 + "chalk": "^5.4.1", 27 28 "chanfana": "^2.0.2", 28 29 "dotenv": "^16.4.7", 29 30 "drizzle-orm": "^0.39.3",
+3
rockskyapi/rocksky-auth/src/index.ts
··· 11 11 unLikeTrack, 12 12 } from "lovedtracks/lovedtracks.service"; 13 13 import { scrobbleTrack } from "nowplaying/nowplaying.service"; 14 + import subscribe from "subscribers"; 14 15 import { saveTrack } from "tracks/tracks.service"; 15 16 import { trackSchema } from "types/track"; 16 17 import bsky from "./bsky/app"; ··· 18 19 import search from "./search/app"; 19 20 import spotify from "./spotify/app"; 20 21 import users from "./users/app"; 22 + 23 + subscribe(ctx); 21 24 22 25 const app = new Hono(); 23 26
+1 -1
rockskyapi/rocksky-auth/src/lexicon/lexicons.ts
··· 329 329 description: 'A declaration of a playlist.', 330 330 record: { 331 331 type: 'object', 332 - required: ['name', 'tracks', 'createdAt'], 332 + required: ['name', 'createdAt'], 333 333 properties: { 334 334 name: { 335 335 type: 'string',
+1 -1
rockskyapi/rocksky-auth/src/lexicon/types/app/rocksky/playlist.ts
··· 15 15 /** The picture of the playlist. */ 16 16 picture?: BlobRef 17 17 /** The tracks in the playlist. */ 18 - tracks: AppRockskySong.Record[] 18 + tracks?: AppRockskySong.Record[] 19 19 /** The date the playlist was created. */ 20 20 createdAt: string 21 21 [k: string]: unknown
+2
rockskyapi/rocksky-auth/src/schema/index.ts
··· 1 1 import albums from "./albums"; 2 2 import artists from "./artists"; 3 + import playlists from "./playlists"; 3 4 import profileShouts from "./profile-shouts"; 4 5 import scrobbles from "./scrobbles"; 5 6 import shoutLikes from "./shout-likes"; ··· 18 19 tracks, 19 20 scrobbles, 20 21 shoutReports, 22 + playlists, 21 23 };
+20
rockskyapi/rocksky-auth/src/schema/playlists.ts
··· 1 + import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 + import users from "./users"; 3 + 4 + const playlists = pgTable("playlists", { 5 + id: text("xata_id").primaryKey(), 6 + name: text("name").notNull(), 7 + picture: text("picture"), 8 + description: text("description"), 9 + uri: text("uri").unique(), 10 + spotifyLink: text("spotify_link"), 11 + tidalLink: text("tidal_link"), 12 + appleMusicLink: text("apple_music_link"), 13 + createdBy: text("created_by") 14 + .notNull() 15 + .references(() => users.id), 16 + createdAt: timestamp("xata_createdat").defaultNow().notNull(), 17 + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 18 + }); 19 + 20 + export default playlists;
+6
rockskyapi/rocksky-auth/src/subscribers/index.ts
··· 1 + import { Context } from "context"; 2 + import { onNewPlaylist } from "./playlist"; 3 + 4 + export default function subscribe(ctx: Context) { 5 + onNewPlaylist(ctx); 6 + }
+90
rockskyapi/rocksky-auth/src/subscribers/playlist.ts
··· 1 + import { TID } from "@atproto/common"; 2 + import { BlobRef } from "@atproto/lexicon"; 3 + import chalk from "chalk"; 4 + import { Context } from "context"; 5 + import * as Playlist from "lexicon/types/app/rocksky/playlist"; 6 + import { createAgent } from "lib/agent"; 7 + import downloadImage, { getContentType } from "lib/downloadImage"; 8 + import { StringCodec } from "nats"; 9 + 10 + export function onNewPlaylist(ctx: Context) { 11 + const sc = StringCodec(); 12 + const sub = ctx.nc.subscribe("rocksky.playlist"); 13 + (async () => { 14 + for await (const m of sub) { 15 + const payload: { 16 + id: string; 17 + did: string; 18 + } = JSON.parse(sc.decode(m.data)); 19 + console.log( 20 + `New playlist: ${chalk.cyan(payload.did)} - ${chalk.greenBright(payload.id)}` 21 + ); 22 + await putPlaylistRecord(ctx, payload); 23 + } 24 + })(); 25 + } 26 + 27 + async function putPlaylistRecord( 28 + ctx: Context, 29 + payload: { id: string; did: string } 30 + ) { 31 + const agent = await createAgent(ctx.oauthClient, payload.did); 32 + 33 + if (!agent) { 34 + console.error( 35 + `Failed to create agent, skipping playlist: ${chalk.cyan(payload.id)} for ${chalk.greenBright(payload.did)}` 36 + ); 37 + return; 38 + } 39 + 40 + const playlist = await ctx.client.db.playlists 41 + .filter("xata_id", payload.id) 42 + .getFirst(); 43 + 44 + if (!playlist.uri) { 45 + const rkey = TID.nextStr(); 46 + const record: { 47 + $type: string; 48 + name: string; 49 + description?: string; 50 + createdAt: string; 51 + picture?: BlobRef; 52 + } = { 53 + $type: "app.rocksky.playlist", 54 + name: playlist.name, 55 + description: playlist.description, 56 + createdAt: new Date().toISOString(), 57 + }; 58 + 59 + if (playlist.picture) { 60 + const imageBuffer = await downloadImage(playlist.picture); 61 + const encoding = await getContentType(playlist.picture); 62 + const uploadResponse = await agent.uploadBlob(imageBuffer, { 63 + encoding, 64 + }); 65 + record.picture = uploadResponse.data.blob; 66 + } 67 + 68 + if (!Playlist.validateRecord(record)) { 69 + console.error( 70 + `Invalid record: ${chalk.redBright(JSON.stringify(record))}` 71 + ); 72 + return; 73 + } 74 + 75 + try { 76 + const res = await agent.com.atproto.repo.putRecord({ 77 + repo: agent.assertDid, 78 + collection: "app.rocksky.playlist", 79 + rkey, 80 + record, 81 + validate: false, 82 + }); 83 + const uri = res.data.uri; 84 + console.log(`Playlist record created: ${chalk.greenBright(uri)}`); 85 + await ctx.client.db.playlists.update(payload.id, { uri }); 86 + } catch (e) { 87 + console.error(`Failed to put record: ${chalk.redBright(e.message)}`); 88 + } 89 + } 90 + }
+107 -10
rockskyapi/rocksky-auth/src/xata.ts
··· 201 201 name: "albums__pgroll_new_uri_key", 202 202 columns: ["uri"], 203 203 }, 204 + albums_apple_music_link_unique: { 205 + name: "albums_apple_music_link_unique", 206 + columns: ["apple_music_link"], 207 + }, 208 + albums_spotify_link_unique: { 209 + name: "albums_spotify_link_unique", 210 + columns: ["spotify_link"], 211 + }, 204 212 }, 205 213 columns: [ 206 214 { ··· 215 223 name: "apple_music_link", 216 224 type: "text", 217 225 notNull: false, 218 - unique: false, 226 + unique: true, 219 227 defaultValue: null, 220 228 comment: "", 221 229 }, ··· 255 263 name: "spotify_link", 256 264 type: "text", 257 265 notNull: false, 258 - unique: false, 266 + unique: true, 259 267 defaultValue: null, 260 268 comment: "", 261 269 }, ··· 612 620 name: "artists__pgroll_new_uri_key", 613 621 columns: ["uri"], 614 622 }, 623 + artists_apple_music_link_unique: { 624 + name: "artists_apple_music_link_unique", 625 + columns: ["apple_music_link"], 626 + }, 627 + artists_spotify_link_unique: { 628 + name: "artists_spotify_link_unique", 629 + columns: ["spotify_link"], 630 + }, 631 + artists_tidal_link_unique: { 632 + name: "artists_tidal_link_unique", 633 + columns: ["tidal_link"], 634 + }, 635 + artists_youtube_link_unique: { 636 + name: "artists_youtube_link_unique", 637 + columns: ["youtube_link"], 638 + }, 615 639 }, 616 640 columns: [ 617 641 { 618 642 name: "apple_music_link", 619 643 type: "text", 620 644 notNull: false, 621 - unique: false, 645 + unique: true, 622 646 defaultValue: null, 623 647 comment: "", 624 648 }, ··· 682 706 name: "spotify_link", 683 707 type: "text", 684 708 notNull: false, 685 - unique: false, 709 + unique: true, 686 710 defaultValue: null, 687 711 comment: "", 688 712 }, ··· 690 714 name: "tidal_link", 691 715 type: "text", 692 716 notNull: false, 693 - unique: false, 717 + unique: true, 694 718 defaultValue: null, 695 719 comment: "", 696 720 }, ··· 738 762 name: "youtube_link", 739 763 type: "text", 740 764 notNull: false, 741 - unique: false, 765 + unique: true, 742 766 defaultValue: null, 743 767 comment: "", 744 768 }, ··· 935 959 definition: "CHECK ((length(xata_id) < 256))", 936 960 }, 937 961 }, 938 - foreignKeys: {}, 962 + foreignKeys: { 963 + created_by_link: { 964 + name: "created_by_link", 965 + columns: ["created_by"], 966 + referencedTable: "users", 967 + referencedColumns: ["xata_id"], 968 + onDelete: "SET NULL", 969 + }, 970 + }, 939 971 primaryKey: [], 940 972 uniqueConstraints: { 941 973 _pgroll_new_playlists_xata_id_key: { ··· 946 978 name: "playlists__pgroll_new_uri_key", 947 979 columns: ["uri"], 948 980 }, 981 + playlists_apple_music_link_unique: { 982 + name: "playlists_apple_music_link_unique", 983 + columns: ["apple_music_link"], 984 + }, 985 + playlists_spotify_link_unique: { 986 + name: "playlists_spotify_link_unique", 987 + columns: ["spotify_link"], 988 + }, 989 + playlists_tidal_link_unique: { 990 + name: "playlists_tidal_link_unique", 991 + columns: ["tidal_link"], 992 + }, 949 993 }, 950 994 columns: [ 995 + { 996 + name: "apple_music_link", 997 + type: "text", 998 + notNull: false, 999 + unique: true, 1000 + defaultValue: null, 1001 + comment: "", 1002 + }, 1003 + { 1004 + name: "created_by", 1005 + type: "link", 1006 + link: { table: "users" }, 1007 + notNull: true, 1008 + unique: false, 1009 + defaultValue: null, 1010 + comment: '{"xata.link":"users"}', 1011 + }, 951 1012 { 952 1013 name: "description", 953 1014 type: "text", ··· 961 1022 type: "text", 962 1023 notNull: true, 963 1024 unique: false, 1025 + defaultValue: null, 1026 + comment: "", 1027 + }, 1028 + { 1029 + name: "picture", 1030 + type: "text", 1031 + notNull: false, 1032 + unique: false, 1033 + defaultValue: null, 1034 + comment: "", 1035 + }, 1036 + { 1037 + name: "spotify_link", 1038 + type: "text", 1039 + notNull: false, 1040 + unique: true, 1041 + defaultValue: null, 1042 + comment: "", 1043 + }, 1044 + { 1045 + name: "tidal_link", 1046 + type: "text", 1047 + notNull: false, 1048 + unique: true, 964 1049 defaultValue: null, 965 1050 comment: "", 966 1051 }, ··· 2038 2123 name: "tracks__pgroll_new_uri_key", 2039 2124 columns: ["uri"], 2040 2125 }, 2126 + tracks_spotify_link_unique: { 2127 + name: "tracks_spotify_link_unique", 2128 + columns: ["spotify_link"], 2129 + }, 2130 + tracks_tidal_link_unique: { 2131 + name: "tracks_tidal_link_unique", 2132 + columns: ["tidal_link"], 2133 + }, 2134 + tracks_youtube_link_unique: { 2135 + name: "tracks_youtube_link_unique", 2136 + columns: ["youtube_link"], 2137 + }, 2041 2138 }, 2042 2139 columns: [ 2043 2140 { ··· 2172 2269 name: "spotify_link", 2173 2270 type: "text", 2174 2271 notNull: false, 2175 - unique: false, 2272 + unique: true, 2176 2273 defaultValue: null, 2177 2274 comment: "", 2178 2275 }, ··· 2180 2277 name: "tidal_link", 2181 2278 type: "text", 2182 2279 notNull: false, 2183 - unique: false, 2280 + unique: true, 2184 2281 defaultValue: null, 2185 2282 comment: "", 2186 2283 }, ··· 2244 2341 name: "youtube_link", 2245 2342 type: "text", 2246 2343 notNull: false, 2247 - unique: false, 2344 + unique: true, 2248 2345 defaultValue: null, 2249 2346 comment: "", 2250 2347 },