
Kaynak: Dev.to · 12 dk okuma · Yazar: Leo Garcez
Eu queria digitar “noite chuvosa, meio melancólica” e receber uma playlist perfeita. Então eu...
Links:

Kaynak: Dev.to · 12 dk okuma · Yazar: Leo Garcez
Eu queria digitar “noite chuvosa, meio melancólica” e receber uma playlist perfeita. Então eu...
Links:

Fazer um gerador de playlists que realmente entendesse vibes, não só tags de gênero. Algo tipo: você digita "tarde fria num apartamento vazio" e recebe uma playlist boa de verdade, já salva no seu Spotify.
plaintext Next.js 14 (App Router) NextAuth v5 beta Anthropic Claude API (claude-sonnet-4-6) Spotify Web API Supabase (PostgreSQL) TypeScript + Tailwind CSS + Framer Motion
Escolhi o Claude porque ele tá bem em alta agora e, na prática, é confiável, alucina menos do que eu esperava pra esse tipo de tarefa. Ele é um pouco menos criativo que o GPT nas recomendações, mas compensa sendo mais previsível no formato das respostas, o que importa bastante quando você tá parseando JSON.
Isso me custou algumas horas e sessões de debug. Vou detalhar porque tem várias camadas de problema e você provavelmente vai bater na mesma parede se estiver usando NextAuth v5 com Spotify.
O Spotify proíbe localhost como redirect URI pra URIs de loopback. A solução é usar 127.0.0.1. Cadastrei http://127.0.0.1:3000/api/auth/callback/spotify no dashboard e setei AUTHURL=http://127.0.0.1:3000 no .env.local.
O Next.js, independente do host que você passa pra next dev -H, normaliza req.url e req.nextUrl.href de volta pra localhost em desenvolvimento. Isso não é bug documentado — é comportamento interno do framework.
O NextAuth v5 tem um utilitário chamado reqWithEnvURL que tenta corrigir exatamente isso, mas falha silenciosamente:
ts // Dentro do next-auth — simplificado function reqWithEnvURL(req: NextRequest): NextRequest { const url = process.env.AUTHURL ?? req.url; return new NextRequest(url, req); // ← o construtor normaliza de volta pra localhost }
Mesmo passando 127.0.0.1 explicitamente, o construtor do NextRequest sobrescreve. A "correção" não funciona.
O sintoma era desconcertante: a URL de autorização mostrava 127.0.0.1 corretamente, mas a troca de token continuava falhando. Fui atrás do código do @auth/core pra entender o que tava acontecendo.
Objetos Request nativos do browser/Node não normalizam URLs. A correção é contornar os route handlers do NextAuth e chamar Auth() do @auth/core diretamente, passando um Request nativo com a URL já corrigida:
ts // src/app/api/auth/[...nextauth]/route.ts import { Auth } from "@auth/core"; import { authConfig } from "../../../../../auth"; import { NextRequest } from "next/server";
function buildRequest(req: NextRequest): Request { const authOrigin = process.env.AUTHURL ?? http://${req.headers.get("host")}; const fixedUrl = req.url.replace(/^https?://[^/]+/, authOrigin);
const hasBody = req.method !== "GET" && req.method !== "HEAD"; return new Request(fixedUrl, { method: req.method, headers: req.headers, body: hasBody ? req.body : undefined, // duplex necessário para streaming de body no Node.js ...(hasBody && ({ duplex: "half" } as object)), }); }
const handler = (req: NextRequest) =>
Auth(buildRequest(req), authConfig as Parameters
Versão do @auth/core: ao usar Auth() diretamente, instale a versão exata que o next-auth usa internamente:
bash npm ls @auth/core # veja qual versão o next-auth requer npm install @auth/core@0.41.0 --save-exact
Configuração explícita no auth.ts: ao contornar os handlers do NextAuth, setEnvDefaults não roda mais. Configure basePath, secret e redirecturi explicitamente:
ts // auth.ts export const authConfig: NextAuthConfig = { trustHost: true, basePath: "/api/auth", secret: process.env.AUTHSECRET, providers: [ Spotify({ clientId: process.env.AUTHSPOTIFYID, clientSecret: process.env.AUTHSPOTIFYSECRET, authorization: { url: "https://accounts.spotify.com/authorize", params: { scope: SPOTIFYSCOPES, showdialog: true, redirecturi: ${process.env.AUTHURL}/api/auth/callback/spotify, }, }, }), ], // ... callbacks e pages como antes };
Mesmo com tudo acima, pode acontecer mais uma falha: se o usuário acessa http://localhost:3000, o navegador seta o cookie PKCE pro domínio localhost. Quando o Spotify redireciona de volta pra http://127.0.0.1:3000/..., o navegador não envia o cookie — domínios diferentes — e o codeverifier some. A troca falha de novo.
localhostA correção é garantir que localhost:3000 nunca apareça pro usuário, redirecionando via next.config.mjs:
js async redirects() { return [ { source: "/:path", has: [{ type: "host", value: "localhost:3000" }], destination: "http://127.0.0.1:3000/:path", permanent: false, }, { source: "/", has: [{ type: "host", value: "localhost:3000" }], destination: "http://127.0.0.1:3000/", permanent: false, }, ]; },
Com isso, todo o fluxo de auth fica em 127.0.0.1 e os cookies PKCE chegam onde precisam chegar.
A migração /tracks → /items está documentada, mas é fácil de perder se você seguiu um tutorial de 2022. Audio features e recomendações sumindo foi mais chato — tive que construir contexto de energia de outra forma, mais sobre isso abaixo.
NextRequest normaliza URLs pra localhostTem uma limitação que eu não vi muito discutida: no modo de desenvolvimento, o Spotify permite apenas 5 usuários autenticados E eles precisam ser adicionados manualmente via allowlist no dashboard.
Pra ir além disso, você precisa solicitar Extended Quota Mode. E o processo atual é bem pesado:
Na prática, isso significa que se você tá construindo algo novo como indie dev, vai ficar travado em modo de desenvolvimento. Você consegue testar e mostrar pra até 4 pessoas além de você — e só. Vale saber disso antes de planejar algum lançamento público.
O system prompt instrui o Claude a retornar apenas um array JSON, sem markdown, sem explicações. Essa parte é direta. O problema mais difícil é conseguir 50 faixas que realmente existam.
Incluir uma conversa de exemplo completa (usuário + assistente) antes do request real reduziu bastante os erros de formato, especialmente com artistas não-ingleses:
ts messages: [ { role: "user", content: "Create a playlist for this mood/vibe: late night drive, nostalgic", }, { role: "assistant", content: JSON.stringify([ { title: "Drive", artist: "The Cars", reason: "Synth-pop clássico com energia perfeita de madrugada" }, { title: "Running Up That Hill", artist: "Kate Bush", reason: "Art-pop etéreo, emocionalmente assombroso" }, // ... ]), }, { role: "user", content: actualUserMessage }, ],
Mesmo com prompting cuidadoso, modelos eventualmente jogam texto de introdução. Sempre extraia o array:
ts const match = raw.match(/[[\s\S]]/); if (!match) throw new Error("Nenhum array JSON encontrado"); const suggestions = JSON.parse(match[0]);
Auth() direto com Request nativoDescobri que quanto mais músicas você pede, mais o modelo inventa títulos.
Pedindo 70 músicas, o Claude começa a criar faixas com nomes plausíveis que não existem. Com 50 e restrições explícitas, fica bem melhor.
plaintext EXISTÊNCIA NO SPOTIFY — CRÍTICO: Cada música DEVE existir no Spotify. Antes de incluir uma faixa, pergunte: "Tenho certeza que esta música existe no Spotify com este título e artista exatos?" Se houver qualquer dúvida, escolha outra música que você tem certeza.
REGRAS DE FORMATO DO TÍTULO:
ARMADILHAS COMUNS DE ALUCINAÇÃO:
Também removi a abordagem de duas etapas "gerar 70, refinar pra 50". Era cara em tempo e custo, e uma única geração de 50 com boas instruções performa melhor.
Each item:
SPOTIFY EXISTENCE — CRITICAL: Every song MUST exist on Spotify. Before including a track, ask yourself: "Am I certain this song exists on Spotify under this exact title and artist?" If there is any doubt, pick a different song you are certain about.
TITLE FORMAT RULES:
COMMON HALLUCINATION TRAPS TO AVOID:
DISTRIBUTION:
DIVERSITY:
Curiosidade: o prompt tá em inglês mesmo que o usuário escreva em português. O Claude entende o humor no idioma que vier e retorna os dados no formato esperado sem problema.
Mesmo com um bom prompt, algumas faixas voltam com títulos ou artistas levemente errados. Três ajustes no lado da busca:
Buscar 10 candidatos em vez de 1 Em vez de limit=1, buscar limit=10 e escolher o melhor match.
Filtro de popularidade Pular resultados com popularity < 30 — evita gravar versões ao vivo obscuras quando a faixa correta não é encontrada:
ts const POPULARITYFLOOR = 30; const aboveFloor = candidates.filter(t => t.popularity >= POPULARITYFLOOR); const pool = aboveFloor.length > 0 ? aboveFloor : candidates;
ts function fuzzyMatch(candidate: SpotifyTrack, suggestion: ClaudeTrackSuggestion): boolean { const trackName = normalizeStr(candidate.name); const artistName = normalizeStr(candidate.artists[0]?.name ?? ""); const sugTitle = normalizeStr(suggestion.title); const sugArtist = normalizeStr(suggestion.artist); const titleMatch = trackName.includes(sugTitle) || sugTitle.includes(trackName); const artistMatch = artistName.includes(sugArtist) || sugArtist.includes(artistName); return titleMatch && artistMatch; }
Playlists geradas por IA alucinam mais quando o humor é específico de artista — tipo "algo como Radiohead". Nesses casos, o grafo de artistas do próprio Spotify é mais confiável que o Claude.
Adicionei detecção automática: uma chamada rápida ao Claude Haiku (~0,5s) classifica o prompt antes da geração principal:
ts // Retorna { mode: "ai" | "related", artists: ["Radiohead", "Nick Cave"] } const detected = await detectPlaylistMode(mood);
Se artistas são detectados, o app:
ts if (detected.mode === "related" && detected.artists.length > 0) { const { suggestions, resolvedSeeds } = await buildRelatedArtistsPlaylist( detected.artists, accessToken, energy ); if (suggestions.length > 0) { return NextResponse.json({ suggestions, mode: "related", seedArtists: resolvedSeeds }); } // fallthrough para modo AI se não encontrou nada }
O app deixa usuários escolher uma playlist de referência ou seus top artistas do Spotify (último mês / 6 meses / histórico completo). Esse contexto é passado pro Claude como uma impressão digital musical.
Mandar nomes de faixas brutos confunde o modelo. Em vez disso, agregue num perfil:
ts // Contar frequência de artistas em todas as faixas da playlist const artistFreq = new Map<string, { name: string; count: number }>(); for (const track of tracks) { const a = track.artists[0]; const prev = artistFreq.get(a.id); artistFreq.set(a.id, { name: a.name, count: (prev?.count ?? 0) + 1 }); }
Pra playlists, também busco dados de gênero dos artistas principais via GET /artists/{id} (5 chamadas paralelas), já que os itens de playlist não retornam gêneros nativamente.
Mesmo sem seleção de referência explícita, o app passa os top artistas e gêneros baseline do usuário como contexto suave pra cada geração.
A instrução no prompt mudou de "não recomende esses artistas" pra algo mais útil:
plaintext → Biase as recomendações em direção a este DNA musical: tempo, humor e estilo de produção similar. → Descubra artistas com som SIMILAR — não necessariamente os mesmos artistas. → NÃO repita faixas já listadas acima.
O fluxo original: buscar todas as 50 faixas em paralelo, retornar tudo de uma vez, mostrar um spinner.
O problema é que o usuário ficava olhando "Salvando no Spotify..." por 5-10 segundos sem nenhum feedback. Server-Sent Events resolve isso — cada faixa é emitida conforme resolve:
ts // Na rota da API const stream = new ReadableStream({ async start(controller) { const send = (data: object) => controller.enqueue(encoder.encode(data: ${JSON.stringify(data)}\n\n));
await Promise.all( suggestions.map(async (suggestion) => { const track = await searchTrack(suggestion, accessToken); if (track) { foundTracks.push({ track, suggestion }); send({ type: "track", track, suggestion }); // emitido imediatamente } }) );
// Criar playlist depois que todas as buscas terminam const playlist = await createPlaylist(...); send({ type: "done", playlist, found: foundTracks.length }); controller.close(); }, });
return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" }, });
A busca paralela continua na velocidade máxima. Mas agora o cliente vê cada faixa aparecer com album art conforme resolve, com uma barra de progresso ao vivo — em vez de um spinner em branco por 10 segundos.
Sobre prompt engineering:
Sobre a API do Spotify:
Sobre streaming no Next.js:
Sobre Next.js + OAuth:
O app se chama Moodify. Está OpenSource no [GitHub] (https://github.com/LeoGarcez/moodify). Se você tá fazendo algo parecido, espero que ajude um pouco.
Como vocês melhorariam esse prompt? Se alguém já passou por algo parecido ou tiver ideias, comenta aí
Construído com Next.js, Claude API, Spotify Web API e Supabase. Deploy no Vercel.
Bu içerik otomatik olarak derlenmektedir. Tüm haklar orijinal yayıncıya aittir.