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.

API RESTfulDocumentación CompletaEjemplos de Uso

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

GET/api/spotify
// 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

.env.local
# 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:

  1. Crear URL de autorización: https://accounts.spotify.com/authorize
  2. Incluir parámetros: client_id, response_type=code, redirect_uri, scope=user-read-currently-playing
  3. Autorizar y obtener el código de autorización desde la URL de callback
  4. Intercambiar el código por access_token y refresh_token usando POST a /api/token
  5. Guardar solo el refresh_token en variables de entorno

💡 Tip: El refresh_token no expira, solo necesitas obtenerlo una vez.

Ejemplo de Implementación

SpotifyNowPlaying.tsx - Componente React
'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

/app/api/spotify/route.ts - Implementación completa
// 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

Reproduciendo:Datos completos de la canción actual
Sin reproducir:isPlaying: false con timestamp
Error:Mensaje de error con detalles para debugging

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

Vercel: Soporte nativo para API Routes
GitHub Pages: Detección automática de entorno
Variables de entorno: Configuración segura
Edge Runtime: Respuestas rápidas globalmente

Seguridad y Privacidad

Tokens: Solo refresh token almacenado
Alcance: Solo lectura de reproducción actual
Datos: Sin persistencia en base de datos
CORS: Headers configurados correctamente

Rendimiento

Cache: Sin caché para datos en tiempo real
Imágenes: Optimización automática con Next.js
Requests: Polling inteligente cada 30s
Errores: Reintento automático en fallos

Limitaciones Conocidas

API Limits: Respeta límites de Spotify API
Privacidad: Solo funciona si mi perfil es público
Delay: Máximo 30s de retraso en actualizaciones
Tokens: Requiere renovación manual si expiran

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.