Skip to main content

Overview

SoundWave integrates with two primary external services: Spotify (via a token server) and Invidious (YouTube frontend instances). All API requests are proxied through intermediate servers to handle CORS restrictions and authentication.

Architecture Diagram

Spotify API Integration

Token Server Architecture

SoundWave uses an external token server hosted on Render.com to manage Spotify credentials: Base URL: https://jsnode-ab0e.onrender.com
The token server acts as a proxy to avoid exposing Spotify client credentials in the frontend code. Never hardcode API credentials in client-side JavaScript.

Authentication Flow

1. Token Acquisition

let accessToken = '';

async function getAccessToken() {
    const response = await fetch('https://jsnode-ab0e.onrender.com', { 
        method: 'POST' 
    });
    const data = await response.json();
    accessToken = data.access_token;
}

window.onload = getAccessToken;
Reference: index.html:96-103, index.html:135
Endpoint
POST
POST https://jsnode-ab0e.onrender.com
access_token
string
required
Spotify API access token valid for approximately 1 hour

Alternative Token Endpoint

index3.html uses a different endpoint:
async function getAccessToken() {
    const response = await fetch('https://jsnode-ab0e.onrender.com/get-token', { 
        method: 'POST' 
    });
    const data = await response.json();
    accessToken = data.access_token;
}
Reference: index3.html:97-101
Both endpoints (/ and /get-token) return the same token structure. The difference is only in the route path.

Track Search API

Request Pattern

async function searchSongs() {
    const query = document.getElementById('search-query').value.trim();
    if (!query) {
        alert('Por favor, ingresa un término de búsqueda.');
        return;
    }

    const response = await fetch(
        `https://jsnode-ab0e.onrender.com/search?query=${encodeURIComponent(query)}&type=track&accessToken=${accessToken}`
    );
    const data = await response.json();

    const songList = document.getElementById('song-list');
    songList.innerHTML = '';

    data.tracks.items.forEach(track => {
        const li = document.createElement('li');
        li.textContent = `${track.name} - ${track.artists.map(artist => artist.name).join(', ')}`;
        li.dataset.embedUrl = `https://open.spotify.com/embed/track/${track.id}?utm_source=generator&theme=0`;
        li.addEventListener('click', () => playTrack(li.dataset.embedUrl));
        songList.appendChild(li);
    });
}
Reference: index.html:106-126
Endpoint
GET
GET https://jsnode-ab0e.onrender.com/search
query
string
required
Search term for song, artist, or album
type
string
default:"track"
Type of search result (track, artist, album, playlist)
accessToken
string
required
Previously obtained Spotify access token

Response Structure

tracks
object
Container for track search results

Spotify Embed Integration

Embed URL Format

function playTrack(embedUrl) {
    const player = document.getElementById('player');
    player.src = embedUrl;
}

// Embed URL construction
const embedUrl = `https://open.spotify.com/embed/track/${track.id}?utm_source=generator&theme=0`;
Reference: index.html:128-132, index.html:122
Embed URL Structure
URL
https://open.spotify.com/embed/track/{TRACK_ID}?utm_source=generator&theme=0
TRACK_ID
string
required
Spotify track identifier from search results
utm_source
string
default:"generator"
Tracking parameter for embed analytics
theme
string
default:"0"
Theme setting: 0 for dark theme, 1 for light theme
index2.html implements a download link generator:
const response = await fetch('https://jsnode-ab0e.onrender.com/get-download-link', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ spotifyUrl })
});

const data = await response.json();

