diff --git a/bun.lockb b/bun.lockb index 8d1ea67..f70b49c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/app/api/spotify/download/route.ts b/src/app/api/spotify/download/route.ts new file mode 100644 index 0000000..a0d92e9 --- /dev/null +++ b/src/app/api/spotify/download/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(request: NextRequest) { + try { + const { url } = await request.json(); + + const response = await fetch( + "https://zylalabs.com/api/4117/spotify+track+download+api/4970/download", + { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.MUSIC_DOWNLOAD_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ url }), + }, + ); + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + return NextResponse.json( + { error: "Failed to download track" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/spotify/lyrics/route.ts b/src/app/api/spotify/lyrics/route.ts new file mode 100644 index 0000000..d98c599 --- /dev/null +++ b/src/app/api/spotify/lyrics/route.ts @@ -0,0 +1,83 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { name, artists } = body; + + const searchResponse = await fetch( + `https://api.genius.com/search?q=${encodeURIComponent( + `${name} ${artists[0].name}`, + )}`, + { + headers: { + Authorization: `Bearer ${process.env.GENIUS_ACCESS_TOKEN}`, + }, + }, + ); + + const searchData = await searchResponse.json(); + + if (!searchData.response.hits.length) { + return NextResponse.json({ error: "No lyrics found" }); + } + + const songHit = searchData.response.hits.find((hit: any) => { + const result = hit.result; + return ( + result.title.toLowerCase().includes(name.toLowerCase()) || + name.toLowerCase().includes(result.title.toLowerCase()) + ); + }); + + if (!songHit) { + return NextResponse.json({ error: "No matching song lyrics found" }); + } + + const hit = songHit.result; + + const lyricsResponse = await fetch(hit.url); + const html = await lyricsResponse.text(); + + const lyricsMatch = html.match( + /data-lyrics-container[^>]*>([\s\S]*?)<\/div>|class="lyrics"[^>]*>([\s\S]*?)<\/div>/g, + ); + + if (!lyricsMatch) { + return NextResponse.json({ + lyrics: `Unable to extract lyrics automatically.\nView lyrics at: ${hit.url}`, + title: hit.title, + artist: hit.primary_artist.name, + url: hit.url, + }); + } + + const lyrics = lyricsMatch + .join("\n") + .replace(//g, "\n") + .replace(/<[^>]*>/g, "") + .replace(/\n{3,}/g, "\n\n") + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/data-lyrics-container="true" class="Lyrics-sc-[^"]*">/g, "") + .replace(/\[Produced by[^\n]*\]/g, "") + .trim(); + + return NextResponse.json({ + lyrics, + title: hit.title, + artist: hit.primary_artist.name, + url: hit.url, + }); + } catch (error) { + console.error("Lyrics fetch error:", error); + return NextResponse.json( + { error: "Failed to fetch lyrics" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/spotify/search/route.ts b/src/app/api/spotify/search/route.ts new file mode 100644 index 0000000..fd97475 --- /dev/null +++ b/src/app/api/spotify/search/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from "next/server"; + +async function getSpotifyAccessToken() { + const client_id = process.env.SPOTIFY_CLIENT_ID!; + const client_secret = process.env.SPOTIFY_CLIENT_SECRET!; + + const response = await fetch("https://accounts.spotify.com/api/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from( + `${client_id}:${client_secret}`, + ).toString("base64")}`, + }, + body: new URLSearchParams({ + grant_type: "client_credentials", + }), + }); + + const data = await response.json(); + return data.access_token; +} + +export async function GET(request: NextRequest) { + try { + const query = request.nextUrl.searchParams.get("q"); + if (!query) { + return NextResponse.json( + { error: "No search query provided" }, + { status: 400 }, + ); + } + + const accessToken = await getSpotifyAccessToken(); + + const response = await fetch( + `https://api.spotify.com/v1/search?q=${encodeURIComponent( + query, + )}&type=track&limit=5`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + return NextResponse.json( + { error: "Failed to search tracks" }, + { status: 500 }, + ); + } +} diff --git a/src/app/spotify-search/page.tsx b/src/app/spotify-search/page.tsx new file mode 100644 index 0000000..a9def47 --- /dev/null +++ b/src/app/spotify-search/page.tsx @@ -0,0 +1,232 @@ +"use client"; + +import { useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Loader2, Download, Search, FileText } from "lucide-react"; + +interface Track { + id: string; + name: string; + artists: { name: string }[]; + external_urls: { spotify: string }; +} + +export default function SpotifySearch() { + const [searchQuery, setSearchQuery] = useState(""); + const [isSearching, setIsSearching] = useState(false); + const [tracks, setTracks] = useState([]); + const [loadingTrackId, setLoadingTrackId] = useState(""); + const [loadingLyricsId, setLoadingLyricsId] = useState(""); + const [lyricsData, setLyricsData] = useState<{ + lyrics: string; + title: string; + artist: string; + trackId: string; + } | null>(null); + const [error, setError] = useState(""); + + const handleSearch = async () => { + if (!searchQuery.trim()) return; + + try { + setIsSearching(true); + setError(""); + setTracks([]); + + const response = await fetch( + `/api/spotify/search?q=${encodeURIComponent(searchQuery)}`, + ); + const data = await response.json(); + + if (data.error) { + throw new Error(data.error); + } + + setTracks(data.tracks?.items || []); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to search for tracks", + ); + } finally { + setIsSearching(false); + } + }; + + const handleViewMp3 = async (track: Track) => { + try { + setLoadingTrackId(track.id); + setError(""); + + console.log("Spotify URL:", track.external_urls.spotify); + + const response = await fetch("/api/spotify/download", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ url: track.external_urls.spotify }), + }); + + const data = await response.json(); + + if (data.error) { + throw new Error(data.error); + } + + console.log("Media URLs response:", data.medias); + + // Redirect to the first media URL if available + if (data.medias?.[0]?.url) { + console.log("Selected media URL:", data.medias[0].url); + window.open(data.medias[0].url, "_blank"); + } else { + throw new Error("No media URL found"); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to get media URL"); + } finally { + setLoadingTrackId(""); + } + }; + + const handleViewLyrics = async (track: Track) => { + try { + setLoadingLyricsId(track.id); + setError(""); + + const response = await fetch("/api/spotify/lyrics", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: track.name, + artists: track.artists, + }), + }); + + const data = await response.json(); + + if (data.error) { + throw new Error(data.error); + } + + setLyricsData({ + ...data, + trackId: track.id, + }); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch lyrics"); + } finally { + setLoadingLyricsId(""); + } + }; + + const handleCloseLyrics = (trackId: string) => { + if (lyricsData?.trackId === trackId) { + setLyricsData(null); + } + }; + + return ( +
+

Spotify Track Downloader

+ +
+
+ +
+ setSearchQuery(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSearch()} + /> + +
+
+ + {tracks.length > 0 && ( +
+

Search Results:

+
+ {tracks.map((track) => ( +
+
+
+

{track.name}

+

+ {track.artists.map((a) => a.name).join(", ")} +

+
+
+ + +
+
+ {lyricsData && lyricsData.trackId === track.id && ( +
+
+

+ {lyricsData.title} - {lyricsData.artist} +

+ +
+
+                        {lyricsData.lyrics}
+                      
+
+ )} +
+ ))} +
+
+ )} + + {error &&

{error}

} +
+
+ ); +}