TrackID.dev

Building Music Apps with TrackID.dev

Complete developer guide with patterns, examples, and best practices for integrating music track identification into your applications.

What You'll Build

By the end of this guide, you'll know how to build:

✅ Core Features

  • • Submit YouTube URLs for analysis
  • • Display real-time processing progress
  • • Show identified tracks with timestamps
  • • Handle errors gracefully
  • • Implement smart caching

ADV

  • • Batch processing multiple mixes
  • • Real-time status updates
  • • Export tracklists in multiple formats
  • • User playlist management
  • • Social sharing features

Architecture Overview

TrackID.dev uses an asynchronous job-based architecture that's perfect for music processing:


┌─────────────┐    POST /api/analyze     ┌──────────────┐
│   Your App  │ ─────────────────────── ► │  TrackID.dev │
└─────────────┘                          └──────────────┘
       │                                         │
       │ 1. Submit YouTube URL                   │ 2. Create Job
       │ 2. Receive Job ID                       │ 3. Queue Processing
       │                                         │
       ▼                                         ▼
┌─────────────┐    GET /api/job/{id}     ┌──────────────┐
│  Poll Status │ ◄──────────────────────── │ Audio Pipeline│
└─────────────┘                          └──────────────┘
       │                                         │
       │ 3. Check Progress (every 5s)           │ • Download Audio
       │ 4. Get Results when done               │ • Segment & Fingerprint  
       │                                         │ • Identify Tracks
       ▼                                         │ • Return Results
┌─────────────┐                          ┌──────────────┐
│ Display     │ ◄──────────────────────── │   Completed  │
│ Tracklist   │        Results            │     Job      │
└─────────────┘                          └──────────────┘
              

INFO

  • Non-blocking: Your UI stays responsive during long processing
  • Scalable: Handle multiple requests without blocking
  • Reliable: Jobs survive temporary network issues
  • Transparent: Real-time progress updates

Step-by-Step Implementation

Step 1: Project Setup

Create a new React/Next.js project and set up the basic structure:

npx create-next-app@latest my-music-app
cd my-music-app
npm install

# Create our API service
mkdir lib
touch lib/trackid.js

Step 2: Create API Client

Build a robust client to handle the TrackID.dev API:

// lib/trackid.js
class TrackIDClient {
  constructor(baseUrl = 'https://trackid.dev/api') {
    this.baseUrl = baseUrl;
  }

