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
POST https://jsnode-ab0e.onrender.com
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
GET https://jsnode-ab0e.onrender.com/search
Search term for song, artist, or album
Type of search result (track, artist, album, playlist)
Previously obtained Spotify access token
Response Structure
Container for track search results Array of track objects Spotify track ID used for embed URLs
Spotify Embed Integration
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
https://open.spotify.com/embed/track/{TRACK_ID}?utm_source=generator&theme=0
Spotify track identifier from search results
utm_source
string
default: "generator"
Tracking parameter for embed analytics
Theme setting: 0 for dark theme, 1 for light theme
Download Link Generator API
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
POST https://jsnode-ab0e.onrender.com/get-download-link
Full Spotify track URL (e.g., https://open.spotify.com/track/...)
Direct URL to audio file for download/playback
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
Show HTML Structure Expected
Invidious search results follow this structure: < div class = "video-card-row" >
< a href = "/watch?v={VIDEO_ID}" >
< p > {Video Title} </ p >
</ a >
< div class = "length" > {Duration} </ div >
</ div >
The parser extracts:
Video ID : From the href attribute after v=
Title : From the <p> element text
Length : From the .length element text
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
https://yewtu.be/search?q={QUERY}&page={PAGE_NUMBER}
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.
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
https://yewtu.be/watch?v={VIDEO_ID}&listen=1
Audio-only mode flag (reduces bandwidth)
The function searches for two types of HTML elements: < source src = "https://example.com/audio.m4a" />
< audio src = "https://example.com/audio.m4a" ></ audio >
All src attributes are collected and validated for accessibility.
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:
Try primary instance (inv.nadeko.net)
On failure, try secondary instance (yewtu.be)
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
Debouncing : 300ms delay on search input reduces unnecessary requests
Result Caching : Store currentSuggestions to avoid redundant updates
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