Skip to content

Commit 051517b

Browse files
committed
feat: add dj support
1 parent f16a24a commit 051517b

File tree

4 files changed

+129
-1
lines changed

4 files changed

+129
-1
lines changed

src/App.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useNetwork } from "./hooks/useNetwork";
1313
import { useGradientState } from "./hooks/useGradientState";
1414
import { useSpotifyData } from "./hooks/useSpotifyData";
1515
import { useSpotifyPlayerState } from "./hooks/useSpotifyPlayerState";
16+
import { useSpotifyPlayerControls } from "./hooks/useSpotifyPlayerControls";
1617
import { useBluetooth } from "./hooks/useBluetooth";
1718

1819
function App() {
@@ -21,6 +22,7 @@ function App() {
2122
return localStorage.getItem("lastActiveSection") || "recents";
2223
});
2324
const [viewingContent, setViewingContent] = useState(null);
25+
const playerControls = useSpotifyPlayerControls();
2426

2527
const { isAuthenticated, accessToken, isLoading: authIsLoading } = useAuth();
2628
const { isConnected, showNoNetwork, checkNetwork } = useNetwork();
@@ -33,7 +35,7 @@ function App() {
3335
denyPairing,
3436
setDiscoverable,
3537
disconnectDevice,
36-
enableNetworking
38+
enableNetworking,
3739
} = useBluetooth();
3840

3941
const {
@@ -204,6 +206,7 @@ function App() {
204206
currentlyPlayingTrackUri={currentPlayback?.item?.uri}
205207
radioMixes={radioMixes}
206208
updateGradientColors={updateGradientColors}
209+
playerControls={playerControls}
207210
/>
208211
);
209212
} else {

src/components/player/NowPlaying.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ForwardIcon,
1515
MenuIcon,
1616
LyricsIcon,
17+
DJIcon,
1718
} from "../common/icons";
1819