if (response.ok) {
    result.innerHTML = `<p>Download Link: <a href="${data.downloadLink}" target="_blank">${data.downloadLink}</a></p>`;
    audioPlayer.src = data.downloadLink;
    audioPlayer.play();
} else {
    errorMessage.textContent = data.error || 'Failed to retrieve download link';
}
Reference: index2.html:96-115
Endpoint
POST
POST https://jsnode-ab0e.onrender.com/get-download-link
spotifyUrl
string
required
Full Spotify track URL (e.g., https://open.spotify.com/track/...)
Direct URL to audio file for download/playback
error
string
Error message if the request fails

Invidious API Integration

Invidious provides a privacy-respecting YouTube frontend. SoundWave scrapes HTML from Invidious instances to extract video data.

CORS Proxy Strategy

All Invidious requests are proxied through AllOrigins to bypass CORS restrictions:
const url = 'https://inv.nadeko.net/search?q=' + encodeURIComponent(songName);
const proxyUrl = 'https://api.allorigins.win/raw?url=' + encodeURIComponent(url);

fetch(proxyUrl)
    .then(response => response.text())
    .then(data => {
        // Process HTML response
    });
Reference: scripts.js:111
Third-Party Dependency: AllOrigins (api.allorigins.win) is a public CORS proxy. Service availability is not guaranteed. Consider hosting your own proxy for production use.

Invidious Instance Selection

SoundWave implements a fallback system across multiple instances:
function fetchSuggestions(songName) {
    const url1 = 'https://inv.nadeko.net/search?q=' + encodeURIComponent(songName);
    const url2 = 'https://yewtu.be/search?q=' + encodeURIComponent(songName);

    fetch('https://api.allorigins.win/raw?url=' + encodeURIComponent(url1))
        .then(response => {
            if (response.ok) return response.text();
            throw new Error('Network response was not ok.');
        })
        .then(data => {
            processSuggestionsData(data);
        })
        .catch(() => {
            // Fallback to second instance
            fetch('https://api.allorigins.win/raw?url=' + encodeURIComponent(url2))
                .then(response => {
                    if (response.ok) return response.text();
                    throw new Error('Network response was not ok.');
                })
                .then(data => {
                    processSuggestionsData(data);
                })
                .catch(error => {
                    console.error('Error al obtener sugerencias:', error);
                    suggestionsContainer.innerHTML = '<div class="suggestion">Error al cargar las sugerencias.</div>';
                });
        });
}
Reference: scripts.js:105-134
Primary Instance: inv.nadeko.netFallback Instance: yewtu.beIf both fail, an error message is displayed to the user.

HTML Parsing Strategy

Invidious responses are HTML pages that must be parsed:
function processSuggestionsData(data) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(data, 'text/html');
    const divs = doc.querySelectorAll('div.video-card-row');

    const newSuggestions = Array.from(divs)
        .map(div => {
            const link = div.querySelector('a');
            const titleElement = link ? link.querySelector('p') : null;
            const title = titleElement ? titleElement.innerText : 'Sin título';
            const videoId = link ? link.getAttribute('href').split('v=')[1] : null;
            const lengthElement = div.querySelector('.length');
            const length = lengthElement ? lengthElement.innerText : 'Desconocida';
            return { title, videoId, length };
        })
        .filter(suggestion => {
            const searchTerm = originalSearchTerm.toLowerCase();
            const songTitle = suggestion.title.toLowerCase();

            const includesSearchTerm = (
                songTitle.includes(searchTerm) ||
                searchTerm.split(' ').every(term => songTitle.includes(term))
            );

            const excludesOfficial = !songTitle.includes('oficial') && !songTitle.includes('official');

            return includesSearchTerm && excludesOfficial;
        });

    const limitedSuggestions = newSuggestions.slice(0, 5);
    
    if (JSON.stringify(limitedSuggestions) !== JSON.stringify(currentSuggestions)) {
        currentSuggestions = limitedSuggestions;
        showItems(currentSuggestions, suggestionsContainer, true);
    }
}
Reference: scripts.js:137-178

Multi-Page Search Implementation

async function fetchSearchResults(songName) {
    const maxPages = 6;
    let allResults = [];

    for (let page = 1; page <= maxPages; page++) {
        const url = `https://yewtu.be/search?q=${encodeURIComponent(songName)}&page=${page}`;

        try {
            const response = await fetch('https://api.allorigins.win/raw?url=' + encodeURIComponent(url));
            if (!response.ok) throw new Error('Network response was not ok.');

            const data = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(data, 'text/html');
            const divs = doc.querySelectorAll('div.video-card-row');

            const searchResults = Array.from(divs)
                .map(div => {
                    const link = div.querySelector('a');
                    const titleElement = link ? link.querySelector('p') : null;
                    const title = titleElement ? titleElement.innerText : 'Sin título';
                    const videoId = link ? link.getAttribute('href').split('v=')[1] : null;
                    return { title, videoId };
                })
                .filter(result => {
                    const searchTerm = originalSearchTerm.toLowerCase();
                    const songTitle = result.title.toLowerCase();

                    const includesSearchTerm = (
                        songTitle.includes(searchTerm) ||
                        searchTerm.split(' ').every(term => songTitle.includes(term))
                    );

                    const excludesOfficial = !songTitle.includes('oficial') && !songTitle.includes('official');

                    return includesSearchTerm && excludesOfficial;
                });

            allResults = allResults.concat(searchResults);
        } catch (error) {
            console.error('There was a problem with the fetch operation:', error);
        }
    }

    // Group results in blocks of 5
    groupedResults = [];
    for (let i = 0; i < allResults.length; i += 5) {
        groupedResults.push(allResults.slice(i, i + 5));
    }

    currentGroupIndex = 0;
    showGroupedResults();
}
Reference: scripts.js:181-239
URL Pattern
GET
https://yewtu.be/search?q={QUERY}&page={PAGE_NUMBER}
q
string
required
URL-encoded search query
page
integer
default:"1"
Page number for pagination (1-based index)
The application searches up to 6 pages sequentially, aggregating all results before displaying them. This ensures comprehensive search coverage.

