A decentralized music tracking and discovery platform built on AT Protocol 馃幍 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
96
fork

Configure Feed

Select the types of activity you want to include in your feed.

at feat/pgpull 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;