// ==UserScript==
// @name DEEZ Notes
// @namespace http://tampermonkey.net/
// @version 3.3.0
// @description Drops DEEZ NOTES on the album page to easily view metadata like label, UPC, genres, release dates, BBCode tracklist export, and ptpimg cover rehosting
// @author waiter7
// @contributors ilikepeaches
// @match https://www.deezer.com/*
// @license MIT
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// === CONSTANTS ===
const DEBUG = true;
const NON_COPYABLE_STATES = ['Loading Genres...', 'Loading...', 'API Call Failed', 'N/A', 'None provided'];
const SELECTORS = {
PLAY_BUTTON: '[data-testid="play"]',
UL_ELEMENT: 'ul.css-1s16397'
};
// === STATE ===
let interceptedAlbumData = null;
let cachedTrackDetails = {};
let isInitializing = false; // Guard against concurrent initialization
let currentAlbumId = null; // Track current album to prevent duplicate work
// === FETCH INTERCEPTION ===
// Intercept Deezer's internal API calls to capture album data
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = function(...args) {
const url = args[0];
return originalFetch.apply(this, args).then(async response => {
const clonedResponse = response.clone();
// Intercept album page data requests
if (url && typeof url === 'string' && url.includes('deezer.pageAlbum')) {
try {
const data = await clonedResponse.json();
if (data.results && data.results.DATA && data.results.SONGS) {
interceptedAlbumData = data.results;
if (DEBUG) {
console.log('[Deez Notes] Album data intercepted:', {
id: interceptedAlbumData.DATA.ALB_ID,
title: interceptedAlbumData.DATA.ALB_TITLE,
tracks: interceptedAlbumData.SONGS.data.length
});
}
// Dispatch event for UI updates
window.dispatchEvent(new CustomEvent('deezNotesAlbumLoaded', {
detail: interceptedAlbumData
}));
}
} catch(e) {
if (DEBUG) console.error('[Deez Notes] Failed to parse intercepted album data:', e);
}
}
return response;
});
};
if (DEBUG) {
console.log('[Deez Notes] Fetch interception initialized');
}
// Check if data already exists in window.__DZR_APP_STATE__ on initial load
function checkInitialAppState() {
const appState = unsafeWindow.__DZR_APP_STATE__;
if (appState?.DATA && appState?.SONGS?.data) {
if (DEBUG) {
console.log('[Deez Notes] Found existing __DZR_APP_STATE__ on page load:', {
id: appState.DATA.ALB_ID,
title: appState.DATA.ALB_TITLE,
tracks: appState.SONGS.data.length
});
}
// Use the existing data
interceptedAlbumData = {
DATA: appState.DATA,
SONGS: appState.SONGS
};
// Dispatch event
window.dispatchEvent(new CustomEvent('deezNotesAlbumLoaded', {
detail: interceptedAlbumData
}));
return true;
}
return false;
}
function injectThemeStyles() {
if (document.getElementById('w7dn_styles')) return;
const style = document.createElement('style');
style.id = 'w7dn_styles';
style.innerHTML = `
:root {
--dn-table-border: #f5f3f8;
--dn-header-bg: #f5f3f8;
--dn-header-text: #343a40;
--dn-cell-bg: #ffffff;
--dn-cell-bg-hover: #f8f9fa;
--dn-cell-text: #495057;
--dn-divider-line: #dee2e6;
--dn-icon-bg: rgba(255, 255, 255, 0.9);
--dn-icon-text: #666;
--dn-api-bg: #f5f2f8;
--dn-api-text: #a238ff;
--dn-api-border: #a238ff;
--dn-api-bg-hover: #a238ff;
--dn-api-text-hover: #ffffff;
--dn-api-border-hover: #a238ff;
}
html[data-theme="dark"] {
--dn-table-border: #4e4b51;
--dn-header-bg: #131215;
--dn-header-text: #ffffff;
--dn-cell-bg: #000000;
--dn-cell-bg-hover: #1c1c24;
--dn-cell-text: #a19fa4;
--dn-divider-line: #39383b;
--dn-icon-bg: #000000;
--dn-icon-text: #a19fa4;
--dn-api-bg: #a238ff;
--dn-api-text: #ffffff;
--dn-api-bg-hover: #f5f2f8;
--dn-api-text-hover: #a238ff;
--dn-api-border-hover: #f5f2f8; /* Match hover background */
}
.w7dn_api-link {
color: var(--dn-api-text);
background: var(--dn-api-bg);
border: 1px solid var(--dn-api-border);
text-decoration: none;
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
transition: all 0.2s ease;
}
.w7dn_api-link:hover {
background: var(--dn-api-bg-hover);
color: var(--dn-api-text-hover);
border-color: var(--dn-api-border-hover);
}
.w7dn_api-link:disabled {
background: var(--dn-cell-bg-hover);
color: var(--dn-icon-text);
border-color: var(--dn-divider-line);
cursor: not-allowed;
opacity: 0.6;
}
.w7dn_copyable-cell:hover {
background-color: var(--dn-cell-bg-hover) !important;
}
.w7dn_copyable-cell.w7dn_na-cell:hover {
background-color: var(--dn-cell-bg) !important;
}
.w7dn_cover-rehost-overlay {
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%) translateY(calc(100% + 8px));
background: var(--dn-api-border);
color: white;
text-align: center;
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s ease, opacity 0.2s ease;
z-index: 10;
user-select: none;
border-radius: 4px;
white-space: nowrap;
}
.w7dn_cover-container:hover .w7dn_cover-rehost-overlay {
transform: translateX(-50%) translateY(0);
}
.w7dn_cover-rehost-overlay.w7dn_stay-visible {
transform: translateX(-50%) translateY(0) !important;
}
.w7dn_cover-rehost-overlay:hover {
opacity: 0.85;
}
.w7dn_cover-container {
position: relative;
overflow: hidden;
}
.w7dn_bbcode-track-button {
color: white !important;
background: var(--dn-api-border) !important;
border: none !important;
font-weight: 500;
padding: 2px 8px 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
margin-left: 8px;
display: inline-block;
transition: opacity 0.2s ease;
}
.w7dn_bbcode-track-button:hover {
opacity: 0.85;
}
.w7dn_bbcode-track-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@keyframes w7dn_fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.w7dn_actions-area {
animation: w7dn_fadeIn 0.1s ease-in;
}
/* Smooth transition for elements that shift when action bar is added */
ul.css-1s16397 {
transition: transform 0.3s ease, margin 0.3s ease;
}
`;
// Wait for document.head to be available before appending
if (document.head) {
document.head.appendChild(style);
} else {
const checkHead = setInterval(() => {
if (document.head) {
clearInterval(checkHead);
document.head.appendChild(style);
}
}, 10);
}
}
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: 'long', // Changed from '2-digit' to 'long'
day: 'numeric', // Changed from '2-digit' to 'numeric'
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 extractArtistId() {
const path = window.location.pathname;
const match = path.match(/\/artist\/(\d+)/);
return match ? match[1] : null;
}
function isAlbumPage() {
return window.location.pathname.includes('/album/');
}
function isArtistPage() {
return window.location.pathname.includes('/artist/');
}
function copyToClipboard(text, element) {
navigator.clipboard.writeText(text).then(() => {
const actionIcon = element.querySelector('span:not(.w7dn_copied-message)');
const copiedMsg = element.querySelector('.w7dn_copied-message');
if (actionIcon && copiedMsg) {
// Hide the copy icon
actionIcon.style.opacity = '0';
actionIcon.style.visibility = 'hidden';
// Show "Copied!" in the same position
copiedMsg.style.opacity = '1';
copiedMsg.style.visibility = 'visible';
setTimeout(() => {
// Hide "Copied!" and restore copy icon
copiedMsg.style.opacity = '0';
setTimeout(() => {
copiedMsg.style.visibility = 'hidden';
actionIcon.style.visibility = 'visible';
}, 200);
}, 1000);
}
}).catch(err => {
if (DEBUG) console.error('Failed to copy text:', err);
});
}
// Get cover URL from page image with size fallback
async function getCoverUrlFromPage() {
// Find the cover image on the page
let imgElement;
if (isAlbumPage()) {
imgElement = document.querySelector('div[role="group"][data-testid="album-cover"] img');
} else if (isArtistPage()) {
imgElement = document.querySelector('div[role="group"][data-testid="artist-cover"] img');
}
if (!imgElement || !imgElement.src) {
throw new Error('Could not find cover image on page');
}
const originalUrl = imgElement.src;
if (DEBUG) console.log('[Deez Notes] Original image URL:', originalUrl);
// Try different sizes: 1500x1500, 1200x1200, 1000x1000, original
const sizes = ['1500x1500', '1200x1200', '1000x1000'];
for (const size of sizes) {
const testUrl = originalUrl.replace(/\d+x\d+/, size);
if (DEBUG) console.log(`[Deez Notes] Testing ${size}:`, testUrl);
try {
const response = await fetch(testUrl, { method: 'HEAD' });
if (response.ok) {
if (DEBUG) console.log(`[Deez Notes] Using ${size} image:`, testUrl);
return testUrl;
}
} catch (e) {
if (DEBUG) console.log(`[Deez Notes] Failed to fetch ${size}:`, e);
}
}
// Fallback to original URL
if (DEBUG) console.log('[Deez Notes] Using original URL:', originalUrl);
return originalUrl;
}
// Rehost cover using page image with size fallback (simplified version without status display)
async function rehostCoverImageSimple() {
// Get or prompt for API key
let apiKey = localStorage.getItem('w7dn_ptpimg_api_key');
if (!apiKey) {
apiKey = prompt('Please enter your ptpimg API key:');
if (!apiKey || !apiKey.trim()) {
throw new Error('No API key provided');
}
localStorage.setItem('w7dn_ptpimg_api_key', apiKey.trim());
}
// Get the best quality cover URL from page with fallback
const coverUrl = await getCoverUrlFromPage();
// Upload to ptpimg
const formData = new FormData();
formData.append('api_key', apiKey);
formData.append('link-upload', coverUrl);
formData.append('upload-links', '');
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://ptpimg.me/upload.php',
headers: {
'Referer': 'https://ptpimg.me/index.php'
},
data: formData,
onload: (response) => {
if (response.status === 200) {
try {
const result = JSON.parse(response.responseText);
if (Array.isArray(result) && result.length > 0 && result[0].code && result[0].ext) {
const ptpimgUrl = `https://ptpimg.me/${result[0].code}.${result[0].ext}`;
navigator.clipboard.writeText(ptpimgUrl);
if (DEBUG) console.log('[Deez Notes] Rehosted and copied:', ptpimgUrl);
resolve(ptpimgUrl);
} else {
reject(new Error('Invalid response from ptpimg'));
}
} catch (e) {
reject(new Error('Failed to parse ptpimg response'));
}
} else {
reject(new Error(`Upload failed with status ${response.status}`));
}
},
onerror: (error) => reject(new Error('Upload request failed')),
ontimeout: () => reject(new Error('Upload timed out'))
});
});
}
function createCopyableCell(content, isGenresCell = false, isFirstColumn = false, isLastColumn = false) {
const td = document.createElement('td');
// Check if this is a reload cell or N/A cell (non-copyable)
const isReloadCell = content === 'Reload page';
const isNACell = !content || content === 'N/A';
const isNonCopyableCell = isReloadCell || isNACell;
// Set appropriate class - add w7dn_na-cell for N/A cells to prevent hover
td.className = isNACell ? 'w7dn_copyable-cell w7dn_na-cell' : 'w7dn_copyable-cell';
// Set text content - for reload cells, show "Reload ↻"
if (isReloadCell) {
td.textContent = 'Reload ↻';
} else {
td.textContent = content || 'N/A';
}
td.style.cssText = `
padding: 12px;
vertical-align: top;
position: relative;
cursor: ${isReloadCell ? 'pointer' : (isNACell ? 'default' : 'pointer')};
background: var(--dn-cell-bg);
border-right: 1px solid var(--dn-divider-line);
border-bottom: 1px solid var(--dn-divider-line);
transition: background-color 0.2s ease;
`;
// Adjust for bottom-left rounded corner
if (isFirstColumn) {
td.style.borderBottomLeftRadius = '8px';
}
// Adjust for bottom-right rounded corner
if (isLastColumn) {
td.style.borderBottomRightRadius = '8px';
}
// Make "Reload" cells italic and gray
if (isReloadCell) {
td.style.fontStyle = 'italic';
td.style.color = '#999';
}
// Make N/A cells italic and gray (but clickable for reload if it's a reload cell)
if (isNACell && !isReloadCell) {
td.style.fontStyle = 'italic';
td.style.color = '#999';
}
// Add copy icon ONLY for copyable cells (not reload, not N/A)
const actionIcon = document.createElement('span');
if (!isNonCopyableCell) {
actionIcon.innerHTML = '⧉';
actionIcon.title = '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: var(--dn-icon-text);
background: var(--dn-icon-bg);
border-radius: 3px;
padding: 2px 4px;
`;
}
// Add copied message (positioned in same place as icon) - only for copyable cells
const copiedMessage = document.createElement('span');
if (!isNonCopyableCell) {
copiedMessage.className = 'w7dn_copied-message';
copiedMessage.textContent = 'Copied!';
copiedMessage.style.cssText = `
position: absolute;
top: 8px;
right: 8px;
color: var(--dn-icon-text);
font-size: 11px;
font-weight: 500;
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.addEventListener('mouseleave', () => {
actionIcon.style.opacity = '0';
});
}
// Click functionality - reload or copy (only for non-N/A cells)
td.addEventListener('click', () => {
if (isReloadCell) {
location.reload();
} else if (!isNACell) {
const currentText = getCellText(td) || content || 'N/A';
// Don't copy if in non-copyable state
if (isNonCopyableState(currentText)) {
return;
}
let copyText = currentText;
if (isGenresCell) {
// 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;
}
// === HELPER FUNCTIONS ===
// Update only the text node of a cell, preserving icons and other elements
function updateCellText(cell, newText) {
const textNode = Array.from(cell.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
if (textNode) {
textNode.textContent = newText;
} else {
// If no text node exists, create one and insert it before other elements
const newTextNode = document.createTextNode(newText);
cell.insertBefore(newTextNode, cell.firstChild);
}
}
// Get current text from a cell (text node only)
function getCellText(cell) {
return Array.from(cell.childNodes)
.find(node => node.nodeType === Node.TEXT_NODE)
?.textContent || '';
}
// Check if text is in a non-copyable state
function isNonCopyableState(text) {
return !text || NON_COPYABLE_STATES.includes(text);
}
// Extract featured artists from app state track data
function extractTrackArtists(track, albumMainArtists = []) {
const artists = track.ARTISTS || [];
const contributors = track.SNG_CONTRIBUTORS || {};
// Prefer SNG_CONTRIBUTORS if available (more reliable)
let trackMainArtists = contributors.main_artist || [];
let featuredArtists = contributors.featuring || [];
let remixers = contributors.remixer || [];
// Fallback to ARTISTS array if SNG_CONTRIBUTORS not available
if (trackMainArtists.length === 0) {
trackMainArtists = artists
.filter(a => a.ROLE_ID === 0 || a.ROLE_ID === '0')
.map(a => a.ART_NAME)
.filter(name => name);
}
if (featuredArtists.length === 0) {
featuredArtists = artists
.filter(a => a.ROLE_ID === 5 || a.ROLE_ID === '5')
.map(a => a.ART_NAME)
.filter(name => name);
// Filter out remixers from featured artists if we have remixer info
if (remixers.length === 0) {
remixers = inferRemixersFromTitle(track.SNG_TITLE, track.VERSION, artists);
}
// Remove remixers from featured list
if (remixers.length > 0) {
featuredArtists = featuredArtists.filter(name =>
!remixers.some(r => r.toLowerCase() === name.toLowerCase())
);
}
}
if (DEBUG) {
console.log('extractTrackArtists:', track.SNG_TITLE, {
main: trackMainArtists,
featured: featuredArtists,
remixers: remixers
});
}
return { trackMainArtists, featuredArtists, remixers };
}
// Infer remixers from track title/version and artist list
function inferRemixersFromTitle(title, version, artists) {
const remixers = [];
const fullTitle = version ? `${title} ${version}` : title;
// Common remix patterns
const remixPatterns = [
/\(([^)]+)\s+(?:Extended\s+)?Remix\)/i,
/\[([^\]]+)\s+(?:Extended\s+)?Remix\]/i,
/(?:Remixed by|Remix by)\s+([^(\[)]+)/i,
/-\s*([^-]+)\s+(?:Extended\s+)?Remix/i
];
for (const pattern of remixPatterns) {
const match = fullTitle.match(pattern);
if (match) {
const remixerName = match[1].trim()
.replace(/\s+Extended$/i, '')
.replace(/\s+Remix$/i, '');
// Try to match against artist list
const matchedArtist = artists.find(a =>
a.ART_NAME && remixerName.toLowerCase().includes(a.ART_NAME.toLowerCase())
);
if (matchedArtist) {
remixers.push(matchedArtist.ART_NAME);
}
}
}
return remixers;
}
// Get track contributors from individual track API call
async function getTrackContributors(trackId, albumMainArtists = []) {
if (cachedTrackDetails[trackId]) {
return cachedTrackDetails[trackId];
}
try {
const response = await fetch(`https://api.deezer.com/track/${trackId}`);
const data = await response.json();
// Get all main artists for the track
let trackMainArtists = data.contributors
?.filter(c => c.role === 'Main')
.map(c => c.name) || [];
// Get featured artists (look for Featured or Guest role)
let featuredArtists = data.contributors
?.filter(c => c.role === 'Featured' || c.role === 'Guest')
.map(c => c.name) || [];
// Get remixers (first check if explicitly labeled)
let remixers = data.contributors
?.filter(c => c.role === 'Remixer')
.map(c => c.name) || [];
// If no explicit remixers, try to infer from title/version and contributor names
if (remixers.length === 0 && data.title_version) {
const allContributors = data.contributors || [];
const inferredRemixers = inferRemixersFromTitle(
data.title_short || data.title,
data.title_version,
allContributors.map(c => ({ ART_NAME: c.name }))
);
if (inferredRemixers.length > 0) {
remixers = inferredRemixers;
// Remove inferred remixers from featured artists list
featuredArtists = featuredArtists.filter(name =>
!remixers.some(r => r.toLowerCase() === name.toLowerCase())
);
}
}
if (DEBUG) console.log('getTrackContributors:', trackId, {
main: trackMainArtists,
featured: featuredArtists,
remixers: remixers
});
const result = { trackMainArtists, featuredArtists, remixers };
cachedTrackDetails[trackId] = result;
return result;
} catch (error) {
console.error(`Failed to fetch track ${trackId}:`, error);
return { trackMainArtists: [], featuredArtists: [], remixers: [] };
}
}
// Format BBCode track with featured artists and remixers
function formatBBCodeTrack(index, title, trackMainArtists, featuredArtists, duration, remixers = [], version = '', albumMainArtists = [], showTrackArtists = true) {
let trackLine = `[b]${index + 1}.[/b]`;
// Show track-level main artists only if they differ from album artists OR if showTrackArtists is explicitly true
if (showTrackArtists && trackMainArtists && trackMainArtists.length > 0) {
trackLine += ` ${formatArtistList(trackMainArtists)} -`;
}
// Build the title with version/remix info
let fullTitle = title;
if (version) {
// Check if version contains remix info and remixer is identified
if (remixers.length > 0 && /remix/i.test(version)) {
// Wrap remixer name in [artist] tags within the version string
let remixVersion = version;
remixers.forEach(remixer => {
const remixerPattern = new RegExp(`(${escapeRegex(remixer)})`, 'gi');
remixVersion = remixVersion.replace(remixerPattern, `[artist]${remixer}[/artist]`);
});
fullTitle = `${title} ${remixVersion}`;
} else {
fullTitle = `${title} ${version}`;
}
}
trackLine += ` ${fullTitle}`;
// Add featured artists
if (featuredArtists.length > 0) {
trackLine += ` (feat. ${formatArtistList(featuredArtists)})`;
}
trackLine += ` [i](${duration})[/i]`;
return trackLine;
}
// Helper to format artist list with proper separators
function formatArtistList(artists) {
const artistsList = artists.map(artist => {
const artistName = typeof artist === 'string' ? artist : (artist.ART_NAME || artist.name);
return `[artist]${artistName}[/artist]`;
});
if (artistsList.length === 1) {
return artistsList[0];
} else if (artistsList.length === 2) {
return artistsList.join(' & ');
} else {
// Multiple artists: join all but last with ", " then " & " before last
const allButLast = artistsList.slice(0, -1).join(', ');
return `${allButLast} & ${artistsList[artistsList.length - 1]}`;
}
}
// Helper to escape special regex characters
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Format duration with hours if over 60 minutes
function formatDuration(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}
// Extract album main artists from API response
function extractAlbumMainArtistsFromAPI(apiData) {
const albumMainArtists = [];
if (apiData.contributors) {
// Find main artists (usually the first contributors)
const mainArtistContributors = apiData.contributors.filter(c =>
c.role === 'Main' || c.role === 'Artist' || !c.role
);
albumMainArtists.push(...mainArtistContributors.map(c => c.name));
}
// Fallback: use the primary artist name
if (albumMainArtists.length === 0 && apiData.artist?.name) {
albumMainArtists.push(apiData.artist.name);
}
return albumMainArtists;
}
// Copy BBCode tracklist with featured artists
async function copyBBCodeTracklist() {
// Check if we have intercepted data
if (!interceptedAlbumData || !interceptedAlbumData.DATA || !interceptedAlbumData.SONGS) {
console.error('[Deez Notes] No intercepted album data available for tracklist');
throw new Error('No album data available');
}
const { DATA, SONGS } = interceptedAlbumData;
const songsData = SONGS.data;
// Extract basic album info
const artistName = DATA.ART_NAME || 'Unknown Artist';
const albumTitle = DATA.ALB_TITLE || 'Unknown Album';
const releaseDate = formatDate(DATA.ORIGINAL_RELEASE_DATE);
const totalSeconds = DATA.DURATION || 0;
// Get album main artists
const albumMainArtists = DATA.ARTISTS || [];
if (DEBUG) console.log('[Deez Notes] Building BBCode tracklist from intercepted data');
// First pass: extract all track data to determine if we need to show track artists
const trackDataList = songsData.map(track => {
const { trackMainArtists, featuredArtists, remixers } = extractTrackArtists(track, albumMainArtists);
return {
track,
trackMainArtists,
featuredArtists,
remixers,
discNumber: track.DISK_NUMBER || '1'
};
});
// Check if this is a multi-disc album
const uniqueDiscs = [...new Set(trackDataList.map(t => t.discNumber))];
const isMultiDisc = uniqueDiscs.length > 1;
if (DEBUG) console.log('Disc detection:', { uniqueDiscs, isMultiDisc });
// Check if all tracks have the same main artists as the album
const albumMainArtistNames = albumMainArtists.map(a => a.ART_NAME).sort().join('|');
const allTracksSameArtist = trackDataList.every(({ trackMainArtists }) => {
const trackArtistNames = trackMainArtists.sort().join('|');
return trackArtistNames === albumMainArtistNames;
});
const showTrackArtists = !allTracksSameArtist;
if (DEBUG) console.log('Show track artists?', showTrackArtists, '(all same:', allTracksSameArtist + ')');
// Second pass: format tracks with the determined showTrackArtists flag
const tracksBodyLines = [];
let currentDisc = null;
let trackIndexInDisc = 0; // Track number within current disc
for (let i = 0; i < trackDataList.length; i++) {
const { track, trackMainArtists, featuredArtists, remixers, discNumber } = trackDataList[i];
// Add disc header if multi-disc and disc changed
if (isMultiDisc && discNumber !== currentDisc) {
// Add blank line before new disc (except first disc)
if (currentDisc !== null) {
tracksBodyLines.push('');
}
// Calculate disc duration
const discTracks = trackDataList.filter(t => t.discNumber === discNumber);
const discDuration = discTracks.reduce((sum, t) => sum + parseInt(t.track.DURATION), 0);
// Add disc header
tracksBodyLines.push(`[b]Disc ${discNumber}[/b] [i](${formatDuration(discDuration)})[/i]`);
currentDisc = discNumber;
trackIndexInDisc = 0; // Reset track index for new disc
}
const duration = parseInt(track.DURATION);
const title = track.SNG_TITLE;
const version = track.VERSION || '';
const formattedDuration = formatDuration(duration);
// Use trackIndexInDisc for multi-disc albums to restart numbering per disc
const trackIndex = isMultiDisc ? trackIndexInDisc : i;
tracksBodyLines.push(formatBBCodeTrack(trackIndex, title, trackMainArtists, featuredArtists, formattedDuration, remixers, version, albumMainArtists, showTrackArtists));
trackIndexInDisc++;
}
const tracksBody = tracksBodyLines;
// Format artist names - use individual [artist] tags if multiple main artists
const displayArtistName = albumMainArtists.length > 1
? albumMainArtists.map(a => `[artist]${a.ART_NAME}[/artist]`).join(', ')
: `[artist]${artistName}[/artist]`;
const finalTracklist = [
`[b]${displayArtistName} - ${albumTitle}[/b]`,
releaseDate,
'',
tracksBody.join('\n'),
'',
`[b]Total length:[/b] ${formatDuration(totalSeconds)}`
].join('\n');
await navigator.clipboard.writeText(finalTracklist);
if (DEBUG) console.log('[Deez Notes] BBCode tracklist copied successfully');
}
// Load genres from public API (genres not available in intercepted data)
async function loadGenresAsync(albumId, values, dataRow) {
try {
if (DEBUG) console.log('[Deez Notes] Loading genres from public API...');
const response = await fetch(`https://api.deezer.com/album/${albumId}`);
const apiData = await response.json();
// Extract genres
const genres = apiData.genres?.data?.length
? apiData.genres.data.map(genre => genre.name).join(', ')
: 'None provided';
// Update the genres cell (index 1)
const genresCell = dataRow.children[1];
if (genresCell) {
updateCellText(genresCell, genres);
values[1] = genres;
}
if (DEBUG) console.log('[Deez Notes] Genres loaded:', genres);
} catch (error) {
console.error('[Deez Notes] Failed to load genres:', error);
// Update the genres cell to show error
const genresCell = dataRow.children[1];
if (genresCell) {
updateCellText(genresCell, 'API Call Failed');
values[1] = 'API Call Failed';
}
}
}
// Inject hover overlay on album/artist cover
function injectCoverRehostOverlay() {
// Find album cover or artist cover
const coverSelector = isAlbumPage()
? 'div[role="group"][data-testid="album-cover"]'
: 'div[role="group"][data-testid="artist-cover"]';
const coverDiv = document.querySelector(coverSelector);
if (!coverDiv) {
if (DEBUG) console.log('[Deez Notes] Cover element not found');
return;
}
// Check if overlay already exists
if (coverDiv.classList.contains('w7dn_cover-container')) {
if (DEBUG) console.log('[Deez Notes] Cover overlay already exists');
return;
}
// Get the image URL
const img = coverDiv.querySelector('img');
if (!img || !img.src) {
if (DEBUG) console.log('[Deez Notes] Cover image not found');
return;
}
// Add container class
coverDiv.classList.add('w7dn_cover-container');
// Create overlay
const overlay = document.createElement('div');
overlay.className = 'w7dn_cover-rehost-overlay';
overlay.textContent = 'Rehost';
// Add click handler
overlay.addEventListener('click', async (e) => {
e.stopPropagation();
const originalText = overlay.textContent;
overlay.textContent = 'Rehosting...';
overlay.style.pointerEvents = 'none'; // Disable clicks during rehost
overlay.classList.add('w7dn_stay-visible'); // Keep visible during processing
try {
await rehostCoverImageSimple();
overlay.textContent = 'Copied!';
// Keep visible for 3 seconds after success, then hide
setTimeout(() => {
overlay.classList.remove('w7dn_stay-visible');
// Wait for slide-down animation (0.2s) before changing text
setTimeout(() => {
overlay.textContent = originalText;
overlay.style.pointerEvents = 'auto';
}, 200);
}, 3000);
} catch (error) {
console.error('[Deez Notes] Error rehosting cover:', error);
overlay.textContent = 'Failed!';
// Keep visible for 2 seconds after failure, then hide
setTimeout(() => {
overlay.classList.remove('w7dn_stay-visible');
// Wait for slide-down animation (0.2s) before changing text
setTimeout(() => {
overlay.textContent = originalText;
overlay.style.pointerEvents = 'auto';
}, 200);
}, 2000);
}
});
coverDiv.appendChild(overlay);
if (DEBUG) console.log('[Deez Notes] Cover rehost overlay injected');
}
// Inject BBCode button next to TRACK heading
async function injectBBCodeTrackButton() {
// Find the TRACK span element
const trackSpan = Array.from(document.querySelectorAll('span.jhaxe'))
.find(span => span.textContent.trim() === 'TRACK');
if (!trackSpan) {
if (DEBUG) console.log('[Deez Notes] TRACK span not found');
return;
}
// Check if button already exists
if (trackSpan.parentElement.querySelector('.w7dn_bbcode-track-button')) {
if (DEBUG) console.log('[Deez Notes] BBCode track button already exists');
return;
}
// Create BBCode button
const bbcodeButton = document.createElement('button');
bbcodeButton.textContent = 'BBCode';
bbcodeButton.className = 'w7dn_bbcode-track-button';
bbcodeButton.title = 'Copy the BBCode tracklist to your clipboard';
// Add click handler
bbcodeButton.addEventListener('click', async () => {
bbcodeButton.disabled = true;
const originalText = bbcodeButton.textContent;
try {
bbcodeButton.textContent = '...';
await copyBBCodeTracklist();
bbcodeButton.textContent = 'Copied!';
setTimeout(() => {
bbcodeButton.textContent = originalText;
bbcodeButton.disabled = false;
}, 2000);
} catch (error) {
console.error('[Deez Notes] Error copying BBCode tracklist:', error);
bbcodeButton.textContent = 'Error!';
setTimeout(() => {
bbcodeButton.textContent = originalText;
bbcodeButton.disabled = false;
}, 2000);
}
});
// Insert button after the TRACK span
trackSpan.parentElement.insertBefore(bbcodeButton, trackSpan.nextSibling);
if (DEBUG) console.log('[Deez Notes] BBCode track button injected');
}
// === BUTTON HELPER FUNCTIONS ===
// Create a styled action button with consistent styling
function createActionButton(text, title, options = {}) {
const button = document.createElement('button');
button.textContent = text;
button.className = 'w7dn_api-link';
button.title = title;
const baseStyles = `
color: var(--dn-api-text);
background: var(--dn-api-bg);
border: 1px solid var(--dn-api-border);
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
font-size: inherit;
font-family: inherit;
${options.minWidth ? `min-width: ${options.minWidth};` : ''}
`;
button.style.cssText = baseStyles;
if (options.disabled) {
button.disabled = true;
}
return button;
}
// Add hover effects to button
function addButtonHoverEffects(button, checkDisabled = true) {
button.addEventListener('mouseenter', () => {
if (!checkDisabled || !button.disabled) {
button.style.background = 'var(--dn-api-bg-hover)';
button.style.color = 'var(--dn-api-text-hover)';
button.style.borderColor = 'var(--dn-api-border-hover)';
}
});
button.addEventListener('mouseleave', () => {
if (!checkDisabled || !button.disabled) {
button.style.background = 'var(--dn-api-bg)';
button.style.color = 'var(--dn-api-text)';
button.style.borderColor = 'var(--dn-api-border)';
}
});
}
// Restore button to normal state
function restoreButtonState(button) {
button.style.background = 'var(--dn-api-bg)';
button.style.color = 'var(--dn-api-text)';
button.style.borderColor = 'var(--dn-api-border)';
}
// Handle async button operation with state management
async function handleButtonOperation(button, operation, messages = {}) {
const { processing = 'Processing...', success = 'Copied!', error = 'Error!' } = messages;
const originalText = button.textContent;
button.disabled = true;
if (processing) button.textContent = processing;
try {
await operation();
button.textContent = success;
setTimeout(() => {
button.textContent = originalText;
button.disabled = false;
restoreButtonState(button);
}, 2000);
} catch (err) {
console.error('[Deez Notes] Button operation error:', err);
button.textContent = error;
setTimeout(() => {
button.textContent = originalText;
button.disabled = false;
restoreButtonState(button);
}, 2000);
}
}
async function addActionsArea(apiUrl, albumId) {
// Find ul element
const ulElement = document.querySelector(SELECTORS.UL_ELEMENT);
if (!ulElement) {
if (DEBUG) console.log('[Deez Notes] ul.css-1s16397 not found');
return;
}
// Remove existing actions area if present
const existingActionsArea = document.querySelector('.w7dn_actions-area');
if (existingActionsArea) {
existingActionsArea.remove();
}
// Check if public API is available (unreleased albums may not have API access)
let apiAvailable = true;
try {
if (DEBUG) console.log('[Deez Notes] Checking API availability...');
const response = await fetch(apiUrl);
const data = await response.json();
if (!data.title) {
apiAvailable = false;
if (DEBUG) console.log('[Deez Notes] Public API not available (likely unreleased album)');
}
} catch (error) {
apiAvailable = false;
if (DEBUG) console.log('[Deez Notes] Public API not available:', error);
}
// Create actions container
const actionsContainer = document.createElement('div');
actionsContainer.className = 'w7dn_actions-area';
actionsContainer.style.cssText = `
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
align-items: center;
`;
// Create API button
const apiButton = createActionButton(
'API',
apiAvailable ? 'Open the API in a new window' : 'Public API not available (unreleased album)',
{ disabled: !apiAvailable }
);
if (apiAvailable) {
apiButton.addEventListener('click', () => window.open(apiUrl, '_blank'));
addButtonHoverEffects(apiButton, false);
}
// Create BBCode button
const bbcodeButton = createActionButton(
'BBCode',
'Copy the BBCode tracklist to your clipboard',
{ minWidth: '80px' }
);
addButtonHoverEffects(bbcodeButton);
bbcodeButton.addEventListener('click', () =>
handleButtonOperation(bbcodeButton, copyBBCodeTracklist, { processing: null })
);
// Create Cover button
const coverButton = createActionButton(
'Cover',
'Rehost the cover on ptpimg and copy the URL'
);
addButtonHoverEffects(coverButton);
coverButton.addEventListener('click', () =>
handleButtonOperation(coverButton, rehostCoverImageSimple, { error: 'Failed!' })
);
// Add buttons to container (order: API, BBCode, Cover)
actionsContainer.appendChild(apiButton);
actionsContainer.appendChild(bbcodeButton);
actionsContainer.appendChild(coverButton);
// Insert after the ul element
ulElement.parentNode.insertBefore(actionsContainer, ulElement.nextSibling);
if (DEBUG) console.log('[Deez Notes] Actions area added successfully');
}
async function addMetadata() {
try {
// Guard against concurrent initialization
if (isInitializing) {
if (DEBUG) console.log('[Deez Notes] Already initializing, skipping...');
return false;
}
// Check if metadata table already exists
if (document.querySelector('.w7dn_metadata-table')) {
if (DEBUG) console.log('[Deez Notes] Metadata table already exists, skipping...');
return true;
}
const albumId = extractAlbumId();
if (!albumId) {
if (DEBUG) console.log('[Deez Notes] Could not extract album ID from URL');
return false;
}
// Check if we're already showing this album
if (currentAlbumId === albumId && document.querySelector('.w7dn_metadata-table')) {
if (DEBUG) console.log('[Deez Notes] Already showing metadata for this album');
return true;
}
// Check if we have intercepted data
if (!interceptedAlbumData || !interceptedAlbumData.DATA) {
if (DEBUG) console.log('[Deez Notes] No intercepted data available yet, will retry...');
return false;
}
// Check if intercepted data matches current album
if (interceptedAlbumData.DATA.ALB_ID !== albumId) {
if (DEBUG) console.log('[Deez Notes] Intercepted data is for different album, will retry...');
return false;
}
// Set guard
isInitializing = true;
currentAlbumId = albumId;
// Find the play button container for positioning
const playButtonContainer = document.querySelector(SELECTORS.PLAY_BUTTON);
if (!playButtonContainer) {
if (DEBUG) console.log('[Deez Notes] Play button container not found');
return false;
}
const buttonStrip = playButtonContainer.parentElement.parentElement;
// Extract data from intercepted album data
const albumData = interceptedAlbumData.DATA;
const label = albumData.LABEL_NAME || 'N/A';
const upc = albumData.UPC || 'N/A';
const digitalReleaseDate = formatDate(albumData.DIGITAL_RELEASE_DATE);
const physicalReleaseDate = formatDate(albumData.PHYSICAL_RELEASE_DATE);
const originalReleaseDate = formatDate(albumData.ORIGINAL_RELEASE_DATE);
// Genres need to be loaded from public API (not in intercepted data)
let genres = 'Loading Genres...';
if (DEBUG) {
console.log('[Deez Notes] Album metadata:', {
label,
upc,
genres,
digitalReleaseDate,
physicalReleaseDate,
originalReleaseDate
});
}
// Create the table directly
const table = document.createElement('table');
table.className = 'w7dn_metadata-table'; // Keep class for easy identification/removal
table.style.cssText = `
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 13px;
color: var(--dn-cell-text);
margin-top: 19px;
margin-bottom: 19px;
border-radius: 8px;
overflow: hidden; /* Ensures rounded corners are visible */
border: 1px solid var(--dn-table-border);
`;
// Create table header
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const headers = ['Label', 'Genres', 'UPC', 'Digital Release', 'Physical Release', 'Original Release'];
const columnWidths = ['22%', '22%', '13%', '13%', '13%', '13%']; // Label and Genres: 24%, UPC and dates: 12%
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: var(--dn-header-text);
background: var(--dn-header-bg);
border-right: 1px solid var(--dn-divider-line);
border-bottom: 1px solid var(--dn-divider-line);
width: ${columnWidths[index]};
`;
// Apply top-left and top-right border-radius to the first and last header cells
if (index === 0) {
th.style.borderTopLeftRadius = '8px';
th.style.borderLeft = 'none'; // No left border on first cell, it's handled by table's outer border
}
if (index === headers.length - 1) {
th.style.borderTopRightRadius = '8px';
th.style.borderRight = 'none'; // No right border on last cell, it's handled by table's outer border
}
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create table body
const tbody = document.createElement('tbody');
const dataRow = document.createElement('tr');
dataRow.style.backgroundColor = 'var(--dn-cell-bg)';
const values = [label, genres, upc, digitalReleaseDate, physicalReleaseDate, originalReleaseDate];
const totalColumns = headers.length; // Count headers for column indexing
// Add data cells with copy functionality
values.forEach((value, index) => {
const isGenresCell = index === 1; // genres is the 2nd item (index 1)
const isFirstColumn = index === 0;
const isLastColumn = index === values.length - 1; // Last column
const td = createCopyableCell(value, isGenresCell, isFirstColumn, isLastColumn);
if (isFirstColumn) {
td.style.borderLeft = 'none'; // No left border on first data cell
}
if (isLastColumn) {
td.style.borderRight = 'none'; // No right border on last data cell
}
dataRow.appendChild(td);
});
tbody.appendChild(dataRow);
table.appendChild(tbody);
// Insert the table after the button strip
buttonStrip.parentNode.insertBefore(table, buttonStrip.nextSibling);
// Asynchronously load genres from public API
loadGenresAsync(albumId, values, dataRow);
// Add actions area with buttons
const apiUrl = `https://api.deezer.com/album/${albumId}`;
addActionsArea(apiUrl, albumId);
// Inject cover rehost overlay on album pages
if (isAlbumPage()) {
setTimeout(() => injectCoverRehostOverlay(), 500);
}
if (DEBUG) console.log('[Deez Notes] Metadata added successfully');
isInitializing = false; // Clear guard on success
return true;
} catch (error) {
console.error('[Deez Notes] Error adding metadata:', error);
isInitializing = false; // Clear guard on error
return false;
}
}
// Add API button to artist page
async function addArtistApiButton() {
const artistId = extractArtistId();
if (!artistId) {
if (DEBUG) console.log('[Deez Notes] Could not extract artist ID');
return;
}
// Find ul element (same selector as album pages)
const ulElement = document.querySelector(SELECTORS.UL_ELEMENT);
if (!ulElement) {
if (DEBUG) console.log('[Deez Notes] ul.css-1s16397 not found');
return;
}
// Remove existing actions area if present
const existingActionsArea = document.querySelector('.w7dn_actions-area');
if (existingActionsArea) {
existingActionsArea.remove();
}
// Check if public API is available
const apiUrl = `https://api.deezer.com/artist/${artistId}`;
let apiAvailable = true;
try {
if (DEBUG) console.log('[Deez Notes] Checking artist API availability...');
const response = await fetch(apiUrl);
const data = await response.json();
if (!data.name) {
apiAvailable = false;
if (DEBUG) console.log('[Deez Notes] Public artist API not available');
}
} catch (error) {
apiAvailable = false;
if (DEBUG) console.log('[Deez Notes] Public artist API not available:', error);
}
// Create actions container
const actionsContainer = document.createElement('div');
actionsContainer.className = 'w7dn_actions-area';
actionsContainer.style.cssText = `
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
align-items: center;
`;
// Create API button
const apiButton = createActionButton(
'API',
apiAvailable ? 'Open the artist API in a new window' : 'Public API not available',
{ disabled: !apiAvailable }
);
if (apiAvailable) {
apiButton.addEventListener('click', () => window.open(apiUrl, '_blank'));
addButtonHoverEffects(apiButton, false);
}
// Create Cover button
const coverButton = createActionButton(
'Cover',
'Rehost the artist picture on ptpimg and copy the URL'
);
addButtonHoverEffects(coverButton);
coverButton.addEventListener('click', () =>
handleButtonOperation(coverButton, rehostCoverImageSimple, { error: 'Failed!' })
);
// Add buttons to container (order: API, Cover)
actionsContainer.appendChild(apiButton);
actionsContainer.appendChild(coverButton);
// Insert after the ul element
ulElement.parentNode.insertBefore(actionsContainer, ulElement.nextSibling);
if (DEBUG) console.log('[Deez Notes] Artist API button added successfully');
}
function initArtistPage() {
if (DEBUG) console.log('[Deez Notes] Starting artist page init...');
// Wait for artist cover to be available
waitForElement('div[role="group"][data-testid="artist-cover"]')
.then(() => {
if (DEBUG) console.log('[Deez Notes] Artist cover found, injecting overlay and API button...');
setTimeout(() => {
injectCoverRehostOverlay();
addArtistApiButton();
}, 500);
})
.catch(error => {
if (DEBUG) console.error('[Deez Notes] Failed to find artist cover:', error);
});
}
function init() {
if (DEBUG) console.log('[Deez Notes] Starting init...');
// Check if we're on an artist page
if (location.href.includes('/artist/')) {
initArtistPage();
return;
}
// Check if we're on an album page
if (!location.href.includes('/album/')) {
if (DEBUG) console.log('[Deez Notes] Not on an album or artist page, skipping...');
return;
}
// Wait for the page to load and the play button to be available
waitForElement(SELECTORS.PLAY_BUTTON)
.then(() => {
if (DEBUG) console.log('[Deez Notes] Play button found, attempting to add metadata...');
// Try to add metadata with multiple retries
let attempts = 0;
const maxAttempts = 10;
let retryTimer = null;
async function tryAddMetadata() {
// Check if we already have metadata (race condition guard)
if (document.querySelector('.w7dn_metadata-table')) {
if (DEBUG) console.log('[Deez Notes] Metadata already present, stopping retries');
if (retryTimer) clearTimeout(retryTimer);
return;
}
attempts++;
try {
const success = await addMetadata();
if (success) {
if (DEBUG) console.log(`[Deez Notes] Metadata added successfully on attempt ${attempts}`);
if (retryTimer) clearTimeout(retryTimer);
return;
}
} catch (error) {
if (DEBUG) console.log('[Deez Notes] Error in addMetadata:', error);
}
if (attempts < maxAttempts) {
if (DEBUG) console.log(`[Deez Notes] Attempt ${attempts} failed, retrying in 500ms...`);
retryTimer = setTimeout(tryAddMetadata, 500); // Retry every 500ms
} else {
if (DEBUG) console.log('[Deez Notes] Max attempts reached, giving up');
}
}
tryAddMetadata();
})
.catch(error => {
console.error('[Deez Notes] Failed to find play button:', error);
});
}
// --- Main Execution ---
injectThemeStyles();
// Navigation detection for SPA
let lastUrl = location.href;
if (DEBUG) console.log('[Deez Notes] Initial URL:', lastUrl);
// Helper function to check if URL is an album page (collision with isAlbumPage() function above)
function isAlbumPageUrl(url) {
return url.includes('/album/');
}
// Helper function to check if URL is an artist page
function isArtistPageUrl(url) {
return url.includes('/artist/');
}
// Listen for album data interception events
window.addEventListener('deezNotesAlbumLoaded', (event) => {
const albumData = event.detail;
const newAlbumId = albumData?.DATA?.ALB_ID;
if (DEBUG) console.log('[Deez Notes] Album data loaded event received:', newAlbumId);
// Only process if this is for the current URL
const urlAlbumId = extractAlbumId();
if (!urlAlbumId || urlAlbumId !== newAlbumId) {
if (DEBUG) console.log('[Deez Notes] Event is for different album, ignoring');
return;
}
// Only process if we're on an album page
if (!isAlbumPageUrl(location.href)) {
if (DEBUG) console.log('[Deez Notes] Not on album page, ignoring event');
return;
}
// Remove existing UI elements
const existingTable = document.querySelector('.w7dn_metadata-table');
if (existingTable) {
if (DEBUG) console.log('[Deez Notes] Removing existing table');
existingTable.remove();
}
const existingActions = document.querySelector('.w7dn_actions-area');
if (existingActions) {
if (DEBUG) console.log('[Deez Notes] Removing existing actions');
existingActions.remove();
}
// Clear state for new album
cachedTrackDetails = {};
isInitializing = false; // Reset initialization guard
currentAlbumId = null; // Reset current album tracker
// Re-initialize with fresh data (longer delay for SPA transitions)
setTimeout(() => {
if (DEBUG) console.log('[Deez Notes] Re-initializing after album load event');
init();
}, 200);
});
// Monitor URL changes for navigation (wait for body to be available)
function setupNavigationObserver() {
if (!document.body) {
setTimeout(setupNavigationObserver, 100);
return;
}
if (DEBUG) console.log('[Deez Notes] Setting up MutationObserver...');
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
if (DEBUG) console.log(`[Deez Notes] Navigation detected: ${lastUrl} → ${url}`);
const wasAlbumPage = isAlbumPageUrl(lastUrl);
const isNowAlbumPage = isAlbumPageUrl(url);
const wasArtistPage = isArtistPageUrl(lastUrl);
const isNowArtistPage = isArtistPageUrl(url);
lastUrl = url;
// Handle album page navigation
if (isNowAlbumPage && !wasAlbumPage) {
// Navigated TO an album page from non-album page
if (DEBUG) console.log('[Deez Notes] Navigated to album page, waiting for data...');
// Wait for intercepted data, will trigger via event
} else if (isNowAlbumPage && wasAlbumPage) {
// Navigated between album pages
if (DEBUG) console.log('[Deez Notes] Navigated between album pages');
// Wait for intercepted data, will trigger via event
}
// Handle leaving album page
if (!isNowAlbumPage && wasAlbumPage) {
// Navigated AWAY from album page
if (DEBUG) console.log('[Deez Notes] Navigated away from album page');
interceptedAlbumData = null;
cachedTrackDetails = {};
}
// Handle artist page navigation (separate from album logic)
if (isNowArtistPage) {
// Navigated TO an artist page (from any page or between artist pages)
if (DEBUG) console.log('[Deez Notes] Navigated to artist page');
setTimeout(() => init(), 500);
}
}
}).observe(document.body, {
subtree: true,
childList: true
});
}
setupNavigationObserver();
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
// Check for initial app state
if (!checkInitialAppState()) {
// If not found immediately, try again after a delay
setTimeout(checkInitialAppState, 500);
setTimeout(checkInitialAppState, 1000);
setTimeout(checkInitialAppState, 2000);
}
init();
});
} else {
// Check for initial app state
if (!checkInitialAppState()) {
// If not found immediately, try again after a delay
setTimeout(checkInitialAppState, 500);
setTimeout(checkInitialAppState, 1000);
setTimeout(checkInitialAppState, 2000);
}
init();
}
})();