  async analyzeTrack(youtubeUrl) {
    try {
      const response = await fetch(`${this.baseUrl}/analyze`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url: youtubeUrl })
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error || `HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      console.error('Failed to start analysis:', error);
      throw error;
    }
  }

  async getJobStatus(jobId) {
    try {
      const response = await fetch(`${this.baseUrl}/job/${jobId}`);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      console.error('Failed to get job status:', error);
      throw error;
    }
  }

  async waitForCompletion(jobId, onProgress) {
    return new Promise((resolve, reject) => {
      const poll = async () => {
        try {
          const job = await this.getJobStatus(jobId);
          
          if (onProgress) onProgress(job);

          if (job.status === 'completed') {
            resolve(job);
          } else if (job.status === 'failed') {
            reject(new Error(job.error || 'Processing failed'));
          } else {
            // Continue polling
            setTimeout(poll, 5000); // Poll every 5 seconds
          }
        } catch (error) {
          reject(error);
        }
      };

      poll();
    });
  }
}

export default TrackIDClient;

Step 3: React Hook for State Management

Create a custom hook to manage the analysis process:

// hooks/useTrackAnalysis.js
import { useState, useCallback } from 'react';
import TrackIDClient from '../lib/trackid';

const client = new TrackIDClient();

export function useTrackAnalysis() {
  const [isAnalyzing, setIsAnalyzing] = useState(false);
  const [progress, setProgress] = useState(0);
  const [currentStep, setCurrentStep] = useState('');
  const [tracks, setTracks] = useState([]);
  const [error, setError] = useState(null);

  const analyzeTrack = useCallback(async (youtubeUrl) => {
    setIsAnalyzing(true);
    setError(null);
    setProgress(0);
    setCurrentStep('Starting analysis...');
    setTracks([]);

    try {
      // Start the analysis
      const { jobId } = await client.analyzeTrack(youtubeUrl);
      
      // Wait for completion with progress updates
      const result = await client.waitForCompletion(jobId, (job) => {
        setProgress(job.progress || 0);
        setCurrentStep(job.currentStep || 'Processing...');
      });

      // Set final results
      setTracks(result.tracks || []);
      setProgress(100);
      setCurrentStep('Analysis complete!');
      
    } catch (error) {
      setError(error.message);
      setCurrentStep('Analysis failed');
    } finally {
      setIsAnalyzing(false);
    }
  }, []);

  const reset = useCallback(() => {
    setIsAnalyzing(false);
    setProgress(0);
    setCurrentStep('');
    setTracks([]);
    setError(null);
  }, []);

  return {
    isAnalyzing,
    progress,
    currentStep,
    tracks,
    error,
    analyzeTrack,
    reset
  };
}

Step 4: Build UI Components

Create the main analysis component:

// components/TrackAnalyzer.jsx
import { useState } from 'react';
import { useTrackAnalysis } from '../hooks/useTrackAnalysis';

export function TrackAnalyzer() {
  const [url, setUrl] = useState('');
  const {
    isAnalyzing,
    progress,
    currentStep,
    tracks,
    error,
    analyzeTrack,
    reset
  } = useTrackAnalysis();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (url.trim()) {
      analyzeTrack(url.trim());
    }
  };

  return (
    <div className="max-w-4xl mx-auto p-6 space-y-6">
      <h1 className="text-3xl font-bold text-center">
        Music Track Analyzer
      </h1>

      {/* URL Input Form */}
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <input
            type="url"
            value={url}
            onChange={(e) => setUrl(e.target.value)}
            placeholder="Enter YouTube URL..."
            className="w-full p-3 border border-gray-300 rounded-lg"
            disabled={isAnalyzing}
          />
        </div>
        <div className="flex gap-3">
          <button
            type="submit"
            disabled={isAnalyzing || !url.trim()}
            className="flex-1 bg-blue-500 text-white p-3 rounded-lg disabled:opacity-50"
          >
            {isAnalyzing ? 'Analyzing...' : 'Analyze Tracks'}
          </button>
          {(tracks.length > 0 || error) && (
            <button
              type="button"
              onClick={reset}
              className="px-6 bg-gray-500 text-white rounded-lg"
            >
              Reset
            </button>
          )}
        </div>
      </form>

      {/* Progress Display */}
      {isAnalyzing && (
        <div className="bg-gray-50 p-4 rounded-lg">
          <div className="flex items-center justify-between mb-2">
            <span className="text-sm font-medium">
              {currentStep}
            </span>
            <span className="text-sm text-gray-500">
              {progress}%
            </span>
          </div>
          <div className="w-full bg-gray-200 rounded-full h-2">
            <div 
              className="bg-blue-500 h-2 rounded-full transition-all duration-300"
              style={{ width: `${progress}%` }}
            />
          </div>
        </div>
      )}

      {/* Error Display */}
      {error && (
        <div className="bg-red-50 border border-red-200 p-4 rounded-lg">
          <h3 className="font-semibold text-red-800">Analysis Failed</h3>
          <p className="text-red-700 text-sm mt-1">{error}</p>
        </div>
      )}

      {/* Results Display */}
      {tracks.length > 0 && (
        <div className="space-y-4">
          <h2 className="text-2xl font-semibold">
            Found {tracks.filter(t => t.artist && t.title).length} Tracks
          </h2>
          
          <div className="space-y-2">
            {tracks.map((track, index) => (
              <TrackItem key={track.id || index} track={track} />
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

Step 5: Track Display Component

// components/TrackItem.jsx
function TrackItem({ track }) {
  const formatTime = (seconds) => {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  };

  const getConfidenceColor = (confidence) => {
    if (confidence >= 0.9) return 'text-green-600';
    if (confidence >= 0.7) return 'text-yellow-600';
    return 'text-red-600';
  };

  const isUnknown = !track.artist || !track.title || track.confidence < 0.3;

  return (
    <div className="flex items-center gap-4 p-4 border border-gray-200 rounded-lg hover:bg-gray-50">
      <div className="text-sm font-mono text-gray-500 min-w-[60px]">
        {formatTime(track.timestamp)}
      </div>
      
      <div className="flex-1">
        {isUnknown ? (
          <div>
            <p className="font-medium text-gray-400">Unknown Track</p>
            <p className="text-sm text-gray-400">
              Could not identify this segment
            </p>
          </div>
        ) : (
          <div>
            <p className="font-medium">{track.title}</p>
            <p className="text-sm text-gray-600">{track.artist}</p>
          </div>
        )}
      </div>
      
      <div className="flex items-center gap-2">
        {!isUnknown && (
          <>
            <div className={`text-sm ${getConfidenceColor(track.confidence)}`}>
              {Math.round(track.confidence * 100)}%
            </div>
            
            {track.youtubeUrl && (
              <a
                href={track.youtubeUrl}
                target="_blank"
                rel="noopener noreferrer"
                className="text-blue-500 hover:text-blue-700 text-sm"
              >
                ↗ YouTube
              </a>
            )}
          </>
        )}
      </div>
    </div>
  );
}

export default TrackItem;

Advanced Implementation Patterns

Smart Caching Strategy

Avoid reprocessing the same URLs by implementing client-side caching:

// lib/cache.js
class TrackCache {
  constructor() {
    this.cache = new Map();
    this.maxAge = 24 * 60 * 60 * 1000; // 24 hours
  }

  getKey(url) {
    return btoa(url).replace(/[^a-zA-Z0-9]/g, '');
  }

  get(url) {
    const key = this.getKey(url);
    const cached = this.cache.get(key);
    
    if (!cached) return null;
    
    if (Date.now() - cached.timestamp > this.maxAge) {
      this.cache.delete(key);
      return null;
    }
    
    return cached.data;
  }

  set(url, tracks) {
    const key = this.getKey(url);
    this.cache.set(key, {
      data: tracks,
      timestamp: Date.now()
    });
  }
}

export const trackCache = new TrackCache();

// Updated hook with caching
export function useTrackAnalysis() {
  // ... existing state ...

  const analyzeTrack = useCallback(async (youtubeUrl) => {
    // Check cache first
    const cached = trackCache.get(youtubeUrl);
    if (cached) {
      setTracks(cached);
      setProgress(100);
      setCurrentStep('Results loaded from cache');
      return;
    }

    // ... existing analysis logic ...
    
    // Cache successful results
    if (result.tracks) {
      trackCache.set(youtubeUrl, result.tracks);
    }
  }, []);
}

Batch Processing Multiple URLs

// hooks/useBatchAnalysis.js
export function useBatchAnalysis() {
  const [batches, setBatches] = useState([]);
  const [isProcessing, setIsProcessing] = useState(false);

  const processBatch = useCallback(async (urls) => {
    setIsProcessing(true);
    const results = [];
    
    for (const url of urls) {
      try {
        setBatches(prev => [...prev, { url, status: 'processing' }]);
        
        const { jobId } = await client.analyzeTrack(url);
        const result = await client.waitForCompletion(jobId);
        
        results.push({ url, tracks: result.tracks, status: 'completed' });
        setBatches(prev => 
          prev.map(b => b.url === url 
            ? { ...b, status: 'completed', tracks: result.tracks }
            : b
          )
        );
        
        // Rate limiting - wait between requests
        await new Promise(resolve => setTimeout(resolve, 2000));
        
      } catch (error) {
        results.push({ url, error: error.message, status: 'failed' });
        setBatches(prev => 
          prev.map(b => b.url === url 
            ? { ...b, status: 'failed', error: error.message }
            : b
          )
        );
      }
    }
    
    setIsProcessing(false);
    return results;
  }, []);

  return { batches, isProcessing, processBatch };
}

Production Considerations

✅ Best Practices

  • Error Boundaries: Wrap components in error boundaries
  • Loading States: Show progress for long operations
  • Debounced Input: Avoid excessive API calls
  • Offline Support: Handle network failures gracefully
  • Accessibility: Include ARIA labels and keyboard navigation
  • Mobile Responsive: Optimize for mobile devices

🔧 Performance Tips

  • Lazy Loading: Load components on demand
  • Memoization: Use React.memo for expensive renders
  • Virtual Scrolling: For large track lists
  • Service Workers: Cache API responses offline
  • Compression: Gzip API responses
  • CDN: Serve static assets from CDN

⚠️ Rate Limiting

Remember TrackID.dev has rate limits (10 requests per 15 minutes). Implement proper retry logic and inform users about limits.

Example Application Ideas

🎧 Consumer Apps

  • • DJ Set Tracker - Identify tracks in mixes
  • • Radio Show Analyzer - Track radio playlists
  • • Playlist Generator - Create Spotify playlists
  • • Music Discovery - Find new tracks from mixes

TOOLS

  • • CLI Tool - Batch process from command line
  • • Discord Bot - Analyze tracks in servers
  • • Browser Extension - One-click identification
  • • Mobile App - On-the-go track analysis

Next Steps

Ready to start building? Here's what to do next:

1

Clone the starter template

Get a head start with our React/Next.js boilerplate

2

Join our Discord community

Get help, share your projects, and connect with other developers

3

Check out the API reference

Detailed documentation for all endpoints and response formats

4

Contribute to the project

Help improve TrackID.dev for everyone