// ==UserScript==
// @name Deez Notes
// @namespace http://tampermonkey.net/
// @version 2.1
// @description Add label, UPC, genres, release dates, and tracklist copy to Deezer album pages
// @author waiter7
// @contributors ilikepeaches
// @match https://www.deezer.com/*
// @license MIT
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Debug mode - set to true to enable console logging
const DEBUG = false;
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver((mutations, obs) => {
const element = document.querySelector(selector);
if (element) {
obs.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Element '${selector}' not found within timeout`));
}, timeout);
});
}
function formatDate(dateString) {
if (!dateString || dateString === '0000-00-00') return 'N/A';
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
timeZone: 'UTC'
});
} catch (e) {
return dateString;
}
}
function extractAlbumId() {
const path = window.location.pathname;
const match = path.match(/\/album\/(\d+)/);
return match ? match[1] : null;
}
function addTracklistCopyIcon(apiData) {
// Find the track count element
const trackCountElement = document.querySelector('li.dMJfv');
if (!trackCountElement || trackCountElement.querySelector('.tracklist-copy-icon')) {
return; // Element not found or icon already added
}
// Create clipboard icon
const copyIcon = document.createElement('span');
copyIcon.className = 'tracklist-copy-icon';
copyIcon.innerHTML = '⧉';
copyIcon.title = 'Copy Tracklist';
copyIcon.style.cssText = `
margin-left: 8px;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s ease;
font-size: 14px;
color: #666;
background: rgba(255, 255, 255, 0.9);
border-radius: 3px;
padding: 2px 4px;
`;
// Add hover effect
copyIcon.addEventListener('mouseenter', () => {
copyIcon.style.opacity = '1';
});
copyIcon.addEventListener('mouseleave', () => {
copyIcon.style.opacity = '0.7';
});
// Add click handler
copyIcon.addEventListener('click', async () => {
// Show loading state
const originalText = copyIcon.innerHTML;
copyIcon.innerHTML = '⏳';
copyIcon.style.cursor = 'wait';
try {
// Format tracklist from API data
const tracklist = apiData.tracks?.data?.length
? apiData.tracks.data.map((track, index) => {
const minutes = Math.floor(track.duration / 60);
const seconds = track.duration % 60;
const formattedDuration = `${minutes}:${seconds.toString().padStart(2, '0')}`;
return `${index + 1}. ${track.title} (${formattedDuration})`;
}).join('\n')
: 'No tracks available';
await navigator.clipboard.writeText(tracklist);
// Show success feedback
copyIcon.innerHTML = '✓';
copyIcon.style.color = '#4CAF50';
setTimeout(() => {
copyIcon.innerHTML = originalText;
copyIcon.style.color = '#666';
copyIcon.style.cursor = 'pointer';
}, 1500);
if (DEBUG) console.log('Tracklist copied to clipboard');
} catch (error) {
console.error('Error copying tracklist:', error);
// Show error feedback
copyIcon.innerHTML = '✗';
copyIcon.style.color = '#f44336';
setTimeout(() => {
copyIcon.innerHTML = originalText;
copyIcon.style.color = '#666';
copyIcon.style.cursor = 'pointer';
}, 1500);
}
});
// Add the icon to the track count element
trackCountElement.appendChild(copyIcon);
}
function copyToClipboard(text, element) {
navigator.clipboard.writeText(text).then(() => {
const copiedMsg = element.querySelector('.copied-message');
if (copiedMsg) {
copiedMsg.style.opacity = '1';
copiedMsg.style.visibility = 'visible';
setTimeout(() => {
copiedMsg.style.opacity = '0';
setTimeout(() => copiedMsg.style.visibility = 'hidden', 200);
}, 1000);
}
}).catch(err => {
if (DEBUG) console.error('Failed to copy text:', err);
});
}
function createCopyableCell(content, isApiCell = false, isGenresCell = false) {
const td = document.createElement('td');
if (isApiCell) {
// Handle API cell differently - no copy functionality
td.style.cssText = `
padding: 12px;
vertical-align: top;
background: #ffffff;
text-align: center;
border-top: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
`;
const apiLink = document.createElement('a');
apiLink.href = content;
apiLink.target = '_blank';
apiLink.textContent = 'View API';
apiLink.style.cssText = `
color: #a238ff; text-decoration: none; font-weight: 500;
padding: 4px 8px; border-radius: 4px; background: #f8f5ff;
border: 1px solid #a238ff; display: inline-block; transition: all 0.2s ease;
`;
// Add hover effects for API link
apiLink.addEventListener('mouseenter', () => {
apiLink.style.background = '#a238ff';
apiLink.style.color = '#ffffff';
});
apiLink.addEventListener('mouseleave', () => {
apiLink.style.background = '#f8f5ff';
apiLink.style.color = '#a238ff';
});
td.appendChild(apiLink);
} else {
// Handle regular data cells
td.textContent = content || 'N/A';
td.style.cssText = `
padding: 12px;
border-right: 1px solid #dee2e6;
border-top: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
vertical-align: top;
background: #ffffff;
position: relative;
cursor: pointer;
transition: background-color 0.2s ease;
`;
// Make "Requires Page Refresh" italic and add refresh functionality
const isRefreshCell = content === 'Requires Page Refresh';
if (isRefreshCell) {
td.style.fontStyle = 'italic';
td.style.color = '#666';
}
// Add copy/refresh icon
const actionIcon = document.createElement('span');
actionIcon.innerHTML = isRefreshCell ? '🔄' : '⧉';
actionIcon.title = isRefreshCell ? 'Refresh Page' : 'Copy to Clipboard';
actionIcon.style.cssText = `
position: absolute;
top: 8px;
right: 8px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
font-size: 14px;
color: #666;
background: rgba(255, 255, 255, 0.9);
border-radius: 3px;
padding: 2px 4px;
`;
// Add copied message
const copiedMessage = document.createElement('span');
copiedMessage.className = 'copied-message';
copiedMessage.textContent = 'Copied';
copiedMessage.style.cssText = `
position: absolute;
bottom: 8px;
right: 8px;
color: #666;
font-size: 10px;
font-style: italic;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease;
white-space: nowrap;
z-index: 1000;
pointer-events: none;
`;
td.appendChild(actionIcon);
td.appendChild(copiedMessage);
// Show/hide action icon on hover
td.addEventListener('mouseenter', () => {
actionIcon.style.opacity = '0.7';
td.style.backgroundColor = '#f8f9fa';
});
td.addEventListener('mouseleave', () => {
actionIcon.style.opacity = '0';
td.style.backgroundColor = '#ffffff';
});
// Click functionality - refresh or copy
td.addEventListener('click', () => {
if (isRefreshCell) {
location.reload();
} else {
let copyText = content || 'N/A';
if (isGenresCell && copyText !== 'N/A' && copyText !== 'Loading...') {
// Format genres for copying: lowercase, trim spaces around commas, replace spaces within genres with periods
copyText = copyText.toLowerCase()
.split(',')
.map(genre => genre.trim().replace(/\s+/g, '.'))
.join(',');
}
copyToClipboard(copyText, td);
}
});
}
return td;
}
async function addMetadata() {
try {
// Check if metadata table already exists
if (document.querySelector('.custom-metadata-table')) {
if (DEBUG) console.log('Metadata table already exists, skipping...');
return true;
}
const albumId = extractAlbumId();
if (!albumId) {
if (DEBUG) console.log('Could not extract album ID from URL');
return false;
}
// Find the play button container for positioning
const playButtonContainer = document.querySelector('[data-testid="play"]');
if (!playButtonContainer) {
if (DEBUG) console.log('Play button container not found');
return false;
}
const buttonStrip = playButtonContainer.parentElement.parentElement;
// Always fetch from API first to get all available data
if (DEBUG) console.log('Fetching album data from API...');
let apiData;
try {
const response = await fetch(`https://api.deezer.com/album/${albumId}`);
apiData = await response.json();
} catch (error) {
if (DEBUG) console.log('API fetch failed, will retry...');
return false;
}
// Extract data from API
const label = apiData.label || 'N/A';
const upc = apiData.upc || 'N/A';
const genres = apiData.genres?.data?.length
? apiData.genres.data.map(genre => genre.name).join(', ')
: 'N/A';
const originalReleaseDate = formatDate(apiData.release_date);
// Get additional date fields from app state (only works on fresh page loads)
let digitalReleaseDate, physicalReleaseDate;
if (isFreshPageLoad) {
const appState = window.__DZR_APP_STATE__;
if (appState?.DATA) {
if (DEBUG) console.log('Fresh page load - using app state for additional dates...');
digitalReleaseDate = formatDate(appState.DATA.ORIGINAL_RELEASE_DATE);
physicalReleaseDate = formatDate(appState.DATA.PHYSICAL_RELEASE_DATE);
} else {
if (DEBUG) console.log('Fresh page load but no app state, using refresh message...');
digitalReleaseDate = physicalReleaseDate = 'Requires Page Refresh';
}
} else {
if (DEBUG) console.log('SPA navigation detected, using refresh message for missing dates...');
digitalReleaseDate = physicalReleaseDate = 'Requires Page Refresh';
}
if (DEBUG) {
console.log('Album metadata:', {
label,
upc,
genres,
digitalReleaseDate,
physicalReleaseDate,
originalReleaseDate
});
}
// Create a clean table for metadata
const tableContainer = document.createElement('div');
tableContainer.className = 'custom-metadata-table';
tableContainer.style.cssText = `
margin-top: 19px;
margin-bottom: 19px;
background: #f8f9fa;
border-radius: 8px;
padding: 14px;
border: 1px solid #e9ecef;
`;
const table = document.createElement('table');
table.style.cssText = `
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 13px;
color: #495057;
`;
// Create table header
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
headerRow.style.cssText = `
border-top: 1px solid #dee2e6;
border-bottom: 2px solid #dee2e6;
`;
const headers = ['Label', 'UPC', 'Genres', 'Digital Release', 'Physical Release', 'Original Release', 'API'];
headers.forEach((headerText, index) => {
const th = document.createElement('th');
th.textContent = headerText;
th.style.cssText = `
text-align: left;
padding: 8px 12px;
font-weight: 600;
color: #343a40;
background: #e9ecef;
border-right: 1px solid #dee2e6;
border-top: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
`;
if (index === 0) th.style.borderLeft = '1px solid #dee2e6';
if (headerText === 'API') th.style.borderRight = '1px solid #dee2e6';
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create table body
const tbody = document.createElement('tbody');
const dataRow = document.createElement('tr');
dataRow.style.cssText = `
background-color: #ffffff;
border-bottom: 1px solid #dee2e6;
`;
const values = [label, upc, genres, digitalReleaseDate, physicalReleaseDate, originalReleaseDate];
// Add data cells with copy functionality
values.forEach((value, index) => {
const isGenresCell = index === 2; // genres is the 3rd item (index 2)
const td = createCopyableCell(value, false, isGenresCell);
if (index === 0) td.style.borderLeft = '1px solid #dee2e6';
dataRow.appendChild(td);
});
// Add API link cell with copy functionality
const apiUrl = `https://api.deezer.com/album/${albumId}`;
const apiTd = createCopyableCell(apiUrl, true);
apiTd.style.borderRight = '1px solid #dee2e6';
dataRow.appendChild(apiTd);
tbody.appendChild(dataRow);
table.appendChild(tbody);
tableContainer.appendChild(table);
// Insert the table after the button strip
buttonStrip.parentNode.insertBefore(tableContainer, buttonStrip.nextSibling);
// Add tracklist copy icon (uses the same apiData we already fetched)
addTracklistCopyIcon(apiData);
if (DEBUG) console.log('Metadata added successfully');
return true;
} catch (error) {
console.error('Error adding metadata:', error);
return false;
}
}
function init() {
if (DEBUG) console.log('Deez Notes: Starting...');
// Check if we're on an album page
if (!location.href.includes('/album/')) {
if (DEBUG) console.log('Not on an album page, skipping...');
return;
}
// Wait for the page to load and the play button to be available
waitForElement('[data-testid="play"]')
.then(() => {
if (DEBUG) console.log('Play button found, attempting to add custom metadata...');
// Try to add metadata with multiple retries for SPA navigation
let attempts = 0;
const maxAttempts = 5;
async function tryAddMetadata() {
attempts++;
try {
const success = await addMetadata();
if (success) {
if (DEBUG) console.log('Metadata added successfully on attempt', attempts);
return;
}
} catch (error) {
if (DEBUG) console.log('Error in addMetadata:', error);
}
if (attempts < maxAttempts) {
if (DEBUG) console.log(`Attempt ${attempts} failed, retrying in ${attempts}s...`);
setTimeout(tryAddMetadata, attempts * 1000); // 1s, 2s, 3s, 4s delays
} else {
if (DEBUG) console.log('Max attempts reached, giving up');
}
}
tryAddMetadata();
})
.catch(error => {
console.error('Failed to find play button:', error);
});
}
// Enhanced navigation detection for SPA
let lastUrl = location.href;
if (DEBUG) console.log('Initial URL:', lastUrl);
// Track if this is a fresh page load or SPA navigation
let isFreshPageLoad = true;
// Simple navigation detection like the working old code
if (DEBUG) console.log('Setting up MutationObserver...');
new MutationObserver(() => {
// Only check URL on mutations, don't log every trigger
const url = location.href;
if (url !== lastUrl) {
if (DEBUG) console.log(`Navigation detected: ${lastUrl} → ${url}`);
lastUrl = url;
if (url.includes('/album/')) {
if (DEBUG) console.log('Album page detected, initializing...');
// Mark as SPA navigation for subsequent runs
isFreshPageLoad = false;
// Use the same approach as the working old code
setTimeout(init, 1000);
} else {
if (DEBUG) console.log('Not an album page, skipping...');
}
}
}).observe(document.body, {
subtree: true,
childList: true
});
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();