1920
const NowPlaying = ({ accessToken, currentPlayback, onClose }) => {
@@ -25,6 +26,8 @@ const NowPlaying = ({ accessToken, currentPlayback, onClose }) => {
2526
const lastUpdateTimeRef = useRef(Date.now());
2627
const currentTrackIdRef = useRef(null);
2728
const containerRef = useRef(null);
29+
const isDJPlaylist =
30+
currentPlayback?.context?.uri === "spotify:playlist:37i9dQZF1EYkqdzj48dyYq";
2831
const { updateGradientColors } = useGradientState();
2932

3033
const {
@@ -36,6 +39,7 @@ const NowPlaying = ({ accessToken, currentPlayback, onClose }) => {
3639
checkIsTrackLiked,
3740
likeTrack,
3841
unlikeTrack,
42+
sendDJSignal,
3943
} = useSpotifyPlayerControls(accessToken);
4044

4145
const handlePlayPause = async () => {
@@ -325,6 +329,11 @@ const NowPlaying = ({ accessToken, currentPlayback, onClose }) => {
325329
</div>
326330

327331
<div className="flex items-center">
332+
{isDJPlaylist && (
333+
<div onClick={sendDJSignal}>
334+
<DJIcon className="w-14 h-14 fill-white/60 mr-4 mb-1" />
335+
</div>
336+
)}
328337
<Menu as="div" className="relative inline-block text-left">
329338
<MenuButton className="focus:outline-none">
330339
<MenuIcon className="w-14 h-14 fill-white/60" />

src/hooks/useSpotifyPlayerControls.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,108 @@ export function useSpotifyPlayerControls(accessToken) {
447447
[accessToken]
448448
);
449449

450+
const playDJMix = useCallback(async () => {
451+
if (!accessToken) return false;
452+
453+
try {
454+
setIsLoading(true);
455+
setError(null);
456+
457+
const deviceResponse = await fetch(
458+
"https://api.spotify.com/v1/me/player",
459+
{
460+
headers: {
461+
Authorization: `Bearer ${accessToken}`,
462+
},
463+
}
464+
);
465+
466+
let deviceId = null;
467+
if (deviceResponse.status !== 204) {
468+
const deviceData = await deviceResponse.json();
469+
deviceId = deviceData.device?.id;
470+
}
471+
472+
const response = await fetch(
473+
`https://gue1-spclient.spotify.com/connect-state/v1/player/command/from/${deviceId}/to/${deviceId}`,
474+
{
475+
method: "POST",
476+
headers: {
477+
"accept-language": "en",
478+
authorization: `Bearer ${accessToken}`,
479+
"content-type": "application/x-www-form-urlencoded",
480+
},
481+
body: '{"command": {"endpoint": "play", "context": {"entity_uri": "spotify:playlist:37i9dQZF1EYkqdzj48dyYq", "uri": "spotify:playlist:37i9dQZF1EYkqdzj48dyYq", "url": "hm:\\/\\/lexicon-session-provider\\/context-resolve\\/v2\\/session?contextUri=spotify:playlist:37i9dQZF1EYkqdzj48dyYq"}}}',
482+
}
483+
);
484+
485+
if (!response.ok) {
486+
throw new Error(`HTTP error! status: ${response.status}`);
487+
}
488+
489+
return true;
490+
} catch (err) {
491+
console.error("Error playing DJ mix:", err);
492+
setError(err.message);
493+
return false;
494+
} finally {
495+
setIsLoading(false);
496+
}
497+
}, [accessToken]);
498+
499+
const sendDJSignal = useCallback(async () => {
500+
if (!accessToken) return false;
501+
502+
try {
503+
setIsLoading(true);
504+
setError(null);
505+
506+
const deviceResponse = await fetch(
507+
"https://api.spotify.com/v1/me/player",
508+
{
509+
headers: {
510+
Authorization: `Bearer ${accessToken}`,
511+
},
512+
}
513+
);
514+
515+
let deviceId = null;
516+
if (deviceResponse.status !== 204) {
517+
const deviceData = await deviceResponse.json();
518+
deviceId = deviceData.device?.id;
519+
}
520+
521+
if (!deviceId) {
522+
throw new Error("No active device found");
523+
}
524+
525+
const response = await fetch(
526+
`https://gue1-spclient.spotify.com/connect-state/v1/player/command/from/${deviceId}/to/${deviceId}`,
527+
{
528+
method: "POST",
529+
headers: {
530+
"accept-language": "en",
531+
authorization: `Bearer ${accessToken}`,
532+
"content-type": "application/x-www-form-urlencoded",
533+
},
534+
body: '{"command": {"endpoint": "signal", "signal_id": "jump"}}',
535+
}
536+
);
537+
538+
if (!response.ok) {
539+
throw new Error(`HTTP error! status: ${response.status}`);
540+
}
541+
542+
return true;
543+
} catch (err) {
544+
console.error("Error sending DJ signal:", err);
545+
setError(err.message);
546+
return false;
547+
} finally {
548+
setIsLoading(false);
549+
}
550+
}, [accessToken]);
551+
450552
return {
451553
playTrack,
452554
pausePlayback,
@@ -459,6 +561,8 @@ export function useSpotifyPlayerControls(accessToken) {
459561
checkIsTrackLiked,
460562
likeTrack,
461563
unlikeTrack,
564+
playDJMix,
565+
sendDJSignal,
462566
isLoading,
463567
error,
464568
};

src/pages/Home.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import HorizontalScroll from "../components/common/navigation/HorizontalScroll";
44
import Redirect from "../components/common/navigation/Redirect";
55
import { useGradientState } from "../hooks/useGradientState";
66
import { useNavigation } from "../hooks/useNavigation";
7+
import { useSpotifyPlayerControls } from "../hooks/useSpotifyPlayerControls";
78

89
export default function Home({
910
accessToken,
@@ -26,6 +27,7 @@ export default function Home({
2627
const hasScrolledToCurrentAlbumRef = useRef(false);
2728
const itemWidth = 290;
2829
const [newAlbumAdded, setNewAlbumAdded] = useState(false);
30+
const { playDJMix } = useSpotifyPlayerControls(accessToken);
2931

3032
const { scrollByAmount } = useNavigation({
3133
containerRef: scrollContainerRef,
@@ -511,6 +513,16 @@ export default function Home({
511513
<div
512514
className="mt-10 aspect-square rounded-[12px] drop-shadow-[0_8px_5px_rgba(0,0,0,0.25)] bg-white/10 cursor-pointer"
513515
style={{ width: 280, height: 280 }}
516+
onClick={() =>
517+
playDJMix().then((success) => {
518+
if (success) {
519+
setTimeout(() => {
520+
refreshPlaybackState();
521+
setActiveSection("nowPlaying");
522+
}, 500);
523+
}
524+
})
525+
}
514526
>
515527
<img
516528
src="/images/radio-cover/dj.webp"

0 commit comments

Comments
 (0)