A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 265 lines 8.3 kB view raw
1import styled from "@emotion/styled"; 2import { Link as DefaultLink } from "@tanstack/react-router"; 3import axios from "axios"; 4import { ProgressBar } from "baseui/progress-bar"; 5import { LabelXSmall } from "baseui/typography"; 6import { useAtom, useAtomValue } from "jotai"; 7import _ from "lodash"; 8import { useCallback, useEffect, useRef } from "react"; 9import { playerAtom } from "../../../atoms/player"; 10import { userNowPlayingAtom } from "../../../atoms/userNowplaying"; 11import { API_URL } from "../../../consts"; 12import { useTimeFormat } from "../../../hooks/useFormat"; 13import styles from "./styles"; 14 15const Cover = styled.img` 16 width: 54px; 17 height: 54px; 18 margin-right: 16px; 19 border-radius: 5px; 20`; 21 22const Link = styled(DefaultLink)` 23 text-decoration: none; 24 &:hover { 25 text-decoration: underline; 26 } 27`; 28 29type NowPlayingProps = { 30 did: string; 31}; 32 33function NowPlaying({ did }: NowPlayingProps) { 34 const { formatTime } = useTimeFormat(); 35 const progressInterval = useRef<number | null>(null); 36 const lastFetchedRef = useRef(0); 37 const nowPlayingInterval = useRef<number | null>(null); 38 const [nowPlaying, setNowPlaying] = useAtom(userNowPlayingAtom); 39 const player = useAtomValue(playerAtom); 40 41 const fetchCurrentlyPlaying = useCallback(async () => { 42 if (player === "rockbox" || player === null) { 43 const [rockbox, spotify] = await Promise.all([ 44 axios.get(`${API_URL}/now-playing`, { 45 headers: { 46 authorization: `Bearer ${localStorage.getItem("token")}`, 47 }, 48 params: { 49 did, 50 }, 51 }), 52 axios.get(`${API_URL}/spotify/currently-playing`, { 53 headers: { 54 authorization: `Bearer ${localStorage.getItem("token")}`, 55 }, 56 params: { 57 did, 58 }, 59 }), 60 ]); 61 62 if (rockbox.data.title) { 63 setNowPlaying({ 64 ...nowPlaying, 65 [did]: { 66 title: rockbox.data.title, 67 artist: rockbox.data.album_artist || rockbox.data.artist, 68 artistUri: rockbox.data.artist_uri, 69 songUri: rockbox.data.song_uri, 70 albumUri: rockbox.data.album_uri, 71 duration: rockbox.data.length, 72 progress: rockbox.data.elapsed, 73 albumArt: _.get(rockbox.data, "album_art"), 74 isPlaying: rockbox.data.is_playing, 75 }, 76 }); 77 } else { 78 if (!spotify.data.item) { 79 setNowPlaying({ 80 ...nowPlaying, 81 [did]: null, 82 }); 83 } 84 } 85 86 if (rockbox.data.title) { 87 return; 88 } 89 } 90 const { data } = await axios.get(`${API_URL}/spotify/currently-playing`, { 91 headers: { 92 authorization: `Bearer ${localStorage.getItem("token")}`, 93 }, 94 params: { 95 did, 96 }, 97 }); 98 if (data.item) { 99 setNowPlaying({ 100 ...nowPlaying, 101 [did]: { 102 title: data.item.name, 103 artist: data.item.artists[0].name, 104 artistUri: data.artistUri, 105 songUri: data.songUri, 106 albumUri: data.albumUri, 107 duration: data.item.duration_ms, 108 progress: data.progress_ms, 109 albumArt: _.get(data, "item.album.images.0.url"), 110 isPlaying: data.is_playing, 111 }, 112 }); 113 } else { 114 setNowPlaying({ 115 ...nowPlaying, 116 [did]: null, 117 }); 118 } 119 lastFetchedRef.current = Date.now(); 120 // eslint-disable-next-line react-hooks/exhaustive-deps 121 }, [setNowPlaying, did, player]); 122 123 const startProgressTracking = useCallback(() => { 124 if (progressInterval.current) { 125 clearInterval(progressInterval.current); 126 } 127 128 progressInterval.current = window.setInterval(() => { 129 setNowPlaying((prev) => { 130 if (!prev[did] || !prev[did].duration) { 131 return prev; 132 } 133 134 if (prev[did].progress >= prev[did].duration) { 135 setTimeout(fetchCurrentlyPlaying, 2000); 136 return prev; 137 } 138 139 if (prev[did].isPlaying) { 140 const progress = prev[did].progress + 100; 141 return { 142 ...prev, 143 [did]: { 144 ...prev[did], 145 progress, 146 }, 147 }; 148 } 149 150 return prev; 151 }); 152 }, 100); 153 }, [fetchCurrentlyPlaying, setNowPlaying, did]); 154 155 useEffect(() => { 156 startProgressTracking(); 157 158 return () => { 159 if (progressInterval.current) { 160 clearInterval(progressInterval.current); 161 } 162 }; 163 // eslint-disable-next-line react-hooks/exhaustive-deps 164 }, []); 165 166 useEffect(() => { 167 if (nowPlayingInterval.current) { 168 clearInterval(nowPlayingInterval.current); 169 } 170 nowPlayingInterval.current = window.setInterval(() => { 171 fetchCurrentlyPlaying(); 172 }, 15000); 173 174 fetchCurrentlyPlaying(); 175 176 return () => { 177 if (nowPlayingInterval.current) { 178 clearInterval(nowPlayingInterval.current); 179 } 180 }; 181 // eslint-disable-next-line react-hooks/exhaustive-deps 182 }, []); 183 184 return ( 185 <> 186 {!!nowPlaying[did]?.duration && ( 187 <> 188 <div className="flex flex-row items-center mt-[25px]"> 189 {!!nowPlaying[did]?.albumUri && ( 190 <Link 191 to={`/${nowPlaying[did]?.albumUri?.split("at://")[1].replace("app.rocksky.", "")}`} 192 > 193 <Cover src={nowPlaying[did]?.albumArt} /> 194 </Link> 195 )} 196 {!nowPlaying[did]?.albumUri && ( 197 <Cover src={nowPlaying[did]?.albumArt} /> 198 )} 199 <div className="max-w-[316px] overflow-hidden"> 200 <div className="max-w-[316px] overflow-hidden truncate"> 201 {nowPlaying[did]?.songUri && ( 202 <Link 203 to={`/${nowPlaying[did]?.songUri?.split("at://")[1].replace("app.rocksky.", "")}`} 204 className="font-semibold truncate whitespace-nowrap text-[var(--color-text)]" 205 > 206 {nowPlaying[did]?.title} 207 </Link> 208 )} 209 {!nowPlaying[did]?.songUri && ( 210 <div className="font-semibold truncate whitespace-nowrap text-[var(--color-text)]"> 211 {nowPlaying[did]?.title} 212 </div> 213 )} 214 </div> 215 <div className="max-w-[316px] overflow-hidden truncate"> 216 {!!nowPlaying[did]?.artistUri?.split("at://")[1] && ( 217 <Link 218 to={`/${nowPlaying[did]?.artistUri?.split("at://")[1].replace("app.rocksky.", "")}`} 219 className="text-[var(--color-text-muted)] font-semibold truncate whitespace-nowrap text-sm" 220 style={{ color: "var(--color-text-muted)" }} 221 > 222 {nowPlaying[did]?.artist} 223 </Link> 224 )} 225 {!nowPlaying[did]?.artistUri?.split("at://")[1] && ( 226 <div 227 className="text-[var(--color-text-muted)] font-semibold truncate whitespace-nowrap text-sm" 228 style={{ color: "var(--color-text-muted)" }} 229 > 230 {nowPlaying[did]?.artist} 231 </div> 232 )} 233 </div> 234 </div> 235 </div> 236 <div className="mt-[0px] flex flex-row items-center"> 237 <div> 238 <LabelXSmall className="!text-[var(--color-text-muted)]"> 239 {formatTime(nowPlaying[did]?.progress || 0)} 240 </LabelXSmall> 241 </div> 242 <div className="flex-1 ml-[10px] mr-[10px]"> 243 <ProgressBar 244 value={ 245 nowPlaying[did]?.progress && nowPlaying[did]?.duration 246 ? (nowPlaying[did].progress / nowPlaying[did].duration) * 247 100 248 : 0 249 } 250 overrides={styles.Progressbar} 251 /> 252 </div> 253 <div> 254 <LabelXSmall className="!text-[var(--color-text-muted)]"> 255 {formatTime(nowPlaying[did]?.duration || 0)} 256 </LabelXSmall> 257 </div> 258 </div> 259 </> 260 )} 261 </> 262 ); 263} 264 265export default NowPlaying;