tangled
alpha
login
or
join now
t1c.dev
/
rocksky
forked from
rocksky.app/rocksky
2
fork
atom
A decentralized music tracking and discovery platform built on AT Protocol 🎵
2
fork
atom
overview
issues
pulls
pipelines
[spotify] restart thread on error
tsiry-sandratraina.com
9 months ago
2b57ca49
b72248dd
+406
-23
5 changed files
expand all
collapse all
unified
split
crates
spotify
src
main.rs
rockskyapi
rocksky-auth
src
xrpc
app
rocksky
spotify
pause.ts
play.ts
previous.ts
seek.ts
+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
61
+
let user_id = user.3.clone();
61
62
let stop_flag = Arc::new(AtomicBool::new(false));
62
63
let cache = cache.clone();
64
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
87
+
88
88
+
// If there's an error, publish a message to restart the thread
89
89
+
match rt.block_on(nc.publish("rocksky.spotify.user", user_id.into())) {
90
90
+
Ok(_) => {
91
91
+
println!(
92
92
+
"{} Published message to restart thread for user: {}",
93
93
+
format!("[{}]", email).bright_green(),
94
94
+
email.bright_green()
95
95
+
);
96
96
+
}
97
97
+
Err(e) => {
98
98
+
println!(
99
99
+
"{} Error publishing message to restart thread: {}",
100
100
+
format!("[{}]", email).bright_green(),
101
101
+
e.to_string().bright_red()
102
102
+
);
103
103
+
}
104
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
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
209
+
match rt.block_on(nc.publish("rocksky.spotify.user", user_id.into())) {
210
210
+
Ok(_) => {},
211
211
+
Err(e) => {
212
212
+
println!(
213
213
+
"{} Error publishing message to restart thread: {}",
214
214
+
format!("[{}]", email).bright_green(),
215
215
+
e.to_string().bright_red()
216
216
+
);
217
217
+
}
218
218
+
}
188
219
}
189
220
}
190
221
});
···
562
593
return Ok(None);
563
594
}
564
595
565
565
-
cache.setex(album_id, &data, 20)?;
596
596
+
match cache.setex(album_id, &data, 20) {
597
597
+
Ok(_) => {}
598
598
+
Err(e) => {
599
599
+
println!(
600
600
+
"{} redis error: {}",
601
601
+
format!("[{}]", album_id).bright_green(),
602
602
+
e.to_string().bright_red()
603
603
+
);
604
604
+
return Ok(None);
605
605
+
}
606
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
618
-
cache.setex(&format!("{}:tracks", album_id), &all_tracks_json, 20)?;
659
659
+
match cache.setex(&format!("{}:tracks", album_id), &all_tracks_json, 20) {
660
660
+
Ok(_) => {}
661
661
+
Err(e) => {
662
662
+
println!(
663
663
+
"{} redis error: {}",
664
664
+
format!("[{}]", album_id).bright_green(),
665
665
+
e.to_string().bright_red()
666
666
+
);
667
667
+
}
668
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
630
-
) -> Result<Vec<(String, String, String)>, Error> {
680
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
651
-
user_tokens.push((result.email.clone(), token, result.did.clone()));
701
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
3
+
import { eq } from "drizzle-orm";
3
4
import { Effect, pipe } from "effect";
4
5
import { Server } from "lexicon";
6
6
+
import { QueryParams } from "lexicon/types/app/rocksky/spotify/pause";
7
7
+
import { decrypt } from "lib/crypto";
8
8
+
import { env } from "lib/env";
9
9
+
import tables from "schema";
10
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
10
-
withSpotifyToken,
16
16
+
withUser,
17
17
+
Effect.flatMap(withSpotifyRefreshToken),
18
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
28
-
const withSpotifyToken = () => {
36
36
+
const withUser = ({
37
37
+
did,
38
38
+
ctx,
39
39
+
}: {
40
40
+
params: QueryParams;
41
41
+
did: string;
42
42
+
ctx: Context;
43
43
+
}) => {
44
44
+
return Effect.tryPromise({
45
45
+
try: () =>
46
46
+
ctx.db
47
47
+
.select()
48
48
+
.from(tables.users)
49
49
+
.where(eq(tables.users.did, did))
50
50
+
.execute()
51
51
+
.then(([user]) => ({
52
52
+
user,
53
53
+
ctx,
54
54
+
did,
55
55
+
})),
56
56
+
catch: (error) => new Error(`Failed to retrieve User: ${error}`),
57
57
+
});
58
58
+
};
59
59
+
60
60
+
const withSpotifyRefreshToken = ({
61
61
+
user,
62
62
+
ctx,
63
63
+
}: {
64
64
+
user: SelectUser;
65
65
+
ctx: Context;
66
66
+
}) => {
67
67
+
return Effect.tryPromise({
68
68
+
try: () =>
69
69
+
ctx.db
70
70
+
.select()
71
71
+
.from(tables.spotifyTokens)
72
72
+
.where(eq(tables.spotifyTokens.userId, user.id))
73
73
+
.execute()
74
74
+
.then(([spotifyToken]) =>
75
75
+
decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY)
76
76
+
)
77
77
+
.then((refreshToken) => ({
78
78
+
user,
79
79
+
ctx,
80
80
+
refreshToken,
81
81
+
})),
82
82
+
catch: (error) =>
83
83
+
new Error(`Failed to retrieve Spotify Refresh token: ${error}`),
84
84
+
});
85
85
+
};
86
86
+
87
87
+
const withSpotifyToken = ({
88
88
+
refreshToken,
89
89
+
ctx,
90
90
+
}: {
91
91
+
refreshToken: string;
92
92
+
ctx: Context;
93
93
+
}) => {
29
94
return Effect.tryPromise({
30
30
-
try: async () => {},
95
95
+
try: () =>
96
96
+
fetch("https://accounts.spotify.com/api/token", {
97
97
+
method: "POST",
98
98
+
headers: {
99
99
+
"Content-Type": "application/x-www-form-urlencoded",
100
100
+
},
101
101
+
body: new URLSearchParams({
102
102
+
grant_type: "refresh_token",
103
103
+
refresh_token: refreshToken,
104
104
+
client_id: env.SPOTIFY_CLIENT_ID,
105
105
+
client_secret: env.SPOTIFY_CLIENT_SECRET,
106
106
+
}),
107
107
+
})
108
108
+
.then((res) => res.json())
109
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
3
+
import { eq } from "drizzle-orm";
3
4
import { Effect, pipe } from "effect";
4
5
import { Server } from "lexicon";
6
6
+
import { QueryParams } from "lexicon/types/app/rocksky/spotify/play";
7
7
+
import { decrypt } from "lib/crypto";
8
8
+
import { env } from "lib/env";
9
9
+
import tables from "schema";
10
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
10
-
withSpotifyToken,
16
16
+
withUser,
17
17
+
Effect.flatMap(withSpotifyRefreshToken),
18
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
28
-
const withSpotifyToken = () => {
36
36
+
const withUser = ({
37
37
+
did,
38
38
+
ctx,
39
39
+
}: {
40
40
+
params: QueryParams;
41
41
+
did: string;
42
42
+
ctx: Context;
43
43
+
}) => {
44
44
+
return Effect.tryPromise({
45
45
+
try: () =>
46
46
+
ctx.db
47
47
+
.select()
48
48
+
.from(tables.users)
49
49
+
.where(eq(tables.users.did, did))
50
50
+
.execute()
51
51
+
.then(([user]) => ({
52
52
+
user,
53
53
+
ctx,
54
54
+
did,
55
55
+
})),
56
56
+
catch: (error) => new Error(`Failed to retrieve User: ${error}`),
57
57
+
});
58
58
+
};
59
59
+
60
60
+
const withSpotifyRefreshToken = ({
61
61
+
user,
62
62
+
ctx,
63
63
+
}: {
64
64
+
user: SelectUser;
65
65
+
ctx: Context;
66
66
+
}) => {
67
67
+
return Effect.tryPromise({
68
68
+
try: () =>
69
69
+
ctx.db
70
70
+
.select()
71
71
+
.from(tables.spotifyTokens)
72
72
+
.where(eq(tables.spotifyTokens.userId, user.id))
73
73
+
.execute()
74
74
+
.then(([spotifyToken]) =>
75
75
+
decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY)
76
76
+
)
77
77
+
.then((refreshToken) => ({
78
78
+
user,
79
79
+
ctx,
80
80
+
refreshToken,
81
81
+
})),
82
82
+
catch: (error) =>
83
83
+
new Error(`Failed to retrieve Spotify Refresh token: ${error}`),
84
84
+
});
85
85
+
};
86
86
+
87
87
+
const withSpotifyToken = ({
88
88
+
refreshToken,
89
89
+
ctx,
90
90
+
}: {
91
91
+
refreshToken: string;
92
92
+
ctx: Context;
93
93
+
}) => {
29
94
return Effect.tryPromise({
30
30
-
try: async () => {},
95
95
+
try: () =>
96
96
+
fetch("https://accounts.spotify.com/api/token", {
97
97
+
method: "POST",
98
98
+
headers: {
99
99
+
"Content-Type": "application/x-www-form-urlencoded",
100
100
+
},
101
101
+
body: new URLSearchParams({
102
102
+
grant_type: "refresh_token",
103
103
+
refresh_token: refreshToken,
104
104
+
client_id: env.SPOTIFY_CLIENT_ID,
105
105
+
client_secret: env.SPOTIFY_CLIENT_SECRET,
106
106
+
}),
107
107
+
})
108
108
+
.then((res) => res.json())
109
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
3
+
import { eq } from "drizzle-orm";
3
4
import { Effect, pipe } from "effect";
4
5
import { Server } from "lexicon";
6
6
+
import { QueryParams } from "lexicon/types/app/rocksky/spotify/previous";
7
7
+
import { decrypt } from "lib/crypto";
8
8
+
import { env } from "lib/env";
9
9
+
import tables from "schema";
10
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
10
-
withSpotifyToken,
16
16
+
withUser,
17
17
+
Effect.flatMap(withSpotifyRefreshToken),
18
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
28
-
const withSpotifyToken = () => {
36
36
+
const withUser = ({
37
37
+
did,
38
38
+
ctx,
39
39
+
}: {
40
40
+
params: QueryParams;
41
41
+
did: string;
42
42
+
ctx: Context;
43
43
+
}) => {
29
44
return Effect.tryPromise({
30
30
-
try: async () => {},
45
45
+
try: () =>
46
46
+
ctx.db
47
47
+
.select()
48
48
+
.from(tables.users)
49
49
+
.where(eq(tables.users.did, did))
50
50
+
.execute()
51
51
+
.then(([user]) => ({
52
52
+
user,
53
53
+
ctx,
54
54
+
did,
55
55
+
})),
56
56
+
catch: (error) => new Error(`Failed to retrieve User: ${error}`),
57
57
+
});
58
58
+
};
59
59
+
60
60
+
const withSpotifyRefreshToken = ({
61
61
+
user,
62
62
+
ctx,
63
63
+
}: {
64
64
+
user: SelectUser;
65
65
+
ctx: Context;
66
66
+
}) => {
67
67
+
return Effect.tryPromise({
68
68
+
try: () =>
69
69
+
ctx.db
70
70
+
.select()
71
71
+
.from(tables.spotifyTokens)
72
72
+
.where(eq(tables.spotifyTokens.userId, user.id))
73
73
+
.execute()
74
74
+
.then(([spotifyToken]) =>
75
75
+
decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY)
76
76
+
)
77
77
+
.then((refreshToken) => ({
78
78
+
refreshToken,
79
79
+
})),
80
80
+
catch: (error) =>
81
81
+
new Error(`Failed to retrieve Spotify Refresh token: ${error}`),
82
82
+
});
83
83
+
};
84
84
+
85
85
+
const withSpotifyToken = ({ refreshToken }: { refreshToken: string }) => {
86
86
+
return Effect.tryPromise({
87
87
+
try: () =>
88
88
+
fetch("https://accounts.spotify.com/api/token", {
89
89
+
method: "POST",
90
90
+
headers: {
91
91
+
"Content-Type": "application/x-www-form-urlencoded",
92
92
+
},
93
93
+
body: new URLSearchParams({
94
94
+
grant_type: "refresh_token",
95
95
+
refresh_token: refreshToken,
96
96
+
client_id: env.SPOTIFY_CLIENT_ID,
97
97
+
client_secret: env.SPOTIFY_CLIENT_SECRET,
98
98
+
}),
99
99
+
})
100
100
+
.then((res) => res.json())
101
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
35
-
const handlePrevious = (params) => {
36
36
-
// Logic to handle the previous action in Spotify
106
106
+
const handlePrevious = (accessToken: string) => {
37
107
return Effect.tryPromise({
38
38
-
try: async () => ({}),
108
108
+
try: () =>
109
109
+
fetch("https://api.spotify.com/v1/me/player/previous", {
110
110
+
method: "POST",
111
111
+
headers: {
112
112
+
Authorization: `Bearer ${accessToken}`,
113
113
+
},
114
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
3
+
import { eq } from "drizzle-orm";
3
4
import { Effect, pipe } from "effect";
4
5
import { Server } from "lexicon";
6
6
+
import { QueryParams } from "lexicon/types/app/rocksky/spotify/seek";
7
7
+
import { decrypt } from "lib/crypto";
8
8
+
import { env } from "lib/env";
9
9
+
import tables from "schema";
10
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
10
-
withSpotifyToken,
16
16
+
withUser,
17
17
+
Effect.flatMap(withSpotifyRefreshToken),
18
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
28
-
const withSpotifyToken = () => {
36
36
+
const withUser = ({
37
37
+
did,
38
38
+
ctx,
39
39
+
params,
40
40
+
}: {
41
41
+
params: QueryParams;
42
42
+
did: string;
43
43
+
ctx: Context;
44
44
+
}) => {
45
45
+
return Effect.tryPromise({
46
46
+
try: () =>
47
47
+
ctx.db
48
48
+
.select()
49
49
+
.from(tables.users)
50
50
+
.where(eq(tables.users.did, did))
51
51
+
.execute()
52
52
+
.then(([user]) => ({
53
53
+
user,
54
54
+
ctx,
55
55
+
params,
56
56
+
})),
57
57
+
catch: (error) => new Error(`Failed to retrieve User: ${error}`),
58
58
+
});
59
59
+
};
60
60
+
61
61
+
const withSpotifyRefreshToken = ({
62
62
+
user,
63
63
+
ctx,
64
64
+
params,
65
65
+
}: {
66
66
+
user: SelectUser;
67
67
+
ctx: Context;
68
68
+
params: QueryParams;
69
69
+
}) => {
70
70
+
return Effect.tryPromise({
71
71
+
try: () =>
72
72
+
ctx.db
73
73
+
.select()
74
74
+
.from(tables.spotifyTokens)
75
75
+
.where(eq(tables.spotifyTokens.userId, user.id))
76
76
+
.execute()
77
77
+
.then(([spotifyToken]) =>
78
78
+
decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY)
79
79
+
)
80
80
+
.then((refreshToken) => ({
81
81
+
refreshToken,
82
82
+
params,
83
83
+
})),
84
84
+
catch: (error) =>
85
85
+
new Error(`Failed to retrieve Spotify Refresh token: ${error}`),
86
86
+
});
87
87
+
};
88
88
+
89
89
+
const withSpotifyToken = ({
90
90
+
refreshToken,
91
91
+
params,
92
92
+
}: {
93
93
+
refreshToken: string;
94
94
+
params: QueryParams;
95
95
+
}) => {
29
96
return Effect.tryPromise({
30
30
-
try: async () => {},
97
97
+
try: () =>
98
98
+
fetch("https://accounts.spotify.com/api/token", {
99
99
+
method: "POST",
100
100
+
headers: {
101
101
+
"Content-Type": "application/x-www-form-urlencoded",
102
102
+
},
103
103
+
body: new URLSearchParams({
104
104
+
grant_type: "refresh_token",
105
105
+
refresh_token: refreshToken,
106
106
+
client_id: env.SPOTIFY_CLIENT_ID,
107
107
+
client_secret: env.SPOTIFY_CLIENT_SECRET,
108
108
+
}),
109
109
+
})
110
110
+
.then((res) => res.json())
111
111
+
.then((data) => ({
112
112
+
accessToken: data.access_token,
113
113
+
position: params.position,
114
114
+
})),
31
115
catch: (error) => new Error(`Failed to retrieve Spotify token: ${error}`),
32
116
});
33
117
};
34
118
35
35
-
const handleSeek = (params) => {
36
36
-
// Logic to handle the seek action in Spotify
119
119
+
const handleSeek = ({
120
120
+
accessToken,
121
121
+
position,
122
122
+
}: {
123
123
+
accessToken: string;
124
124
+
position: number;
125
125
+
}) => {
37
126
return Effect.tryPromise({
38
38
-
try: async () => {},
39
39
-
catch: (error) => new Error(`Failed to handle seek action: ${error}`),
127
127
+
try: () =>
128
128
+
fetch(
129
129
+
`https://api.spotify.com/v1/me/player/seek?position_ms=${position}`,
130
130
+
{
131
131
+
method: "PUT",
132
132
+
headers: {
133
133
+
Authorization: `Bearer ${accessToken}`,
134
134
+
},
135
135
+
}
136
136
+
).then((res) => res.status),
137
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
143
+
console.log("Seek action result:", result);
45
144
return Effect.sync(() => ({}));
46
145
};