forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
1import axios from "axios";
2import { useAtom } from "jotai";
3import _ from "lodash";
4import { useCallback, useEffect, useRef, useState } from "react";
5import { nowPlayingAtom } from "../../atoms/nowpaying";
6import { playerAtom } from "../../atoms/player";
7import { API_URL } from "../../consts";
8import useLike from "../../hooks/useLike";
9import useSpotify from "../../hooks/useSpotify";
10import StickyPlayer from "./StrickyPlayer";
11
12function StickyPlayerWithData() {
13 const [liked, setLiked] = useState<Record<string, boolean>>({});
14 const [nowPlaying, setNowPlaying] = useAtom(nowPlayingAtom);
15 const progressInterval = useRef<number | null>(null);
16 const lastFetchedRef = useRef(0);
17 const nowPlayingInterval = useRef<number | null>(null);
18 const socketRef = useRef<WebSocket | null>(null);
19 const heartbeatInterval = useRef<number | null>(null);
20 const { play, pause, next, previous, seek } = useSpotify();
21 const { like, unlike } = useLike();
22 const [player, setPlayer] = useAtom(playerAtom);
23 const nowPlayingRef = useRef(nowPlaying);
24 const playerRef = useRef(player);
25 const likedRef = useRef(liked);
26
27 const onLike = (uri: string) => {
28 setLiked({
29 ...liked,
30 [uri]: true,
31 });
32 like(uri);
33 setNowPlaying((prev) => {
34 if (!prev) {
35 return prev;
36 }
37 return {
38 ...prev,
39 liked: true,
40 };
41 });
42 };
43
44 const onDislike = (uri: string) => {
45 setLiked({
46 ...liked,
47 [uri]: false,
48 });
49 unlike(uri);
50 setNowPlaying((prev) => {
51 if (!prev) {
52 return prev;
53 }
54 return {
55 ...prev,
56 liked: false,
57 };
58 });
59 };
60
61 const onPlay = () => {
62 if (player === "rockbox" && socketRef.current) {
63 socketRef.current.send(
64 JSON.stringify({
65 type: "command",
66 action: "play",
67 token: localStorage.getItem("token"),
68 }),
69 );
70 return;
71 }
72 play();
73 };
74
75 const onPause = () => {
76 if (player === "rockbox" && socketRef.current) {
77 socketRef.current.send(
78 JSON.stringify({
79 type: "command",
80 action: "pause",
81 token: localStorage.getItem("token"),
82 }),
83 );
84 return;
85 }
86 pause();
87 };
88
89 const onNext = () => {
90 if (player === "rockbox" && socketRef.current) {
91 socketRef.current.send(
92 JSON.stringify({
93 type: "command",
94 action: "next",
95 token: localStorage.getItem("token"),
96 }),
97 );
98 return;
99 }
100 next();
101 };
102
103 const onPrevious = () => {
104 if (player === "rockbox" && socketRef.current) {
105 socketRef.current.send(
106 JSON.stringify({
107 type: "command",
108 action: "previous",
109 token: localStorage.getItem("token"),
110 }),
111 );
112 return;
113 }
114 previous();
115 };
116
117 const onSeek = (position: number) => {
118 if (player === "rockbox" && socketRef.current) {
119 socketRef.current.send(
120 JSON.stringify({
121 type: "command",
122 action: "seek",
123 token: localStorage.getItem("token"),
124 args: {
125 position,
126 },
127 }),
128 );
129 return;
130 }
131 seek(position);
132 };
133
134 const fetchCurrentlyPlaying = useCallback(async () => {
135 if (player === "rockbox") {
136 return;
137 }
138 const { data } = await axios.get(`${API_URL}/spotify/currently-playing`, {
139 headers: {
140 authorization: `Bearer ${localStorage.getItem("token")}`,
141 },
142 });
143 if (data.item) {
144 setNowPlaying({
145 title: data.item.name,
146 artist: data.item.artists[0].name,
147 artistUri: data.artistUri,
148 songUri: data.songUri,
149 albumUri: data.albumUri,
150 duration: data.item.duration_ms,
151 progress: data.progress_ms,
152 albumArt: _.get(data, "item.album.images.0.url"),
153 isPlaying: data.is_playing,
154 sha256: data.sha256,
155 liked:
156 likedRef.current[data.songUri] !== undefined
157 ? likedRef.current[data.songUri]
158 : data.liked,
159 });
160 setPlayer("spotify");
161 } else {
162 if (player === "spotify") {
163 setNowPlaying(null);
164 setPlayer(null);
165 }
166 }
167 lastFetchedRef.current = Date.now();
168 // eslint-disable-next-line react-hooks/exhaustive-deps
169 }, [setNowPlaying, player]);
170
171 const startProgressTracking = useCallback(() => {
172 if (progressInterval.current) {
173 clearInterval(progressInterval.current);
174 }
175
176 progressInterval.current = window.setInterval(() => {
177 setNowPlaying((prev) => {
178 if (!prev || !prev.duration) {
179 return prev;
180 }
181
182 if (prev.progress >= prev.duration) {
183 if (player === "spotify") {
184 setTimeout(fetchCurrentlyPlaying, 2000);
185 }
186 return prev;
187 }
188
189 if (prev.isPlaying) {
190 return {
191 ...prev,
192 progress: prev.progress + 100,
193 };
194 }
195
196 return prev;
197 });
198 }, 100);
199 // eslint-disable-next-line react-hooks/exhaustive-deps
200 }, [fetchCurrentlyPlaying, setNowPlaying]);
201
202 useEffect(() => {
203 startProgressTracking();
204
205 return () => {
206 if (progressInterval.current) {
207 clearInterval(progressInterval.current);
208 }
209 };
210 // eslint-disable-next-line react-hooks/exhaustive-deps
211 }, []);
212
213 useEffect(() => {
214 nowPlayingRef.current = nowPlaying;
215 playerRef.current = player;
216 likedRef.current = liked;
217 }, [nowPlaying, player, liked]);
218
219 useEffect(() => {
220 if (player === "rockbox") {
221 return;
222 }
223
224 if (nowPlayingInterval.current) {
225 clearInterval(nowPlayingInterval.current);
226 }
227 nowPlayingInterval.current = window.setInterval(() => {
228 fetchCurrentlyPlaying();
229 }, 15000);
230
231 fetchCurrentlyPlaying();
232
233 return () => {
234 if (nowPlayingInterval.current) {
235 clearInterval(nowPlayingInterval.current);
236 }
237 };
238 // eslint-disable-next-line react-hooks/exhaustive-deps
239 }, []);
240
241 useEffect(() => {
242 if (!localStorage.getItem("token")) {
243 return;
244 }
245 const ws = new WebSocket(`${API_URL.replace("http", "ws")}/ws`);
246 socketRef.current = ws;
247
248 ws.onopen = () => {
249 ws.send(
250 JSON.stringify({
251 type: "register",
252 clientName: "rocksky",
253 token: localStorage.getItem("token"),
254 }),
255 );
256
257 if (heartbeatInterval.current) {
258 clearInterval(heartbeatInterval.current);
259 }
260
261 heartbeatInterval.current = window.setInterval(() => {
262 ws.send(
263 JSON.stringify({
264 type: "heartbeat",
265 token: localStorage.getItem("token"),
266 }),
267 );
268 }, 3000);
269
270 ws.onmessage = (event) => {
271 if (playerRef.current !== "rockbox" && playerRef.current !== null) {
272 return;
273 }
274
275 const msg = JSON.parse(event.data);
276 if (msg.type === "message" && msg.data?.type === "track") {
277 if (
278 lastFetchedRef.current &&
279 Date.now() - lastFetchedRef.current < 3000
280 ) {
281 return;
282 }
283
284 if (
285 nowPlayingRef.current !== null &&
286 nowPlayingRef.current.isPlaying === undefined
287 ) {
288 return;
289 }
290
291 setNowPlaying({
292 ...(nowPlayingRef.current ? nowPlayingRef.current : {}),
293 title: msg.data.title,
294 artist: msg.data.album_artist || msg.data.artist,
295 artistUri: msg.data.artist_uri,
296 songUri: msg.data.song_uri,
297 albumUri: msg.data.album_uri,
298 duration: msg.data.length,
299 progress: msg.data.elapsed,
300 albumArt: _.get(msg, "data.album_art"),
301 isPlaying: !!nowPlayingRef.current?.isPlaying,
302 sha256: msg.data.sha256,
303 liked:
304 likedRef.current[msg.data.song_uri] !== undefined
305 ? likedRef.current[msg.data.song_uri]
306 : msg.data.liked,
307 });
308 setPlayer("rockbox");
309 lastFetchedRef.current = Date.now();
310 }
311
312 if (msg.data?.status === 0) {
313 setNowPlaying(null);
314 }
315
316 if (msg.data?.status === 1 && nowPlayingRef.current) {
317 setNowPlaying({
318 ...nowPlayingRef.current,
319 isPlaying: true,
320 });
321 }
322 if (
323 (msg.data?.status === 2 || msg.data?.status === 3) &&
324 nowPlayingRef.current
325 ) {
326 setNowPlaying({
327 ...nowPlayingRef.current,
328 isPlaying: false,
329 });
330 }
331 };
332
333 console.log(">> WebSocket connection opened");
334 };
335
336 return () => {
337 if (ws) {
338 if (heartbeatInterval.current) {
339 clearInterval(heartbeatInterval.current);
340 }
341 ws.close();
342 }
343 console.log(">> WebSocket connection closed");
344 };
345 }, []);
346
347 if (!nowPlaying) {
348 return <></>;
349 }
350
351 return (
352 <StickyPlayer
353 nowPlaying={nowPlaying}
354 onPlay={onPlay}
355 onPause={onPause}
356 onPrevious={onPrevious}
357 onNext={onNext}
358 onSpeaker={() => {}}
359 onEqualizer={() => {}}
360 onPlaylist={() => {}}
361 onSeek={onSeek}
362 isPlaying={nowPlaying.isPlaying}
363 onLike={onLike}
364 onDislike={onDislike}
365 />
366 );
367}
368
369export default StickyPlayerWithData;