From 86f8c9edf79a36bb2ce9f746e4eb540bbcc942a1 Mon Sep 17 00:00:00 2001 From: Michael Tikhonovsky Date: Sun, 23 Feb 2025 19:27:10 -0500 Subject: [PATCH] spotify mp3 and lyrics page --- bun.lockb | Bin 429659 -> 429627 bytes src/app/api/spotify/download/route.ts | 27 +++ src/app/api/spotify/lyrics/route.ts | 83 +++++++++ src/app/api/spotify/search/route.ts | 55 ++++++ src/app/spotify-search/page.tsx | 232 ++++++++++++++++++++++++++ 5 files changed, 397 insertions(+) create mode 100644 src/app/api/spotify/download/route.ts create mode 100644 src/app/api/spotify/lyrics/route.ts create mode 100644 src/app/api/spotify/search/route.ts create mode 100644 src/app/spotify-search/page.tsx diff --git a/bun.lockb b/bun.lockb index 8d1ea67b9b7ef1d96e8f52b964849d7d58405ceb..f70b49c8b34aa990cbbb5cb671f47bbc925b57b5 100755 GIT binary patch delta 4070 zcmXw+32;@_8OPsyCl5$MP%vN;BtSqG6CmtTBt=m`Ac=xVD?32~A*`}5c?p_Qi?Z3y zQ3qLrfLbk1YdSEaU`3_1tJqcrWfLI@AqjyHmfGJJ{+`Ki?stFhyZ4@Z)|;=lBpuw6 zv@t9AW>I!=cCa`*^oMM>_+&8NA35p9L>dQfmzgdQ2oi?i4!K!4{u*d5m#@4i&g13g z$b~s?DK}TH5$EZ0^W^NAy>GinV7_D%L|-K?kc;NLzuZE(7|wqp_l#UD=Y!=I$u;Ht zA-TnJahwmATY?J(B7s0N#7N0yO0*VuQf@hpzrdX^UT&50tW_q-t(I%Sd4ko5QXqF1 z@i$g;N<_|DFBnl`p`dH$91Pu&DK7>1pv#Iup+z{f= zm1oUs*FFqy;B5LGm3xG^L=XA1+)!K#oXx7g$PKgc*Mz&YZ*4;Se`VEu22Suh=G)9T-H*~D?W;eX{G z!!?upO73yoW(~sQIBS{FFp=2G368cujDg9-Hcn1E`ri_tfX9$FP|k2PHx|aoot3jC z`3Yhx-^q<54iVdcI>*sox(UjgPu$vSMM=V0xWL~3Wo~G-Z!Niy*gg&HVfWzp3p@iSxWaDSPI-%nPsz2H zTZ~(%=1-AZf@`A(caU3(OU5;{e@Dq>$hJtU>AiBxiQDVqPB`n56_6s=MR_Z61!|eD za;tE*L)Z)5C%2mT9M{?lrA9Hi?LGz2i@<7{rbMe*cbrwAn_MArN1RolyWC(K`Cc_V^a07#xzPQ2-OPP$B0#m`JK@Ozygf!>|-DCZMN|)tN zUUKm<_LcNQLlt~RmD?H~PL)UaFRR_K((@IrX)xOM?47X7-@52hTP`QG9oBYK+d)@D zfge=qa-;jRFx?M{KJJqH+}B=mX)O-XPlw?{*a^Er{;Mk2Jua7cCgefB4_CX+&88Ag z1KXvtLO!e7%^3X}wb=@`BW;HruoHGc2^7L=IL|Al^J+aI8NTKo-}6?{#4!*HeaY(= z^vShuQus+4ew;6=b*+P6_*ZJ(_~x_tH4pM(9?bX4uegD}V~s0m{!8BRc~}PD2EET2$gUNszQvWYJ!@Ouc>uUWxPl2OW}|3d)Ny_PyiQbr!pvqeVkvUjVhrA zn$kuCf_}+Wml3|g{l@tHS6z-DRp-+Ep)2m;(T(Zb1H8pMV4oWAf~|B@VH!+_8DI-o z9?XV(m;-ZRKG>o)7REtc$hW9-DLoHUkq_ZdZ~zWMF!FtWehVK2 z{pM?~SMtl&&HQek&9|^R(icha+?tR7s?YpW;tdF_wIwjBMd@@W0`^0GuBZ-GXlc3M4cSHLw z=$U4{=Q5Xm`HbC*E*v zTiX8`zlII)EZD}f$Yu0W+@W<^TWy delta 4200 zcmXw+33L|K8HVT2O&|+@n8f=ModvQYp)Ap z1QhhNQfxI=juy05miBZ(YtK=31VR$_Bm}VS^CfTZ$#dqLC*Pg9bLTGe@7tR2-PVLn zqeDjuG7B?9g_#i_X1ctSp%nk+NjE0kG^i`tE(k(|5zt(2t{jJ;g4=E+5I zUSDp$Ts_Y3m0KWZIsNKfI9Mndjrge&7sbg0qKc03+p| zQeH!O(|NTn90ZLx8E=O2G?KrdF-$Q*S+2@Wh^N+?AXp)1y=S>rR;q3@;!1lOO1|=b zLVVh~f)d94zn9O1B?~02o6pKUBNvaWuYN7m1zX~_DepPuwZgqA_q_62ak|`Ax$d~(a&OpRu($`@W>na?DMr{x?Fm2X z>3)N=>1k80k4+28`*OXByU6X4OT%@Q`z?;Ypbw z*Oz#f@;;WcIlRYOMfro=eZ+6deS+gJ=nplT5uYm0ruv=epsxQvL)x_mKu9gzuf(6> zqHs3V4k&LRakTQjP~OjRF>-&ByC2s;?n}8rxN&-rKim7a#y$Y0YT;K(9E`gtX9LRe zhCrFzA)H<3K`6)B96uuW5OEI9R?@%94JDq9vnl>JxncJF4Rt{qp?1M^Xe4)3Za8j> zX2dr*Ys?5JWqjBy_?O&B;%US-@V=EBMf^Cim49=zWjX^MCJx(ZJf_6a#AArf9hb`_ z9!qTHgxtf#5yUoVPja+IKLU|*rL}`C)@aY4{6h-Ow!iHPH7{znAn!>SsdHSEg^mqXHTAp<1cs;j!}1^z5n(~ zdM(sSbRmT=E zn`(W)HlF&vX}L>kZEKwkAzSJPf-U9NadEzXxf{^10bvF-ghoEE+zm@;OlX5)I@ovF z444VCf|9gLu2Cpv2e-0sob7)2WtZB*zC`TH!#*_Z!(utC_C=RnR(vYETsPWhX z+Aqom=7~xCfLq88b*ZtimPr;r)~7&8(=%>PS_4RpcvkQHL%)O zRJdUg7Xm;2x_hVTMjpv_i1kne&-;cqTxzpkw6!<1gYUS*`B>lgh8qzZ!+9*k#rgaj zE;&4vcV(MI24upcFcL^35<;KFc~^?V_WMF!$0Au-+RqX@hO#VV)qjM z_c43~zlYsW0QoQ%av&Gx!y=z}-3^E>WyD;BvcPYtazn%K@~;XmdkL;VeO`Y9`pCX* z>|16J?1c|t0oWou17?CPs`i^F2XbLH%z=5Z5Ns_S2NR$__o?O)D`6v#wF$C9fj?5| zdi%&KH{Exuc6Y^pLfSs~6b``WP#E$vt6ZADQ|($!x=tI{BR7J5yL|}u^Cu4q!G0J# z3+uqX-`B&7umLu~X1GZkYG5;M$mTk^99R0))vklzS?vZ5zJWUde}qXe5bgn67284+ zc!l9+E8V*PYTVEVHc)RZtb?WS7jo>Q$QH=2VISE370rA6jN@*w z4(+#iaar4oZSS>hH4C!+p<6DsQvo$@(Yk|RJER@o$6cbDZFT~do@sP!_~ z-etdE?f1<_|IlqWB{7REtGS@+2JOy_1mi}F8H%lU1OAY&7xssHNA%FO`OK8~> zZ*+ll+v_P!x<>k>GS|K&<&HbpEoPAIHox!Y 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}

} +
+
+ ); +}