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

[spotify] restart thread on error

+406 -23
+54 -4
crates/spotify/src/main.rs
··· 58 58 let email = user.0.clone(); 59 59 let token = user.1.clone(); 60 60 let did = user.2.clone(); 61 + let user_id = user.3.clone(); 61 62 let stop_flag = Arc::new(AtomicBool::new(false)); 62 63 let cache = cache.clone(); 64 + let nc = nc.clone(); 63 65 let thread_map = Arc::clone(&thread_map); 64 66 65 67 thread_map ··· 82 84 email.bright_green(), 83 85 e.to_string().bright_red() 84 86 ); 87 + 88 + // If there's an error, publish a message to restart the thread 89 + match rt.block_on(nc.publish("rocksky.spotify.user", user_id.into())) { 90 + Ok(_) => { 91 + println!( 92 + "{} Published message to restart thread for user: {}", 93 + format!("[{}]", email).bright_green(), 94 + email.bright_green() 95 + ); 96 + } 97 + Err(e) => { 98 + println!( 99 + "{} Error publishing message to restart thread: {}", 100 + format!("[{}]", email).bright_green(), 101 + e.to_string().bright_red() 102 + ); 103 + } 104 + } 85 105 } 86 106 } 87 107 }); ··· 161 181 let did = user.2.clone(); 162 182 let stop_flag = Arc::new(AtomicBool::new(false)); 163 183 let cache = cache.clone(); 184 + let nc = nc.clone(); 164 185 165 186 thread_map.insert(email.clone(), Arc::clone(&stop_flag)); 166 187 ··· 185 206 email.bright_green(), 186 207 e.to_string().bright_red() 187 208 ); 209 + match rt.block_on(nc.publish("rocksky.spotify.user", user_id.into())) { 210 + Ok(_) => {}, 211 + Err(e) => { 212 + println!( 213 + "{} Error publishing message to restart thread: {}", 214 + format!("[{}]", email).bright_green(), 215 + e.to_string().bright_red() 216 + ); 217 + } 218 + } 188 219 } 189 220 } 190 221 }); ··· 562 593 return Ok(None); 563 594 } 564 595 565 - cache.setex(album_id, &data, 20)?; 596 + match cache.setex(album_id, &data, 20) { 597 + Ok(_) => {} 598 + Err(e) => { 599 + println!( 600 + "{} redis error: {}", 601 + format!("[{}]", album_id).bright_green(), 602 + e.to_string().bright_red() 603 + ); 604 + return Ok(None); 605 + } 606 + } 566 607 567 608 Ok(Some(serde_json::from_str(&data)?)) 568 609 } ··· 615 656 } 616 657 617 658 let all_tracks_json = serde_json::to_string(&all_tracks)?; 618 - cache.setex(&format!("{}:tracks", album_id), &all_tracks_json, 20)?; 659 + match cache.setex(&format!("{}:tracks", album_id), &all_tracks_json, 20) { 660 + Ok(_) => {} 661 + Err(e) => { 662 + println!( 663 + "{} redis error: {}", 664 + format!("[{}]", album_id).bright_green(), 665 + e.to_string().bright_red() 666 + ); 667 + } 668 + } 619 669 620 670 Ok(AlbumTracks { 621 671 items: all_tracks, ··· 627 677 pool: &Pool<Postgres>, 628 678 offset: usize, 629 679 limit: usize, 630 - ) -> Result<Vec<(String, String, String)>, Error> { 680 + ) -> Result<Vec<(String, String, String, String)>, Error> { 631 681 let results: Vec<SpotifyTokenWithEmail> = sqlx::query_as( 632 682 r#" 633 683 SELECT * FROM spotify_tokens ··· 648 698 &result.refresh_token, 649 699 &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 650 700 )?; 651 - user_tokens.push((result.email.clone(), token, result.did.clone())); 701 + user_tokens.push((result.email.clone(), token, result.did.clone(), result.user_id.clone())); 652 702 } 653 703 654 704 Ok(user_tokens)
+82 -3
rockskyapi/rocksky-auth/src/xrpc/app/rocksky/spotify/pause.ts
··· 1 1 import { HandlerAuth } from "@atproto/xrpc-server"; 2 2 import { Context } from "context"; 3 + import { eq } from "drizzle-orm"; 3 4 import { Effect, pipe } from "effect"; 4 5 import { Server } from "lexicon"; 6 + import { QueryParams } from "lexicon/types/app/rocksky/spotify/pause"; 7 + import { decrypt } from "lib/crypto"; 8 + import { env } from "lib/env"; 9 + import tables from "schema"; 10 + import { SelectUser } from "schema/users"; 5 11 6 12 export default function (server: Server, ctx: Context) { 7 13 const pause = (params, auth: HandlerAuth) => 8 14 pipe( 9 15 { params, ctx, did: auth.credentials?.did }, 10 - withSpotifyToken, 16 + withUser, 17 + Effect.flatMap(withSpotifyRefreshToken), 18 + Effect.flatMap(withSpotifyToken), 11 19 Effect.flatMap(handlePause), 12 20 Effect.flatMap(presentation), 13 21 Effect.retry({ times: 3 }), ··· 25 33 }); 26 34 } 27 35 28 - const withSpotifyToken = () => { 36 + const withUser = ({ 37 + did, 38 + ctx, 39 + }: { 40 + params: QueryParams; 41 + did: string; 42 + ctx: Context; 43 + }) => { 44 + return Effect.tryPromise({ 45 + try: () => 46 + ctx.db 47 + .select() 48 + .from(tables.users) 49 + .where(eq(tables.users.did, did)) 50 + .execute() 51 + .then(([user]) => ({ 52 + user, 53 + ctx, 54 + did, 55 + })), 56 + catch: (error) => new Error(`Failed to retrieve User: ${error}`), 57 + }); 58 + }; 59 + 60 + const withSpotifyRefreshToken = ({ 61 + user, 62 + ctx, 63 + }: { 64 + user: SelectUser; 65 + ctx: Context; 66 + }) => { 67 + return Effect.tryPromise({ 68 + try: () => 69 + ctx.db 70 + .select() 71 + .from(tables.spotifyTokens) 72 + .where(eq(tables.spotifyTokens.userId, user.id)) 73 + .execute() 74 + .then(([spotifyToken]) => 75 + decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY) 76 + ) 77 + .then((refreshToken) => ({ 78 + user, 79 + ctx, 80 + refreshToken, 81 + })), 82 + catch: (error) => 83 + new Error(`Failed to retrieve Spotify Refresh token: ${error}`), 84 + }); 85 + }; 86 + 87 + const withSpotifyToken = ({ 88 + refreshToken, 89 + ctx, 90 + }: { 91 + refreshToken: string; 92 + ctx: Context; 93 + }) => { 29 94 return Effect.tryPromise({ 30 - try: async () => {}, 95 + try: () => 96 + fetch("https://accounts.spotify.com/api/token", { 97 + method: "POST", 98 + headers: { 99 + "Content-Type": "application/x-www-form-urlencoded", 100 + }, 101 + body: new URLSearchParams({ 102 + grant_type: "refresh_token", 103 + refresh_token: refreshToken, 104 + client_id: env.SPOTIFY_CLIENT_ID, 105 + client_secret: env.SPOTIFY_CLIENT_SECRET, 106 + }), 107 + }) 108 + .then((res) => res.json()) 109 + .then((data) => data.access_token), 31 110 catch: (error) => new Error(`Failed to retrieve Spotify token: ${error}`), 32 111 }); 33 112 };
+82 -3
rockskyapi/rocksky-auth/src/xrpc/app/rocksky/spotify/play.ts
··· 1 1 import { HandlerAuth } from "@atproto/xrpc-server"; 2 2 import { Context } from "context"; 3 + import { eq } from "drizzle-orm"; 3 4 import { Effect, pipe } from "effect"; 4 5 import { Server } from "lexicon"; 6 + import { QueryParams } from "lexicon/types/app/rocksky/spotify/play"; 7 + import { decrypt } from "lib/crypto"; 8 + import { env } from "lib/env"; 9 + import tables from "schema"; 10 + import { SelectUser } from "schema/users"; 5 11 6 12 export default function (server: Server, ctx: Context) { 7 13 const play = (params, auth: HandlerAuth) => 8 14 pipe( 9 15 { params, ctx, did: auth.credentials?.did }, 10 - withSpotifyToken, 16 + withUser, 17 + Effect.flatMap(withSpotifyRefreshToken), 18 + Effect.flatMap(withSpotifyToken), 11 19 Effect.flatMap(handlePlay), 12 20 Effect.flatMap(presentation), 13 21 Effect.retry({ times: 3 }), ··· 25 33 }); 26 34 } 27 35 28 - const withSpotifyToken = () => { 36 + const withUser = ({ 37 + did, 38 + ctx, 39 + }: { 40 + params: QueryParams; 41 + did: string; 42 + ctx: Context; 43 + }) => { 44 + return Effect.tryPromise({ 45 + try: () => 46 + ctx.db 47 + .select() 48 + .from(tables.users) 49 + .where(eq(tables.users.did, did)) 50 + .execute() 51 + .then(([user]) => ({ 52 + user, 53 + ctx, 54 + did, 55 + })), 56 + catch: (error) => new Error(`Failed to retrieve User: ${error}`), 57 + }); 58 + }; 59 + 60 + const withSpotifyRefreshToken = ({ 61 + user, 62 + ctx, 63 + }: { 64 + user: SelectUser; 65 + ctx: Context; 66 + }) => { 67 + return Effect.tryPromise({ 68 + try: () => 69 + ctx.db 70 + .select() 71 + .from(tables.spotifyTokens) 72 + .where(eq(tables.spotifyTokens.userId, user.id)) 73 + .execute() 74 + .then(([spotifyToken]) => 75 + decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY) 76 + ) 77 + .then((refreshToken) => ({ 78 + user, 79 + ctx, 80 + refreshToken, 81 + })), 82 + catch: (error) => 83 + new Error(`Failed to retrieve Spotify Refresh token: ${error}`), 84 + }); 85 + }; 86 + 87 + const withSpotifyToken = ({ 88 + refreshToken, 89 + ctx, 90 + }: { 91 + refreshToken: string; 92 + ctx: Context; 93 + }) => { 29 94 return Effect.tryPromise({ 30 - try: async () => {}, 95 + try: () => 96 + fetch("https://accounts.spotify.com/api/token", { 97 + method: "POST", 98 + headers: { 99 + "Content-Type": "application/x-www-form-urlencoded", 100 + }, 101 + body: new URLSearchParams({ 102 + grant_type: "refresh_token", 103 + refresh_token: refreshToken, 104 + client_id: env.SPOTIFY_CLIENT_ID, 105 + client_secret: env.SPOTIFY_CLIENT_SECRET, 106 + }), 107 + }) 108 + .then((res) => res.json()) 109 + .then((data) => data.access_token), 31 110 catch: (error) => new Error(`Failed to retrieve Spotify token: ${error}`), 32 111 }); 33 112 };
+82 -6
rockskyapi/rocksky-auth/src/xrpc/app/rocksky/spotify/previous.ts
··· 1 1 import { HandlerAuth } from "@atproto/xrpc-server"; 2 2 import { Context } from "context"; 3 + import { eq } from "drizzle-orm"; 3 4 import { Effect, pipe } from "effect"; 4 5 import { Server } from "lexicon"; 6 + import { QueryParams } from "lexicon/types/app/rocksky/spotify/previous"; 7 + import { decrypt } from "lib/crypto"; 8 + import { env } from "lib/env"; 9 + import tables from "schema"; 10 + import { SelectUser } from "schema/users"; 5 11 6 12 export default function (server: Server, ctx: Context) { 7 13 const previous = (params, auth: HandlerAuth) => 8 14 pipe( 9 15 { params, ctx, did: auth.credentials?.did }, 10 - withSpotifyToken, 16 + withUser, 17 + Effect.flatMap(withSpotifyRefreshToken), 18 + Effect.flatMap(withSpotifyToken), 11 19 Effect.flatMap(handlePrevious), 12 20 Effect.flatMap(presentation), 13 21 Effect.retry({ times: 3 }), ··· 25 33 }); 26 34 } 27 35 28 - const withSpotifyToken = () => { 36 + const withUser = ({ 37 + did, 38 + ctx, 39 + }: { 40 + params: QueryParams; 41 + did: string; 42 + ctx: Context; 43 + }) => { 29 44 return Effect.tryPromise({ 30 - try: async () => {}, 45 + try: () => 46 + ctx.db 47 + .select() 48 + .from(tables.users) 49 + .where(eq(tables.users.did, did)) 50 + .execute() 51 + .then(([user]) => ({ 52 + user, 53 + ctx, 54 + did, 55 + })), 56 + catch: (error) => new Error(`Failed to retrieve User: ${error}`), 57 + }); 58 + }; 59 + 60 + const withSpotifyRefreshToken = ({ 61 + user, 62 + ctx, 63 + }: { 64 + user: SelectUser; 65 + ctx: Context; 66 + }) => { 67 + return Effect.tryPromise({ 68 + try: () => 69 + ctx.db 70 + .select() 71 + .from(tables.spotifyTokens) 72 + .where(eq(tables.spotifyTokens.userId, user.id)) 73 + .execute() 74 + .then(([spotifyToken]) => 75 + decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY) 76 + ) 77 + .then((refreshToken) => ({ 78 + refreshToken, 79 + })), 80 + catch: (error) => 81 + new Error(`Failed to retrieve Spotify Refresh token: ${error}`), 82 + }); 83 + }; 84 + 85 + const withSpotifyToken = ({ refreshToken }: { refreshToken: string }) => { 86 + return Effect.tryPromise({ 87 + try: () => 88 + fetch("https://accounts.spotify.com/api/token", { 89 + method: "POST", 90 + headers: { 91 + "Content-Type": "application/x-www-form-urlencoded", 92 + }, 93 + body: new URLSearchParams({ 94 + grant_type: "refresh_token", 95 + refresh_token: refreshToken, 96 + client_id: env.SPOTIFY_CLIENT_ID, 97 + client_secret: env.SPOTIFY_CLIENT_SECRET, 98 + }), 99 + }) 100 + .then((res) => res.json()) 101 + .then((data) => data.access_token), 31 102 catch: (error) => new Error(`Failed to retrieve Spotify token: ${error}`), 32 103 }); 33 104 }; 34 105 35 - const handlePrevious = (params) => { 36 - // Logic to handle the previous action in Spotify 106 + const handlePrevious = (accessToken: string) => { 37 107 return Effect.tryPromise({ 38 - try: async () => ({}), 108 + try: () => 109 + fetch("https://api.spotify.com/v1/me/player/previous", { 110 + method: "POST", 111 + headers: { 112 + Authorization: `Bearer ${accessToken}`, 113 + }, 114 + }).then((res) => res.status), 39 115 catch: (error) => new Error(`Failed to handle previous action: ${error}`), 40 116 }); 41 117 };
+106 -7
rockskyapi/rocksky-auth/src/xrpc/app/rocksky/spotify/seek.ts
··· 1 1 import { HandlerAuth } from "@atproto/xrpc-server"; 2 2 import { Context } from "context"; 3 + import { eq } from "drizzle-orm"; 3 4 import { Effect, pipe } from "effect"; 4 5 import { Server } from "lexicon"; 6 + import { QueryParams } from "lexicon/types/app/rocksky/spotify/seek"; 7 + import { decrypt } from "lib/crypto"; 8 + import { env } from "lib/env"; 9 + import tables from "schema"; 10 + import { SelectUser } from "schema/users"; 5 11 6 12 export default function (server: Server, ctx: Context) { 7 13 const seek = (params, auth: HandlerAuth) => 8 14 pipe( 9 15 { params, ctx, did: auth.credentials?.did }, 10 - withSpotifyToken, 16 + withUser, 17 + Effect.flatMap(withSpotifyRefreshToken), 18 + Effect.flatMap(withSpotifyToken), 11 19 Effect.flatMap(handleSeek), 12 20 Effect.flatMap(presentation), 13 21 Effect.retry({ times: 3 }), ··· 25 33 }); 26 34 } 27 35 28 - const withSpotifyToken = () => { 36 + const withUser = ({ 37 + did, 38 + ctx, 39 + params, 40 + }: { 41 + params: QueryParams; 42 + did: string; 43 + ctx: Context; 44 + }) => { 45 + return Effect.tryPromise({ 46 + try: () => 47 + ctx.db 48 + .select() 49 + .from(tables.users) 50 + .where(eq(tables.users.did, did)) 51 + .execute() 52 + .then(([user]) => ({ 53 + user, 54 + ctx, 55 + params, 56 + })), 57 + catch: (error) => new Error(`Failed to retrieve User: ${error}`), 58 + }); 59 + }; 60 + 61 + const withSpotifyRefreshToken = ({ 62 + user, 63 + ctx, 64 + params, 65 + }: { 66 + user: SelectUser; 67 + ctx: Context; 68 + params: QueryParams; 69 + }) => { 70 + return Effect.tryPromise({ 71 + try: () => 72 + ctx.db 73 + .select() 74 + .from(tables.spotifyTokens) 75 + .where(eq(tables.spotifyTokens.userId, user.id)) 76 + .execute() 77 + .then(([spotifyToken]) => 78 + decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY) 79 + ) 80 + .then((refreshToken) => ({ 81 + refreshToken, 82 + params, 83 + })), 84 + catch: (error) => 85 + new Error(`Failed to retrieve Spotify Refresh token: ${error}`), 86 + }); 87 + }; 88 + 89 + const withSpotifyToken = ({ 90 + refreshToken, 91 + params, 92 + }: { 93 + refreshToken: string; 94 + params: QueryParams; 95 + }) => { 29 96 return Effect.tryPromise({ 30 - try: async () => {}, 97 + try: () => 98 + fetch("https://accounts.spotify.com/api/token", { 99 + method: "POST", 100 + headers: { 101 + "Content-Type": "application/x-www-form-urlencoded", 102 + }, 103 + body: new URLSearchParams({ 104 + grant_type: "refresh_token", 105 + refresh_token: refreshToken, 106 + client_id: env.SPOTIFY_CLIENT_ID, 107 + client_secret: env.SPOTIFY_CLIENT_SECRET, 108 + }), 109 + }) 110 + .then((res) => res.json()) 111 + .then((data) => ({ 112 + accessToken: data.access_token, 113 + position: params.position, 114 + })), 31 115 catch: (error) => new Error(`Failed to retrieve Spotify token: ${error}`), 32 116 }); 33 117 }; 34 118 35 - const handleSeek = (params) => { 36 - // Logic to handle the seek action in Spotify 119 + const handleSeek = ({ 120 + accessToken, 121 + position, 122 + }: { 123 + accessToken: string; 124 + position: number; 125 + }) => { 37 126 return Effect.tryPromise({ 38 - try: async () => {}, 39 - catch: (error) => new Error(`Failed to handle seek action: ${error}`), 127 + try: () => 128 + fetch( 129 + `https://api.spotify.com/v1/me/player/seek?position_ms=${position}`, 130 + { 131 + method: "PUT", 132 + headers: { 133 + Authorization: `Bearer ${accessToken}`, 134 + }, 135 + } 136 + ).then((res) => res.status), 137 + catch: (error) => new Error(`Failed to handle next action: ${error}`), 40 138 }); 41 139 }; 42 140 43 141 const presentation = (result) => { 44 142 // Logic to format the result for presentation 143 + console.log("Seek action result:", result); 45 144 return Effect.sync(() => ({})); 46 145 };