Audio Source Extraction

function fetchSourceCode(videoId, groupIndex = currentGroupIndex, resultIndex = 0) {
    const watchUrl = `https://yewtu.be/watch?v=${videoId}&listen=1`;
    const allOriginsUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(watchUrl)}`;

    fetch(allOriginsUrl)
        .then(response => {
            if (response.ok) return response.text();
            throw new Error('Network response was not ok.');
        })
        .then(data => {
            const parser = new DOMParser();
            const doc = parser.parseFromString(data, 'text/html');
            const sourceElements = doc.querySelectorAll('source[src]');
            const audioElements = doc.querySelectorAll('audio[src]');

            const sourceLinks = Array.from(sourceElements).map(source => source.src);
            const audioLinks = Array.from(audioElements).map(audio => audio.src);
            const allLinks = [...sourceLinks, ...audioLinks];

            checkAccessibleLinks(allLinks, videoId, groupIndex, resultIndex);
        })
        .catch(error => {
            console.error('Error fetching source code:', error);
            sourceLinksContainer.textContent = 'No se pudo cargar el código fuente.';
            findNextVideo(groupIndex, resultIndex);
        });
}
Reference: scripts.js:287-316
Watch URL
GET
https://yewtu.be/watch?v={VIDEO_ID}&listen=1
v
string
required
YouTube video identifier
listen
integer
default:"1"
Audio-only mode flag (reduces bandwidth)

Error Handling Patterns

Network Failures

fetch(url)
    .then(response => {
        if (response.ok) return response.text();
        throw new Error('Network response was not ok.');
    })
    .catch(error => {
        console.error('Error:', error);
        // Fallback logic or user notification
    });

Retry Logic

The Invidious integration implements automatic retry with fallback instances:
  1. Try primary instance (inv.nadeko.net)
  2. On failure, try secondary instance (yewtu.be)
  3. On failure, display error message

User Feedback

if (response.ok) {
    // Success path
} else {
    errorMessage.textContent = data.error || 'Failed to retrieve download link';
}
Reference: index2.html:104-115

Rate Limiting Considerations

Spotify API: Rate limits are managed by the token server. Excessive requests may result in temporary blocks.
Invidious: Public instances may have rate limits. The multi-page search makes 6 sequential requests, which could trigger rate limiting.

Mitigation Strategies

  1. Debouncing: 300ms delay on search input reduces unnecessary requests
  2. Result Caching: Store currentSuggestions to avoid redundant updates
  3. Instance Rotation: Fallback system distributes load across multiple servers

Best Practices

Always Encode Parameters

Use encodeURIComponent() for all user input in URLs to prevent injection attacks

Implement Fallbacks

Always have backup API endpoints or instances for critical functionality

Validate Responses

Check response.ok before processing data to handle HTTP errors gracefully

User Feedback

Display clear error messages when API requests fail

Testing API Integration

Spotify Token Test

// Test token acquisition
fetch('https://jsnode-ab0e.onrender.com', { method: 'POST' })
    .then(res => res.json())
    .then(data => console.log('Token:', data.access_token))
    .catch(err => console.error('Token error:', err));

Invidious Instance Test

// Test instance availability
const testUrl = 'https://api.allorigins.win/raw?url=' + 
    encodeURIComponent('https://inv.nadeko.net/search?q=test');
    
fetch(testUrl)
    .then(res => res.text())
    .then(html => console.log('Instance available:', html.length > 0))
    .catch(err => console.error('Instance error:', err));

Next Steps

Architecture

Understand the overall application structure

Styling System

Learn about the CSS architecture and theming

Build docs developers (and LLMs) love