A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at feat/feed-generator 369 lines 9.4 kB view raw
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;