API Spotify Now Playing
Integración en tiempo real con la API de Spotify
Aprende cómo implementar la funcionalidad para mostrar la música que estoy escuchando actualmente.
Spotify Now Playing API
Integración en tiempo real con Spotify Web API
Esta API permite obtener en tiempo real la canción que estoy escuchando actualmente en Spotify. Utiliza OAuth 2.0 para la autenticación segura y se actualiza automáticamente cada 30 segundos. La implementación está optimizada para funcionar tanto en desarrollo local como en GitHub Pages, con manejo robusto de errores y estados de conexión.
Características Principales
Tiempo Real
Actualización automática cada 30 segundos con progreso de reproducción en vivo
Autenticación Segura
OAuth 2.0 con refresh tokens para acceso continuo sin re-autorización
Datos Completos
Información completa: canción, artista, álbum, imagen, progreso y enlaces
TypeScript
Completamente tipado con interfaces para Spotify API y respuestas
Next.js 15
Implementado como API Route con App Router y manejo de errores robusto
Multi-entorno
Compatible con desarrollo local, Vercel y GitHub Pages automáticamente
Endpoint y Respuesta
// Endpoint de la API
GET /api/spotify
// Respuesta cuando está reproduciendo música
{
"isPlaying": true,
"title": "Bohemian Rhapsody",
"artist": "Queen",
"album": "A Night at the Opera",
"albumImageUrl": "https://i.scdn.co/image/...",
"songUrl": "https://open.spotify.com/track/...",
"progress": 120000,
"duration": 355000,
"timestamp": "2025-05-26T10:30:00Z"
}
// Respuesta cuando no está reproduciendo
{
"isPlaying": false,
"timestamp": "2025-05-26T10:30:00Z"
}
// Respuesta en caso de error
{
"isPlaying": false,
"error": "Error processing Spotify request",
"details": "...",
"timestamp": "2025-05-26T10:30:00Z"
}
Configuración y Setup
1Crear Aplicación en Spotify
1. Ve a Spotify Developer Dashboard
2. Crea una nueva aplicación con nombre descriptivo
3. Obtén tu Client ID
y Client Secret
4. En Settings, añade Redirect URI: http://localhost:3000/callback
5. Guarda los scopes necesarios: user-read-currently-playing
2Configurar Variables de Entorno
# Variables de entorno necesarias (.env.local)
SPOTIFY_CLIENT_ID=tu_client_id_aqui
SPOTIFY_CLIENT_SECRET=tu_client_secret_aqui
SPOTIFY_REFRESH_TOKEN=tu_refresh_token_aqui
3Obtener Refresh Token
Para obtener el refresh token necesitas completar el flujo OAuth una vez:
- Crear URL de autorización:
https://accounts.spotify.com/authorize
- Incluir parámetros: client_id, response_type=code, redirect_uri, scope=user-read-currently-playing
- Autorizar y obtener el código de autorización desde la URL de callback
- Intercambiar el código por access_token y refresh_token usando POST a /api/token
- Guardar solo el
refresh_token
en variables de entorno
💡 Tip: El refresh_token no expira, solo necesitas obtenerlo una vez.
Ejemplo de Implementación
'use client'
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FaSpotify, FaHeadphones, FaPause, FaPlay, FaVolumeUp, FaExclamationTriangle } from 'react-icons/fa';
import Image from 'next/image';
interface SpotifyData {
isPlaying: boolean;
title?: string;
artist?: string;
album?: string;
albumImageUrl?: string;
songUrl?: string;
progress?: number;
duration?: number;
error?: string;
timestamp?: string;
}
const formatTime = (ms: number): string => {
const seconds = Math.floor((ms / 1000) % 60);
const minutes = Math.floor((ms / 1000 / 60) % 60);
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
};
// Función para obtener la URL correcta del API
const getApiUrl = () => {
const isGitHubPages = window.location.hostname.includes('github.io');
if (isGitHubPages) {
return `${window.location.origin}/api/spotify`;
}
return '/api/spotify';
};
const SpotifyNowPlaying: React.FC = () => {
const [data, setData] = useState<SpotifyData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [progress, setProgress] = useState(0);
const [clientProgress, setClientProgress] = useState(0);
useEffect(() => {
const fetchSpotifyData = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch(getApiUrl(), {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
cache: 'no-store'
});
if (!res.ok) {
throw new Error(`API responded with status: ${res.status}`);
}
const newData: SpotifyData = await res.json();
if (newData.error) {
setError(newData.error);
} else {
setData(newData);
if (newData.isPlaying && newData.progress && newData.duration) {
setProgress((newData.progress / newData.duration) * 100);
setClientProgress(newData.progress);
}
}
} catch (error) {
setError('No se pudieron cargar los datos de Spotify');
} finally {
setLoading(false);
}
};
fetchSpotifyData();
// Actualización cada 30 segundos
const interval = setInterval(fetchSpotifyData, 30000);
return () => clearInterval(interval);
}, []);
// Progreso en tiempo real
useEffect(() => {
let intervalId: NodeJS.Timeout;
if (data?.isPlaying && data.duration) {
intervalId = setInterval(() => {
setClientProgress(prev => {
if (prev >= data.duration!) return prev;
const newProgress = prev + 1000;
setProgress((newProgress / data.duration!) * 100);
return newProgress;
});
}, 1000);
}
return () => {
if (intervalId) clearInterval(intervalId);
};
}, [data]);
if (loading) {
return (
<div className="flex items-center space-x-2 text-sm text-gray-400">
<FaSpotify className="text-emerald-400 animate-pulse" />
<span>Conectando con Spotify...</span>
</div>
);
}
if (error) {
return (
<div className="flex flex-col">
<div className="flex items-center space-x-2 text-sm text-red-400">
<FaExclamationTriangle className="text-red-400" />
<span>Error al conectar con Spotify</span>
</div>
<p className="text-xs text-gray-500 mt-1">{error}</p>
</div>
);
}
if (!data || !data.isPlaying) {
return (
<div className="flex flex-col">
<div className="flex items-center space-x-2 text-sm text-gray-400">
<FaHeadphones className="text-gray-400" />
<span>No escucho nada ahora</span>
</div>
<p className="text-xs text-gray-500 mt-1">
La música aparecerá aquí cuando reproduzca algo en Spotify
</p>
</div>
);
}
return (
<motion.div className="flex flex-col">
<div className="flex items-center mb-2">
<motion.div
animate={{ scale: [1, 1.2, 1], rotate: [0, 5, -5, 0] }}
transition={{ duration: 2, repeat: Infinity, repeatType: "reverse" }}
className="mr-2"
>
<FaSpotify className="text-lg text-green-500" />
</motion.div>
<span className="text-sm font-medium text-emerald-400">Escuchando ahora</span>
</div>
<div className="relative overflow-hidden rounded-lg bg-gradient-to-br from-neutral-800 to-neutral-900">
<div
className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-green-500 to-emerald-400"
style={{ transform: `scaleX(${progress / 100})`, transformOrigin: 'left' }}
/>
<div className="p-3">
<div className="flex space-x-3 items-center">
{data.albumImageUrl && (
<div className="relative w-14 h-14 min-w-[56px] rounded-md overflow-hidden shadow-lg">
<Image
src={data.albumImageUrl}
alt={`${data.album} cover`}
className="w-full h-full object-cover"
width={56}
height={56}
/>
<div className="absolute bottom-1 right-1 bg-black/60 rounded-full p-1">
<FaPlay className="text-white text-[8px]" />
</div>
</div>
)}
<div className="flex flex-col overflow-hidden flex-1">
<a
href={data.songUrl}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-white truncate hover:text-emerald-300 transition-colors text-sm"
>
{data.title}
</a>
<p className="text-gray-400 text-xs truncate">
{data.artist}
</p>
<div className="flex justify-between items-center mt-1 text-[10px] text-gray-500">
<span>
{data.progress && formatTime(clientProgress)}
</span>
<span className="flex items-center">
<FaVolumeUp className="mr-1" size={8} />
{data.duration && formatTime(data.duration)}
</span>
</div>
</div>
</div>
</div>
</div>
</motion.div>
);
};
export default SpotifyNowPlaying;
📍 Ubicación en el Proyecto
• Componente: /src/components/SpotifyNowPlaying.tsx
• API Route: /src/app/api/spotify/route.ts
• Usado en: /src/ui/sobremi/ProfileSection.tsx
• Página: Sección "Sobre mí" del portafolio (/sobremi
)
📦 Dependencias Utilizadas
Principales:
- •
next
^15.2.4 - Framework - •
react
^19.0.0 - UI Library - •
framer-motion
^12.5.0 - Animaciones - •
react-icons
^5.5.0 - Iconos
Features:
- • TypeScript para tipado
- • TailwindCSS para estilos
- • Next.js Image para optimización
- • Animaciones con Framer Motion
API Route Implementation
// API Route: /app/api/spotify/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
const CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
const REFRESH_TOKEN = process.env.SPOTIFY_REFRESH_TOKEN;
if (!CLIENT_ID || !CLIENT_SECRET || !REFRESH_TOKEN) {
return NextResponse.json({
isPlaying: false,
error: 'Missing Spotify credentials',
});
}
try {
// 1. Obtener token de acceso usando refresh token
const authResponse = 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: 'refresh_token',
refresh_token: REFRESH_TOKEN
})
});
const authData = await authResponse.json();
// 2. Obtener estado actual de reproducción
const nowPlayingResponse = await fetch(
'https://api.spotify.com/v1/me/player/currently-playing',
{
headers: {
'Authorization': `Bearer ${authData.access_token}`
}
}
);
// 3. Manejar respuestas vacías o errores
if (nowPlayingResponse.status === 204 || nowPlayingResponse.status > 400) {
return NextResponse.json({
isPlaying: false,
timestamp: new Date().toISOString()
});
}
const nowPlaying = await nowPlayingResponse.json();
if (!nowPlaying.is_playing) {
return NextResponse.json({
isPlaying: false,
timestamp: new Date().toISOString()
});
}
// 4. Formatear respuesta
return NextResponse.json({
isPlaying: nowPlaying.is_playing,
title: nowPlaying.item.name,
artist: nowPlaying.item.artists.map(artist => artist.name).join(', '),
album: nowPlaying.item.album.name,
albumImageUrl: nowPlaying.item.album.images[0].url,
songUrl: nowPlaying.item.external_urls.spotify,
progress: nowPlaying.progress_ms,
duration: nowPlaying.item.duration_ms,
timestamp: new Date().toISOString()
}, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Access-Control-Allow-Origin': '*'
}
});
} catch (error) {
return NextResponse.json({
isPlaying: false,
error: 'Error processing Spotify request',
details: String(error)
});
}
}
🔧 Características del API Route
Manejo de Errores
- • Validación de variables de entorno
- • Manejo de tokens expirados
- • Respuestas consistentes en errores
- • Logging detallado para debugging
Optimizaciones
- • Headers de CORS configurados
- • Cache-Control sin caché
- • Respuestas TypeScript tipadas
- • Formateo automático de artistas
📊 Estados de Respuesta
Demo en Vivo
Esta API está funcionando en tiempo real en mi portafolio. Puedes verla en acción en la sección "Sobre mí" donde se muestra la música que estoy escuchando actualmente en Spotify.
✨ Características en Vivo
- • 🎵 Actualización automática cada 30 segundos
- • 🎨 Animaciones fluidas con Framer Motion
- • 📱 Diseño responsive y optimizado
- • 🔄 Barra de progreso en tiempo real
- • 🖼️ Imagen del álbum optimizada con Next.js
- • 🔗 Enlaces directos a Spotify
🚀 Optimizaciones
- • 🌐 Compatible con GitHub Pages
- • ⚡ Sin caché para datos en tiempo real
- • 🛡️ Manejo robusto de errores
- • 📱 Estados de carga y error
- • 🎯 TypeScript para mejor desarrollo
- • 🎪 Estados expandibles interactivos
Consideraciones Técnicas
Despliegue y Hosting
Seguridad y Privacidad
Rendimiento
Limitaciones Conocidas
Documentación Técnica Completa
Explora la documentación completa del portafolio, incluyendo arquitectura, sistema de terminal, componentes y guías de desarrollo.
Ver Documentación Completa¿Necesitas ayuda con la implementación??
Contáctame para soporte personalizado y optimización de tu integración.