diff --git a/README.md b/README.md index c080374a..489eba75 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Fill in the `.env` file in `apps/client` with the following: ```sh NEXT_PUBLIC_API_URL=http://localhost:8080 NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws +NEXT_PUBLIC_APPLE_MUSIC_DEVELOPER_TOKEN=your_apple_music_developer_token_here ``` Run the following commands to start the server and client: diff --git a/apps/client/next.config.ts b/apps/client/next.config.ts index c69d831d..bbeb9ea4 100644 --- a/apps/client/next.config.ts +++ b/apps/client/next.config.ts @@ -2,6 +2,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + env: { + NEXT_PUBLIC_APPLE_MUSIC_DEVELOPER_TOKEN: process.env.NEXT_PUBLIC_APPLE_MUSIC_DEVELOPER_TOKEN, + }, async rewrites() { return [ { diff --git a/apps/client/src/components/AppleMusicConnector.tsx b/apps/client/src/components/AppleMusicConnector.tsx new file mode 100644 index 00000000..e02813aa --- /dev/null +++ b/apps/client/src/components/AppleMusicConnector.tsx @@ -0,0 +1,166 @@ +import { useEffect, useState } from "react"; + +const APPLE_MUSIC_DEVELOPER_TOKEN = process.env.NEXT_PUBLIC_APPLE_MUSIC_DEVELOPER_TOKEN; + +// Dynamically load MusicKit JS from Apple CDN +function loadMusicKitScript(): Promise { + return new Promise((resolve, reject) => { + if ((window as any).MusicKit) { + resolve(); + return; + } + const script = document.createElement("script"); + script.src = "https://js-cdn.music.apple.com/musickit/v1/musickit.js"; + script.async = true; + script.onload = () => resolve(); + script.onerror = () => reject(new Error("Failed to load MusicKit JS")); + document.body.appendChild(script); + }); +} + +interface AppleMusicConnectorProps { + onTrackSelected?: (track: any) => void; +} + +export default function AppleMusicConnector({ onTrackSelected }: AppleMusicConnectorProps) { + const [isReady, setIsReady] = useState(false); + const [isAuthorized, setIsAuthorized] = useState(false); + const [userToken, setUserToken] = useState(null); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [selectedTrack, setSelectedTrack] = useState(null); + + useEffect(() => { + if (!APPLE_MUSIC_DEVELOPER_TOKEN) { + setError("Apple Music developer token is not set. Please add NEXT_PUBLIC_APPLE_MUSIC_DEVELOPER_TOKEN to your .env file."); + return; + } + loadMusicKitScript() + .then(() => { + (window as any).MusicKit.configure({ + developerToken: APPLE_MUSIC_DEVELOPER_TOKEN, + app: { + name: "beatsync", + build: "1.0.0", + }, + }); + setIsReady(true); + }) + .catch((err) => setError(err.message)); + }, []); + + const handleSignIn = async () => { + try { + const musicKit = (window as any).MusicKit.getInstance(); + const token = await musicKit.authorize(); + setUserToken(token); + setIsAuthorized(true); + } catch (err: any) { + setError(err.message || "Failed to authorize with Apple Music"); + } + }; + + const handleSearch = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); + setIsSearching(true); + setSearchResults([]); + setSelectedTrack(null); + try { + const musicKit = (window as any).MusicKit.getInstance(); + const result = await musicKit.api.search(searchQuery, { types: ["songs"], limit: 10 }); + const songs = result.songs?.data || []; + setSearchResults(songs); + } catch (err: any) { + setError(err.message || "Failed to search Apple Music"); + } finally { + setIsSearching(false); + } + }; + + const handleSelectTrack = (track: any) => { + setSelectedTrack(track); + if (onTrackSelected) { + onTrackSelected(track); + } + }; + + return ( +
+

Apple Music Integration

+ {!isReady && !error &&
Loading Apple Music SDK...
} + {error &&
Error: {error}
} + {isReady && !isAuthorized && !error && ( + + )} + {isAuthorized && !error && ( +
+
Signed in to Apple Music!
+ {/* Search Input */} +
+ setSearchQuery(e.target.value)} + placeholder="Search for songs, artists, albums..." + className="flex-1 px-2 py-1 rounded text-black" + disabled={isSearching} + /> + +
+ {/* Search Results */} + {searchResults.length > 0 && !selectedTrack && ( +
+ {searchResults.map((track) => ( +
+ {track.attributes.name} +
+
{track.attributes.name}
+
{track.attributes.artistName}
+
+ +
+ ))} +
+ )} + {/* Selected Track */} + {selectedTrack && ( +
+ {selectedTrack.attributes.name} +
+
{selectedTrack.attributes.name}
+
{selectedTrack.attributes.artistName}
+
Album: {selectedTrack.attributes.albumName}
+
+
+ )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/client/src/components/AudioUploaderMinimal.tsx b/apps/client/src/components/AudioUploaderMinimal.tsx index 8e4528c2..4f982060 100644 --- a/apps/client/src/components/AudioUploaderMinimal.tsx +++ b/apps/client/src/components/AudioUploaderMinimal.tsx @@ -1,5 +1,6 @@ "use client"; +import React from "react"; import { uploadAudioFile } from "@/lib/api"; import { cn, trimFileName } from "@/lib/utils"; import { useRoomStore } from "@/store/room"; @@ -7,13 +8,15 @@ import { CloudUpload, Plus } from "lucide-react"; import { usePostHog } from "posthog-js/react"; import { useState } from "react"; import { toast } from "sonner"; +import AppleMusicConnector from "./AppleMusicConnector"; -export const AudioUploaderMinimal = () => { +export const AudioUploaderMinimal = ({ setSelectedAppleMusicTrack, selectedAppleMusicTrack }: { setSelectedAppleMusicTrack?: (track: any) => void, selectedAppleMusicTrack?: any }) => { const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false); const [fileName, setFileName] = useState(null); const roomId = useRoomStore((state) => state.roomId); const posthog = usePostHog(); + const [showAppleMusic, setShowAppleMusic] = useState(false); const handleFileUpload = async (file: File) => { // Store file name for display @@ -63,6 +66,11 @@ export const AudioUploaderMinimal = () => { } }; + // Placeholder for Apple Music connection logic + const handleConnectAppleMusic = () => { + setShowAppleMusic(true); + }; + const onInputChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; @@ -98,53 +106,101 @@ export const AudioUploaderMinimal = () => { }; return ( -
-