// ==UserScript==
// @name Spotify Enhancer (Spotify to YouTube)
// @description Easily find and open YouTube videos for any Spotify track with a single click.
// @icon https://raw.githubusercontent.com/exyezed/spotify-enhancer/refs/heads/main/extras/spotify-enhancer.png
// @version 1.2
// @author exyezed
// @namespace https://github.com/exyezed/spotify-enhancer/
// @supportURL https://github.com/exyezed/spotify-enhancer/issues
// @license MIT
// @match https://open.spotify.com/*
// @grant GM_openInTab
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000;
const spinnerSVG = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="spinner-icon" style="
width: 36px;
height: 36px;
margin-left: 15px;
vertical-align: middle;
cursor: pointer;
transition: transform 0.2s ease;
display: none;
">
<defs>
<style>.fa-secondary{opacity:.4}</style>
</defs>
<path class="fa-secondary" fill="#FFFFFF" d="M0 256C0 114.9 114.1 .5 255.1 0C237.9 .5 224 14.6 224 32c0 17.7 14.3 32 32 32C150 64 64 150 64 256s86 192 192 192c69.7 0 130.7-37.1 164.5-92.6c-3 6.6-3.3 14.8-1 22.2c1.2 3.7 3 7.2 5.4 10.3c1.2 1.5 2.6 3 4.1 4.3c.8 .7 1.6 1.3 2.4 1.9c.4 .3 .8 .6 1.3 .9s.9 .6 1.3 .8c5 2.9 10.6 4.3 16 4.3c11 0 21.8-5.7 27.7-16c-44.3 76.5-127 128-221.7 128C114.6 512 0 397.4 0 256z"/>
<path class="fa-primary" fill="#FFFFFF" d="M224 32c0-17.7 14.3-32 32-32C397.4 0 512 114.6 512 256c0 46.6-12.5 90.4-34.3 128c-8.8 15.3-28.4 20.5-43.7 11.7s-20.5-28.4-11.7-43.7c16.3-28.2 25.7-61 25.7-96c0-106-86-192-192-192c-17.7 0-32-14.3-32-32z"/>
</svg>
`;
const youtubeIconSVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" class="youtube-icon" style="
margin-left: 10px;
vertical-align: middle;
cursor: pointer;
transition: transform 0.2s ease;
">
<path fill="#FF0033" d="M21.58,7.19c-0.23-0.86-0.91-1.54-1.77-1.77C18.25,5,12,5,12,5S5.75,5,4.19,5.42C3.33,5.65,2.65,6.33,2.42,7.19C2,8.75,2,12,2,12s0,3.25,0.42,4.81c0.23,0.86,0.91,1.54,1.77,1.77C5.75,19,12,19,12,19s6.25,0,7.81-0.42c0.86-0.23,1.54-0.91,1.77-1.77C22,15.25,22,12,22,12S22,8.75,21.58,7.19z"/>
<polygon fill="#FFFFFF" points="10,15 15,12 10,9"/>
</svg>
`;
function insertSVGIconNextToH1() {
if (!window.location.href.includes('/track/')) {
return;
}
const h1Elements = document.querySelectorAll('h1');
const iconContainer = document.createElement('div');
iconContainer.style.display = 'inline-block';
iconContainer.style.position = 'relative';
h1Elements.forEach(h1 => {
if (!h1.querySelector('.youtube-icon')) {
iconContainer.innerHTML = youtubeIconSVG + spinnerSVG;
const youtubeIcon = iconContainer.querySelector('.youtube-icon');
const spinnerIcon = iconContainer.querySelector('.spinner-icon');
const styleTag = document.createElement('style');
styleTag.textContent = `
.youtube-icon:hover, .spinner-icon:hover {
transform: scale(1.1);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.spinner-icon {
animation: spin 1s linear infinite;
}
`;
document.head.appendChild(styleTag);
const trackId = extractTrackId();
youtubeIcon.addEventListener('click', () => {
youtubeIcon.style.display = 'none';
spinnerIcon.style.display = 'inline-block';
fetchYouTubeLink(trackId, youtubeIcon, spinnerIcon);
});
h1.appendChild(iconContainer);
}
});
}
function extractTrackId() {
const urlMatch = window.location.href.match(/track\/([a-zA-Z0-9]+)/);
return urlMatch ? urlMatch[1] : null;
}
function fetchYouTubeLink(trackId, youtubeIcon, spinnerIcon) {
if (!trackId) {
console.error('No track ID found');
return;
}
const cachedData = GM_getValue(`youtube_link_${trackId}`);
if (cachedData && (Date.now() - cachedData.timestamp) < CACHE_DURATION) {
GM_openInTab(cachedData.youtube_url, { active: true });
youtubeIcon.style.display = 'inline-block';
spinnerIcon.style.display = 'none';
return;
}
fetch(`https://spotapis.vercel.app/track/${trackId}`)
.then(response => response.json())
.then(data => {
if (data.youtube_url) {
GM_setValue(`youtube_link_${trackId}`, {
youtube_url: data.youtube_url,
timestamp: Date.now()
});
GM_openInTab(data.youtube_url, { active: true });
} else {
console.error('No YouTube URL found');
}
})
.catch(error => {
console.error('Error fetching YouTube link:', error);
})
.finally(() => {
youtubeIcon.style.display = 'inline-block';
spinnerIcon.style.display = 'none';
});
}
function removeYouTubeIcon() {
const iconContainer = document.querySelector('.youtube-icon').parentNode;
if (iconContainer) {
iconContainer.remove();
}
}
function handleURLChange() {
removeYouTubeIcon();
insertSVGIconNextToH1();
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
insertSVGIconNextToH1();
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
const urlObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
handleURLChange();
}
});
});
urlObserver.observe(document.querySelector('title'), {
subtree: true,
characterData: true,
childList: true
});
insertSVGIconNextToH1();
console.log('Spotify Enhancer (Spotify to YouTube) is running');
})();