|
|
// BEATPORT REBUILD SLIDER FUNCTIONALITY
|
|
|
// =================================
|
|
|
|
|
|
let beatportRebuildSliderState = {
|
|
|
currentSlide: 0,
|
|
|
totalSlides: 4,
|
|
|
autoPlayInterval: null,
|
|
|
autoPlayDelay: 5000
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Initialize the beatport rebuild slider functionality
|
|
|
*/
|
|
|
function initializeBeatportRebuildSlider() {
|
|
|
console.log('🔄 Initializing beatport rebuild slider...');
|
|
|
|
|
|
const slider = document.getElementById('beatport-rebuild-slider');
|
|
|
if (!slider) {
|
|
|
console.warn('Beatport rebuild slider not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Check if already initialized to prevent duplicate event listeners
|
|
|
if (slider.dataset.initialized === 'true') {
|
|
|
console.log('Beatport rebuild slider already initialized, skipping...');
|
|
|
startBeatportRebuildSliderAutoPlay(); // Just restart autoplay
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Mark as initialized
|
|
|
slider.dataset.initialized = 'true';
|
|
|
|
|
|
// Load real Beatport data first
|
|
|
loadBeatportHeroTracks();
|
|
|
|
|
|
console.log('✅ Beatport rebuild slider initialized successfully');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Load real Beatport hero tracks and populate the slider
|
|
|
*/
|
|
|
async function loadBeatportHeroTracks() {
|
|
|
console.log('🎯 Loading real Beatport hero tracks...');
|
|
|
|
|
|
try {
|
|
|
const signal = getBeatportContentSignal();
|
|
|
const response = await fetch('/api/beatport/hero-tracks', signal ? { signal } : undefined);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.tracks && data.tracks.length > 0) {
|
|
|
console.log(`✅ Loaded ${data.tracks.length} Beatport tracks`);
|
|
|
populateBeatportSlider(data.tracks);
|
|
|
} else {
|
|
|
console.warn('❌ No tracks received from Beatport API, using placeholder data');
|
|
|
setupBeatportSliderWithPlaceholders();
|
|
|
}
|
|
|
} catch (error) {
|
|
|
if (error && error.name === 'AbortError') return;
|
|
|
console.error('❌ Error loading Beatport tracks:', error);
|
|
|
setupBeatportSliderWithPlaceholders();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Populate the slider with real Beatport track data
|
|
|
*/
|
|
|
function populateBeatportSlider(tracks) {
|
|
|
const sliderTrack = document.getElementById('beatport-rebuild-slider-track');
|
|
|
const indicatorsContainer = document.querySelector('.beatport-rebuild-slider-indicators');
|
|
|
|
|
|
if (!sliderTrack || !indicatorsContainer) {
|
|
|
console.warn('Slider elements not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Clear existing content
|
|
|
sliderTrack.innerHTML = '';
|
|
|
indicatorsContainer.innerHTML = '';
|
|
|
|
|
|
// Update state
|
|
|
beatportRebuildSliderState.totalSlides = tracks.length;
|
|
|
beatportRebuildSliderState.currentSlide = 0;
|
|
|
|
|
|
// Generate slides HTML
|
|
|
tracks.forEach((track, index) => {
|
|
|
const slideHtml = `
|
|
|
<div class="beatport-rebuild-slide ${index === 0 ? 'active' : ''}"
|
|
|
data-slide="${index}"
|
|
|
data-url="${track.url}"
|
|
|
data-image="${track.image_url}"
|
|
|
style="--slide-bg-image: url('${track.image_url}')">
|
|
|
<div class="beatport-rebuild-slide-background">
|
|
|
<div class="beatport-rebuild-slide-gradient"></div>
|
|
|
</div>
|
|
|
<div class="beatport-rebuild-slide-content">
|
|
|
<div class="beatport-rebuild-track-info">
|
|
|
<h2 class="beatport-rebuild-track-title">${track.title}</h2>
|
|
|
<p class="beatport-rebuild-artist-name">${track.artist}</p>
|
|
|
<p class="beatport-rebuild-album-name">New on Beatport</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
sliderTrack.insertAdjacentHTML('beforeend', slideHtml);
|
|
|
|
|
|
// Add indicator
|
|
|
const indicatorHtml = `<button class="beatport-rebuild-indicator ${index === 0 ? 'active' : ''}" data-slide="${index}"></button>`;
|
|
|
indicatorsContainer.insertAdjacentHTML('beforeend', indicatorHtml);
|
|
|
});
|
|
|
|
|
|
// Now set up all the functionality
|
|
|
setupBeatportSliderFunctionality();
|
|
|
|
|
|
// Add individual click handlers for each slide (like top 10 releases pattern)
|
|
|
setupHeroSliderIndividualClickHandlers(tracks);
|
|
|
|
|
|
console.log(`✅ Populated slider with ${tracks.length} real Beatport tracks`);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up individual click handlers for hero slider slides (like top 10 releases)
|
|
|
*/
|
|
|
function setupHeroSliderIndividualClickHandlers(tracks) {
|
|
|
const slides = document.querySelectorAll('.beatport-rebuild-slide[data-url]');
|
|
|
|
|
|
slides.forEach((slide, index) => {
|
|
|
const releaseUrl = slide.getAttribute('data-url');
|
|
|
if (releaseUrl && releaseUrl !== '#' && releaseUrl !== '') {
|
|
|
// Create release data object from the track data (similar to top 10 releases)
|
|
|
const track = tracks[index];
|
|
|
if (track) {
|
|
|
const releaseData = {
|
|
|
url: releaseUrl,
|
|
|
title: track.title || 'Unknown Title',
|
|
|
artist: track.artist || 'Unknown Artist',
|
|
|
label: track.label || 'Unknown Label',
|
|
|
image_url: track.image_url || ''
|
|
|
};
|
|
|
|
|
|
// Add click handler that mimics the top 10 releases behavior
|
|
|
slide.addEventListener('click', (event) => {
|
|
|
// Prevent navigation button clicks from triggering this
|
|
|
if (event.target.closest('.beatport-rebuild-nav-btn') ||
|
|
|
event.target.closest('.beatport-rebuild-indicator')) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
console.log(`🎯 Hero slider slide clicked: ${releaseData.title} by ${releaseData.artist}`);
|
|
|
handleBeatportReleaseCardClick(slide, releaseData);
|
|
|
});
|
|
|
|
|
|
slide.style.cursor = 'pointer';
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
console.log(`✅ Set up individual click handlers for ${slides.length} hero slider slides`);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up placeholder data if API fails
|
|
|
*/
|
|
|
function setupBeatportSliderWithPlaceholders() {
|
|
|
console.log('🔄 Setting up slider with placeholder data...');
|
|
|
|
|
|
// The HTML already has placeholder slides, just set up functionality
|
|
|
setupBeatportSliderFunctionality();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up all slider functionality after content is loaded
|
|
|
*/
|
|
|
function setupBeatportSliderFunctionality() {
|
|
|
// Set up navigation buttons
|
|
|
setupBeatportRebuildSliderNavigation();
|
|
|
|
|
|
// Set up indicators
|
|
|
setupBeatportRebuildSliderIndicators();
|
|
|
|
|
|
|
|
|
// Start auto-play
|
|
|
startBeatportRebuildSliderAutoPlay();
|
|
|
|
|
|
// Set up pause on hover
|
|
|
setupBeatportRebuildSliderHoverPause();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up navigation button functionality
|
|
|
*/
|
|
|
function setupBeatportRebuildSliderNavigation() {
|
|
|
const prevBtn = document.getElementById('beatport-rebuild-prev-btn');
|
|
|
const nextBtn = document.getElementById('beatport-rebuild-next-btn');
|
|
|
|
|
|
if (prevBtn) {
|
|
|
prevBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Previous button clicked, current slide:', beatportRebuildSliderState.currentSlide);
|
|
|
goToBeatportRebuildSlide(beatportRebuildSliderState.currentSlide - 1);
|
|
|
resetBeatportRebuildSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (nextBtn) {
|
|
|
nextBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Next button clicked, current slide:', beatportRebuildSliderState.currentSlide);
|
|
|
goToBeatportRebuildSlide(beatportRebuildSliderState.currentSlide + 1);
|
|
|
resetBeatportRebuildSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up indicator functionality
|
|
|
*/
|
|
|
function setupBeatportRebuildSliderIndicators() {
|
|
|
const indicators = document.querySelectorAll('.beatport-rebuild-indicator');
|
|
|
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
goToBeatportRebuildSlide(index);
|
|
|
resetBeatportRebuildSliderAutoPlay();
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Navigate to a specific slide
|
|
|
*/
|
|
|
function goToBeatportRebuildSlide(slideIndex) {
|
|
|
console.log('goToBeatportRebuildSlide called with:', slideIndex, 'current:', beatportRebuildSliderState.currentSlide);
|
|
|
|
|
|
// Wrap around if out of bounds
|
|
|
if (slideIndex < 0) {
|
|
|
slideIndex = beatportRebuildSliderState.totalSlides - 1;
|
|
|
} else if (slideIndex >= beatportRebuildSliderState.totalSlides) {
|
|
|
slideIndex = 0;
|
|
|
}
|
|
|
|
|
|
console.log('After wrapping, slideIndex:', slideIndex);
|
|
|
|
|
|
// Update current slide
|
|
|
beatportRebuildSliderState.currentSlide = slideIndex;
|
|
|
|
|
|
// Update slide visibility
|
|
|
const slides = document.querySelectorAll('.beatport-rebuild-slide');
|
|
|
slides.forEach((slide, index) => {
|
|
|
slide.classList.remove('active', 'prev', 'next');
|
|
|
|
|
|
if (index === slideIndex) {
|
|
|
slide.classList.add('active');
|
|
|
} else if (index < slideIndex) {
|
|
|
slide.classList.add('prev');
|
|
|
} else {
|
|
|
slide.classList.add('next');
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Update indicators
|
|
|
const indicators = document.querySelectorAll('.beatport-rebuild-indicator');
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.classList.toggle('active', index === slideIndex);
|
|
|
});
|
|
|
|
|
|
console.log('Slide updated to:', beatportRebuildSliderState.currentSlide);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Start auto-play functionality
|
|
|
*/
|
|
|
function startBeatportRebuildSliderAutoPlay() {
|
|
|
if (beatportRebuildSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportRebuildSliderState.autoPlayInterval);
|
|
|
}
|
|
|
|
|
|
beatportRebuildSliderState.autoPlayInterval = setInterval(() => {
|
|
|
goToBeatportRebuildSlide(beatportRebuildSliderState.currentSlide + 1);
|
|
|
}, beatportRebuildSliderState.autoPlayDelay);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Reset auto-play timer
|
|
|
*/
|
|
|
function resetBeatportRebuildSliderAutoPlay() {
|
|
|
startBeatportRebuildSliderAutoPlay();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up hover pause functionality
|
|
|
*/
|
|
|
function setupBeatportRebuildSliderHoverPause() {
|
|
|
const sliderContainer = document.querySelector('.beatport-rebuild-slider-container');
|
|
|
|
|
|
if (sliderContainer) {
|
|
|
sliderContainer.addEventListener('mouseenter', () => {
|
|
|
if (beatportRebuildSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportRebuildSliderState.autoPlayInterval);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
sliderContainer.addEventListener('mouseleave', () => {
|
|
|
startBeatportRebuildSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
/**
|
|
|
* Clean up beatport rebuild slider when switching away
|
|
|
*/
|
|
|
function cleanupBeatportRebuildSlider() {
|
|
|
if (beatportRebuildSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportRebuildSliderState.autoPlayInterval);
|
|
|
beatportRebuildSliderState.autoPlayInterval = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===================================
|
|
|
// BEATPORT NEW RELEASES SLIDER
|
|
|
// ===================================
|
|
|
|
|
|
// State management for new releases slider (copied from hero slider)
|
|
|
let beatportReleasesSliderState = {
|
|
|
currentSlide: 0,
|
|
|
totalSlides: 0,
|
|
|
autoPlayInterval: null,
|
|
|
autoPlayDelay: 8000,
|
|
|
isInitialized: false
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Initialize the beatport new releases slider functionality (based on hero slider)
|
|
|
*/
|
|
|
function initializeBeatportReleasesSlider() {
|
|
|
console.log('🆕 Initializing beatport new releases slider...');
|
|
|
|
|
|
const slider = document.getElementById('beatport-releases-slider');
|
|
|
if (!slider) {
|
|
|
console.warn('Beatport releases slider not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Prevent double initialization
|
|
|
if (slider.dataset.initialized === 'true') {
|
|
|
console.log('Releases slider already initialized');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const sliderTrack = document.getElementById('beatport-releases-slider-track');
|
|
|
const indicatorsContainer = document.getElementById('beatport-releases-slider-indicators');
|
|
|
|
|
|
if (!sliderTrack || !indicatorsContainer) {
|
|
|
console.warn('Releases slider elements not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Load data and initialize
|
|
|
loadBeatportNewReleases().then(success => {
|
|
|
if (success) {
|
|
|
setupBeatportReleasesSliderNavigation();
|
|
|
setupBeatportReleasesSliderIndicators();
|
|
|
setupBeatportReleasesSliderHoverPause();
|
|
|
startBeatportReleasesSliderAutoPlay();
|
|
|
slider.dataset.initialized = 'true';
|
|
|
beatportReleasesSliderState.isInitialized = true;
|
|
|
console.log('✅ New releases slider initialized successfully');
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Load new releases data from API
|
|
|
*/
|
|
|
async function loadBeatportNewReleases() {
|
|
|
try {
|
|
|
console.log('📡 Fetching new releases data...');
|
|
|
|
|
|
const signal = getBeatportContentSignal();
|
|
|
const response = await fetch('/api/beatport/new-releases', signal ? { signal } : undefined);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.releases && data.releases.length > 0) {
|
|
|
console.log(`📀 Loaded ${data.releases.length} releases`);
|
|
|
populateBeatportReleasesSlider(data.releases);
|
|
|
return true;
|
|
|
} else {
|
|
|
console.error('Failed to load releases:', data.error || 'No releases found');
|
|
|
showBeatportReleasesError(data.error || 'No releases available');
|
|
|
return false;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
if (error && error.name === 'AbortError') return false;
|
|
|
console.error('Error loading new releases:', error);
|
|
|
showBeatportReleasesError('Failed to load releases');
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Populate the releases slider with data (based on hero slider)
|
|
|
*/
|
|
|
function populateBeatportReleasesSlider(releases) {
|
|
|
const sliderTrack = document.getElementById('beatport-releases-slider-track');
|
|
|
const indicatorsContainer = document.getElementById('beatport-releases-slider-indicators');
|
|
|
|
|
|
if (!sliderTrack || !indicatorsContainer) return;
|
|
|
|
|
|
// Calculate slides needed (10 cards per slide)
|
|
|
const cardsPerSlide = 10;
|
|
|
const totalSlides = Math.ceil(releases.length / cardsPerSlide);
|
|
|
|
|
|
// Clear existing content
|
|
|
sliderTrack.innerHTML = '';
|
|
|
indicatorsContainer.innerHTML = '';
|
|
|
|
|
|
// Update state
|
|
|
beatportReleasesSliderState.totalSlides = totalSlides;
|
|
|
beatportReleasesSliderState.currentSlide = 0;
|
|
|
|
|
|
console.log(`🎯 Creating ${totalSlides} slides with ${cardsPerSlide} cards each`);
|
|
|
|
|
|
// Generate slides HTML (similar to hero slider)
|
|
|
for (let slideIndex = 0; slideIndex < totalSlides; slideIndex++) {
|
|
|
const startIndex = slideIndex * cardsPerSlide;
|
|
|
const endIndex = Math.min(startIndex + cardsPerSlide, releases.length);
|
|
|
const slideReleases = releases.slice(startIndex, endIndex);
|
|
|
|
|
|
// Create grid HTML for this slide
|
|
|
let gridHtml = '';
|
|
|
for (let i = 0; i < cardsPerSlide; i++) {
|
|
|
if (i < slideReleases.length) {
|
|
|
const release = slideReleases[i];
|
|
|
gridHtml += `
|
|
|
<div class="beatport-release-card" data-url="${release.url}" style="--card-bg-image: url('${release.image_url}')">
|
|
|
<div class="beatport-release-card-content">
|
|
|
<div class="beatport-release-artwork">
|
|
|
${release.image_url ? `<img src="${release.image_url}" alt="${release.title}" loading="lazy">` : ''}
|
|
|
</div>
|
|
|
<div class="beatport-release-info">
|
|
|
<div class="beatport-release-title" title="${release.title}">${release.title}</div>
|
|
|
<div class="beatport-release-artist" title="${release.artist}">${release.artist}</div>
|
|
|
<div class="beatport-release-label" title="${release.label}">${release.label}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
} else {
|
|
|
// Placeholder card
|
|
|
gridHtml += `
|
|
|
<div class="beatport-release-card beatport-release-placeholder">
|
|
|
<div class="beatport-release-card-content">
|
|
|
<div class="beatport-release-artwork">
|
|
|
<div class="placeholder-icon">📀</div>
|
|
|
</div>
|
|
|
<div class="beatport-release-info">
|
|
|
<div class="beatport-release-title">More Releases</div>
|
|
|
<div class="beatport-release-artist">Coming Soon</div>
|
|
|
<div class="beatport-release-label">Beatport</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const slideHtml = `
|
|
|
<div class="beatport-releases-slide ${slideIndex === 0 ? 'active' : ''}"
|
|
|
data-slide="${slideIndex}">
|
|
|
<div class="beatport-releases-grid">
|
|
|
${gridHtml}
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
sliderTrack.innerHTML += slideHtml;
|
|
|
|
|
|
// Create indicator
|
|
|
const indicatorHtml = `<button class="beatport-releases-indicator ${slideIndex === 0 ? 'active' : ''}" data-slide="${slideIndex}"></button>`;
|
|
|
indicatorsContainer.innerHTML += indicatorHtml;
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Created ${totalSlides} slides for releases slider`);
|
|
|
|
|
|
// Add click handlers for individual release discovery (matching Top 10 Releases pattern)
|
|
|
const releaseCards = sliderTrack.querySelectorAll('.beatport-release-card[data-url]:not(.beatport-release-placeholder)');
|
|
|
releaseCards.forEach((card) => {
|
|
|
const releaseUrl = card.getAttribute('data-url');
|
|
|
if (releaseUrl && releaseUrl !== '#') {
|
|
|
// Find the corresponding release data
|
|
|
const releaseData = releases.find(release => release.url === releaseUrl);
|
|
|
if (releaseData) {
|
|
|
card.addEventListener('click', () => handleBeatportReleaseCardClick(card, releaseData));
|
|
|
card.style.cursor = 'pointer';
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up navigation functionality (copied from hero slider)
|
|
|
*/
|
|
|
function setupBeatportReleasesSliderNavigation() {
|
|
|
const prevBtn = document.getElementById('beatport-releases-prev-btn');
|
|
|
const nextBtn = document.getElementById('beatport-releases-next-btn');
|
|
|
|
|
|
if (prevBtn) {
|
|
|
// Clone button to remove all existing event listeners
|
|
|
const newPrevBtn = prevBtn.cloneNode(true);
|
|
|
prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn);
|
|
|
|
|
|
newPrevBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Previous releases button clicked, current slide:', beatportReleasesSliderState.currentSlide);
|
|
|
goToBeatportReleasesSlide(beatportReleasesSliderState.currentSlide - 1);
|
|
|
resetBeatportReleasesSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (nextBtn) {
|
|
|
// Clone button to remove all existing event listeners
|
|
|
const newNextBtn = nextBtn.cloneNode(true);
|
|
|
nextBtn.parentNode.replaceChild(newNextBtn, nextBtn);
|
|
|
|
|
|
newNextBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Next releases button clicked, current slide:', beatportReleasesSliderState.currentSlide);
|
|
|
goToBeatportReleasesSlide(beatportReleasesSliderState.currentSlide + 1);
|
|
|
resetBeatportReleasesSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up indicator functionality (copied from hero slider)
|
|
|
*/
|
|
|
function setupBeatportReleasesSliderIndicators() {
|
|
|
const indicators = document.querySelectorAll('.beatport-releases-indicator');
|
|
|
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.addEventListener('click', () => {
|
|
|
goToBeatportReleasesSlide(index);
|
|
|
resetBeatportReleasesSliderAutoPlay();
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Navigate to a specific slide (copied from hero slider)
|
|
|
*/
|
|
|
function goToBeatportReleasesSlide(slideIndex) {
|
|
|
console.log('goToBeatportReleasesSlide called with:', slideIndex, 'current:', beatportReleasesSliderState.currentSlide);
|
|
|
|
|
|
// Wrap around if out of bounds
|
|
|
if (slideIndex < 0) {
|
|
|
slideIndex = beatportReleasesSliderState.totalSlides - 1;
|
|
|
} else if (slideIndex >= beatportReleasesSliderState.totalSlides) {
|
|
|
slideIndex = 0;
|
|
|
}
|
|
|
|
|
|
console.log('After wrapping, slideIndex:', slideIndex);
|
|
|
|
|
|
// Update current slide
|
|
|
beatportReleasesSliderState.currentSlide = slideIndex;
|
|
|
|
|
|
// Update slide visibility
|
|
|
const slides = document.querySelectorAll('.beatport-releases-slide');
|
|
|
slides.forEach((slide, index) => {
|
|
|
slide.classList.remove('active', 'prev', 'next');
|
|
|
|
|
|
if (index === slideIndex) {
|
|
|
slide.classList.add('active');
|
|
|
} else if (index < slideIndex) {
|
|
|
slide.classList.add('prev');
|
|
|
} else {
|
|
|
slide.classList.add('next');
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Update indicators
|
|
|
const indicators = document.querySelectorAll('.beatport-releases-indicator');
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.classList.toggle('active', index === slideIndex);
|
|
|
});
|
|
|
|
|
|
console.log('Releases slide updated to:', beatportReleasesSliderState.currentSlide);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Start auto-play functionality (copied from hero slider)
|
|
|
*/
|
|
|
function startBeatportReleasesSliderAutoPlay() {
|
|
|
if (beatportReleasesSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportReleasesSliderState.autoPlayInterval);
|
|
|
}
|
|
|
|
|
|
beatportReleasesSliderState.autoPlayInterval = setInterval(() => {
|
|
|
goToBeatportReleasesSlide(beatportReleasesSliderState.currentSlide + 1);
|
|
|
}, beatportReleasesSliderState.autoPlayDelay);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Reset auto-play timer (copied from hero slider)
|
|
|
*/
|
|
|
function resetBeatportReleasesSliderAutoPlay() {
|
|
|
startBeatportReleasesSliderAutoPlay();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up hover pause functionality (copied from hero slider)
|
|
|
*/
|
|
|
function setupBeatportReleasesSliderHoverPause() {
|
|
|
const sliderContainer = document.querySelector('.beatport-releases-slider-container');
|
|
|
|
|
|
if (sliderContainer) {
|
|
|
sliderContainer.addEventListener('mouseenter', () => {
|
|
|
if (beatportReleasesSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportReleasesSliderState.autoPlayInterval);
|
|
|
beatportReleasesSliderState.autoPlayInterval = null;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
sliderContainer.addEventListener('mouseleave', () => {
|
|
|
startBeatportReleasesSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Show error state
|
|
|
*/
|
|
|
function showBeatportReleasesError(errorMessage) {
|
|
|
const sliderTrack = document.getElementById('beatport-releases-slider-track');
|
|
|
if (!sliderTrack) return;
|
|
|
|
|
|
sliderTrack.innerHTML = `
|
|
|
<div class="beatport-releases-loading">
|
|
|
<div class="beatport-releases-loading-content">
|
|
|
<h3>❌ Error Loading Releases</h3>
|
|
|
<p>${errorMessage}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Clean up releases slider when switching away (copied from hero slider)
|
|
|
*/
|
|
|
function cleanupBeatportReleasesSlider() {
|
|
|
if (beatportReleasesSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportReleasesSliderState.autoPlayInterval);
|
|
|
beatportReleasesSliderState.autoPlayInterval = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===================================
|
|
|
// BEATPORT HYPE PICKS SLIDER
|
|
|
// ===================================
|
|
|
|
|
|
// Hype Picks Slider State
|
|
|
let beatportHypePicksSliderState = {
|
|
|
currentSlide: 0,
|
|
|
totalSlides: 0,
|
|
|
autoPlayInterval: null,
|
|
|
autoPlayDelay: 4000,
|
|
|
isInitialized: false
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Initialize the beatport hype picks slider functionality (based on releases slider)
|
|
|
*/
|
|
|
function initializeBeatportHypePicksSlider() {
|
|
|
console.log('🔥 Initializing beatport hype picks slider...');
|
|
|
|
|
|
const slider = document.getElementById('beatport-hype-picks-slider');
|
|
|
if (!slider) {
|
|
|
console.warn('Beatport hype picks slider not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Check if already initialized
|
|
|
if (beatportHypePicksSliderState.isInitialized) {
|
|
|
console.log('Beatport hype picks slider already initialized, skipping...');
|
|
|
startBeatportHypePicksSliderAutoPlay(); // Just restart autoplay
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Mark as initialized
|
|
|
beatportHypePicksSliderState.isInitialized = true;
|
|
|
|
|
|
// Reset state
|
|
|
beatportHypePicksSliderState.currentSlide = 0;
|
|
|
beatportHypePicksSliderState.totalSlides = 0;
|
|
|
|
|
|
// Load data and initialize
|
|
|
loadBeatportHypePicks().then(success => {
|
|
|
if (success) {
|
|
|
setupBeatportHypePicksSliderNavigation();
|
|
|
setupBeatportHypePicksSliderIndicators();
|
|
|
setupBeatportHypePicksSliderHoverPause();
|
|
|
startBeatportHypePicksSliderAutoPlay();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
console.log('✅ Beatport hype picks slider initialized successfully');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Load hype picks data from API
|
|
|
*/
|
|
|
async function loadBeatportHypePicks() {
|
|
|
try {
|
|
|
console.log('🔥 Fetching hype picks data...');
|
|
|
|
|
|
const signal = getBeatportContentSignal();
|
|
|
const response = await fetch('/api/beatport/hype-picks', signal ? { signal } : undefined);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.releases && data.releases.length > 0) {
|
|
|
console.log(`🔥 Loaded ${data.releases.length} hype picks releases`);
|
|
|
populateBeatportHypePicksSlider(data.releases);
|
|
|
return true;
|
|
|
} else {
|
|
|
console.error('Failed to load hype picks:', data.error || 'No hype picks found');
|
|
|
showBeatportHypePicksError(data.error || 'No hype picks available');
|
|
|
return false;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
if (error && error.name === 'AbortError') return false;
|
|
|
console.error('Error loading hype picks:', error);
|
|
|
showBeatportHypePicksError('Failed to load hype picks');
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Populate the hype picks slider with data (based on releases slider)
|
|
|
*/
|
|
|
function populateBeatportHypePicksSlider(releases) {
|
|
|
const sliderTrack = document.getElementById('beatport-hype-picks-slider-track');
|
|
|
const indicatorsContainer = document.getElementById('beatport-hype-picks-slider-indicators');
|
|
|
|
|
|
if (!sliderTrack || !indicatorsContainer) return;
|
|
|
|
|
|
// Clear existing content
|
|
|
sliderTrack.innerHTML = '';
|
|
|
indicatorsContainer.innerHTML = '';
|
|
|
|
|
|
// Group releases into slides (10 releases per slide in 5x2 grid)
|
|
|
const releasesPerSlide = 10;
|
|
|
const slides = [];
|
|
|
for (let i = 0; i < releases.length; i += releasesPerSlide) {
|
|
|
slides.push(releases.slice(i, i + releasesPerSlide));
|
|
|
}
|
|
|
|
|
|
console.log(`🔥 Hype Picks: Got ${releases.length} releases, creating ${slides.length} slides`);
|
|
|
beatportHypePicksSliderState.totalSlides = slides.length;
|
|
|
beatportHypePicksSliderState.currentSlide = 0;
|
|
|
|
|
|
// Create slides
|
|
|
slides.forEach((slideReleases, slideIndex) => {
|
|
|
const slideHtml = `
|
|
|
<div class="beatport-hype-picks-slide ${slideIndex === 0 ? 'active' : ''}"
|
|
|
data-slide="${slideIndex}">
|
|
|
<div class="beatport-hype-picks-grid">
|
|
|
${slideReleases.map(release => createBeatportHypePickCard(release)).join('')}
|
|
|
${slideReleases.length < releasesPerSlide ?
|
|
|
Array(releasesPerSlide - slideReleases.length).fill(0).map(() =>
|
|
|
`<div class="beatport-hype-pick-card beatport-hype-pick-placeholder">
|
|
|
<div class="placeholder-icon">🔥</div>
|
|
|
</div>`
|
|
|
).join('') : ''
|
|
|
}
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
sliderTrack.insertAdjacentHTML('beforeend', slideHtml);
|
|
|
console.log(`🔥 Created slide ${slideIndex + 1}/${slides.length} with ${slideReleases.length} releases`);
|
|
|
|
|
|
// Create indicator
|
|
|
const indicatorHtml = `<button class="beatport-hype-picks-indicator ${slideIndex === 0 ? 'active' : ''}" data-slide="${slideIndex}"></button>`;
|
|
|
indicatorsContainer.insertAdjacentHTML('beforeend', indicatorHtml);
|
|
|
});
|
|
|
|
|
|
// Add click handlers to track cards
|
|
|
setupBeatportHypePickCardHandlers();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Create a hype pick card HTML (for release cards, same as new releases)
|
|
|
*/
|
|
|
function createBeatportHypePickCard(release) {
|
|
|
const artworkUrl = release.image_url || '';
|
|
|
const bgStyle = artworkUrl ? `style="--card-bg-image: url('${artworkUrl}')"` : '';
|
|
|
|
|
|
return `
|
|
|
<div class="beatport-hype-pick-card" data-url="${release.url || ''}" ${bgStyle}>
|
|
|
<div class="beatport-hype-pick-card-content">
|
|
|
<div class="beatport-hype-pick-artwork">
|
|
|
${artworkUrl ? `<img src="${artworkUrl}" alt="${release.title || 'Release'}" loading="lazy">` : ''}
|
|
|
</div>
|
|
|
<div class="beatport-hype-pick-info">
|
|
|
<div class="beatport-hype-pick-title">${release.title || 'Unknown Title'}</div>
|
|
|
<div class="beatport-hype-pick-artist">${release.artist || 'Unknown Artist'}</div>
|
|
|
<div class="beatport-hype-pick-label">${release.label || 'Hype Pick'}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Setup navigation for hype picks slider (same pattern as releases)
|
|
|
*/
|
|
|
function setupBeatportHypePicksSliderNavigation() {
|
|
|
const prevBtn = document.getElementById('beatport-hype-picks-prev-btn');
|
|
|
const nextBtn = document.getElementById('beatport-hype-picks-next-btn');
|
|
|
|
|
|
if (prevBtn) {
|
|
|
// Clone button to remove all existing event listeners
|
|
|
const newPrevBtn = prevBtn.cloneNode(true);
|
|
|
prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn);
|
|
|
|
|
|
newPrevBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Previous hype picks button clicked, current slide:', beatportHypePicksSliderState.currentSlide);
|
|
|
goToBeatportHypePicksSlide(beatportHypePicksSliderState.currentSlide - 1);
|
|
|
resetBeatportHypePicksSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (nextBtn) {
|
|
|
// Clone button to remove all existing event listeners
|
|
|
const newNextBtn = nextBtn.cloneNode(true);
|
|
|
nextBtn.parentNode.replaceChild(newNextBtn, nextBtn);
|
|
|
|
|
|
newNextBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Next hype picks button clicked, current slide:', beatportHypePicksSliderState.currentSlide);
|
|
|
goToBeatportHypePicksSlide(beatportHypePicksSliderState.currentSlide + 1);
|
|
|
resetBeatportHypePicksSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Setup indicators for hype picks slider
|
|
|
*/
|
|
|
function setupBeatportHypePicksSliderIndicators() {
|
|
|
const indicators = document.querySelectorAll('.beatport-hype-picks-indicator');
|
|
|
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.addEventListener('click', () => {
|
|
|
goToBeatportHypePicksSlide(index);
|
|
|
resetBeatportHypePicksSliderAutoPlay();
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Navigate to specific slide
|
|
|
*/
|
|
|
function goToBeatportHypePicksSlide(slideIndex) {
|
|
|
console.log('goToBeatportHypePicksSlide called with:', slideIndex, 'current:', beatportHypePicksSliderState.currentSlide);
|
|
|
|
|
|
// Handle wrap around
|
|
|
if (slideIndex < 0) {
|
|
|
slideIndex = beatportHypePicksSliderState.totalSlides - 1;
|
|
|
} else if (slideIndex >= beatportHypePicksSliderState.totalSlides) {
|
|
|
slideIndex = 0;
|
|
|
}
|
|
|
|
|
|
// Update current slide
|
|
|
beatportHypePicksSliderState.currentSlide = slideIndex;
|
|
|
|
|
|
// Update slides
|
|
|
const slides = document.querySelectorAll('.beatport-hype-picks-slide');
|
|
|
slides.forEach((slide, index) => {
|
|
|
slide.classList.remove('active', 'prev', 'next');
|
|
|
if (index === slideIndex) {
|
|
|
slide.classList.add('active');
|
|
|
} else if (index < slideIndex) {
|
|
|
slide.classList.add('prev');
|
|
|
} else {
|
|
|
slide.classList.add('next');
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Update indicators
|
|
|
const indicators = document.querySelectorAll('.beatport-hype-picks-indicator');
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.classList.toggle('active', index === slideIndex);
|
|
|
});
|
|
|
|
|
|
console.log('Slide updated to:', beatportHypePicksSliderState.currentSlide);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Start auto-play for hype picks slider
|
|
|
*/
|
|
|
function startBeatportHypePicksSliderAutoPlay() {
|
|
|
if (beatportHypePicksSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportHypePicksSliderState.autoPlayInterval);
|
|
|
}
|
|
|
|
|
|
beatportHypePicksSliderState.autoPlayInterval = setInterval(() => {
|
|
|
goToBeatportHypePicksSlide(beatportHypePicksSliderState.currentSlide + 1);
|
|
|
}, beatportHypePicksSliderState.autoPlayDelay);
|
|
|
|
|
|
console.log('🔥 Hype picks slider autoplay started');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Reset auto-play for hype picks slider
|
|
|
*/
|
|
|
function resetBeatportHypePicksSliderAutoPlay() {
|
|
|
startBeatportHypePicksSliderAutoPlay();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Setup hover pause for hype picks slider
|
|
|
*/
|
|
|
function setupBeatportHypePicksSliderHoverPause() {
|
|
|
const sliderContainer = document.querySelector('.beatport-hype-picks-slider-container');
|
|
|
if (sliderContainer) {
|
|
|
sliderContainer.addEventListener('mouseenter', () => {
|
|
|
if (beatportHypePicksSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportHypePicksSliderState.autoPlayInterval);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
sliderContainer.addEventListener('mouseleave', () => {
|
|
|
startBeatportHypePicksSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Setup click handlers for hype pick cards
|
|
|
*/
|
|
|
function setupBeatportHypePickCardHandlers() {
|
|
|
const cards = document.querySelectorAll('.beatport-hype-pick-card:not(.beatport-hype-pick-placeholder)');
|
|
|
|
|
|
cards.forEach(card => {
|
|
|
const releaseUrl = card.getAttribute('data-url');
|
|
|
if (releaseUrl && releaseUrl !== '#' && releaseUrl !== '') {
|
|
|
// Extract release data from the card elements
|
|
|
const titleElement = card.querySelector('.beatport-hype-pick-title');
|
|
|
const artistElement = card.querySelector('.beatport-hype-pick-artist');
|
|
|
const labelElement = card.querySelector('.beatport-hype-pick-label');
|
|
|
const imageElement = card.querySelector('.beatport-hype-pick-artwork img');
|
|
|
|
|
|
const releaseData = {
|
|
|
url: releaseUrl,
|
|
|
title: titleElement ? titleElement.textContent.trim() : 'Unknown Title',
|
|
|
artist: artistElement ? artistElement.textContent.trim() : 'Unknown Artist',
|
|
|
label: labelElement ? labelElement.textContent.trim() : 'Unknown Label',
|
|
|
image_url: imageElement ? imageElement.src : ''
|
|
|
};
|
|
|
|
|
|
card.addEventListener('click', () => handleBeatportReleaseCardClick(card, releaseData));
|
|
|
card.style.cursor = 'pointer';
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Show error state for hype picks slider
|
|
|
*/
|
|
|
function showBeatportHypePicksError(errorMessage) {
|
|
|
const sliderTrack = document.getElementById('beatport-hype-picks-slider-track');
|
|
|
if (sliderTrack) {
|
|
|
sliderTrack.innerHTML = `
|
|
|
<div class="beatport-hype-picks-loading">
|
|
|
<div class="beatport-hype-picks-loading-content">
|
|
|
<h3>❌ Error Loading Hype Picks</h3>
|
|
|
<p>${errorMessage}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Clean up hype picks slider when switching away
|
|
|
*/
|
|
|
function cleanupBeatportHypePicksSlider() {
|
|
|
if (beatportHypePicksSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportHypePicksSliderState.autoPlayInterval);
|
|
|
beatportHypePicksSliderState.autoPlayInterval = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===================================
|
|
|
// BEATPORT FEATURED CHARTS SLIDER
|
|
|
// ===================================
|
|
|
|
|
|
// State management for featured charts slider (copied from releases slider)
|
|
|
let beatportChartsSliderState = {
|
|
|
currentSlide: 0,
|
|
|
totalSlides: 0,
|
|
|
autoPlayInterval: null,
|
|
|
autoPlayDelay: 10000, // Slightly longer auto-play for charts
|
|
|
isInitialized: false
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Initialize the beatport featured charts slider functionality (based on releases slider)
|
|
|
*/
|
|
|
function initializeBeatportChartsSlider() {
|
|
|
console.log('🔥 Initializing beatport featured charts slider...');
|
|
|
|
|
|
const slider = document.getElementById('beatport-charts-slider');
|
|
|
if (!slider) {
|
|
|
console.warn('Beatport charts slider not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Prevent double initialization
|
|
|
if (slider.dataset.initialized === 'true') {
|
|
|
console.log('Charts slider already initialized');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const sliderTrack = document.getElementById('beatport-charts-slider-track');
|
|
|
const indicatorsContainer = document.getElementById('beatport-charts-slider-indicators');
|
|
|
|
|
|
if (!sliderTrack || !indicatorsContainer) {
|
|
|
console.warn('Charts slider elements not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Load data and initialize
|
|
|
loadBeatportFeaturedCharts().then(success => {
|
|
|
if (success) {
|
|
|
setupBeatportChartsSliderNavigation();
|
|
|
setupBeatportChartsSliderIndicators();
|
|
|
setupBeatportChartsSliderHoverPause();
|
|
|
startBeatportChartsSliderAutoPlay();
|
|
|
slider.dataset.initialized = 'true';
|
|
|
beatportChartsSliderState.isInitialized = true;
|
|
|
console.log('✅ Featured charts slider initialized successfully');
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Load featured charts data from API
|
|
|
*/
|
|
|
async function loadBeatportFeaturedCharts() {
|
|
|
try {
|
|
|
console.log('📊 Loading featured charts data...');
|
|
|
const signal = getBeatportContentSignal();
|
|
|
const response = await fetch('/api/beatport/featured-charts', signal ? { signal } : undefined);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.charts && data.charts.length > 0) {
|
|
|
console.log(`📈 Loaded ${data.charts.length} featured charts`);
|
|
|
createBeatportChartsSlides(data.charts);
|
|
|
return true;
|
|
|
} else {
|
|
|
console.warn('No featured charts data available');
|
|
|
return false;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
if (error && error.name === 'AbortError') return false;
|
|
|
console.error('❌ Error loading featured charts:', error);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Create chart slides with grid layout (copied from releases slider)
|
|
|
*/
|
|
|
function createBeatportChartsSlides(charts) {
|
|
|
const sliderTrack = document.getElementById('beatport-charts-slider-track');
|
|
|
const indicatorsContainer = document.getElementById('beatport-charts-slider-indicators');
|
|
|
|
|
|
if (!sliderTrack || !indicatorsContainer) {
|
|
|
console.error('Charts slider elements not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const cardsPerSlide = 10; // 5x2 grid
|
|
|
const totalSlides = Math.ceil(charts.length / cardsPerSlide);
|
|
|
|
|
|
// Clear existing content
|
|
|
sliderTrack.innerHTML = '';
|
|
|
indicatorsContainer.innerHTML = '';
|
|
|
|
|
|
// Update state
|
|
|
beatportChartsSliderState.totalSlides = totalSlides;
|
|
|
beatportChartsSliderState.currentSlide = 0;
|
|
|
|
|
|
console.log(`🎯 Creating ${totalSlides} chart slides with ${cardsPerSlide} cards each`);
|
|
|
|
|
|
// Generate slides HTML
|
|
|
for (let slideIndex = 0; slideIndex < totalSlides; slideIndex++) {
|
|
|
const startIndex = slideIndex * cardsPerSlide;
|
|
|
const endIndex = Math.min(startIndex + cardsPerSlide, charts.length);
|
|
|
const slideCharts = charts.slice(startIndex, endIndex);
|
|
|
|
|
|
// Create grid HTML for this slide
|
|
|
const gridHtml = slideCharts.map(chart => {
|
|
|
const bgImageStyle = chart.image ? `--chart-bg-image: url('${chart.image}')` : '';
|
|
|
return `
|
|
|
<div class="beatport-chart-card" style="${bgImageStyle}" data-url="${chart.url || ''}">
|
|
|
<div class="beatport-chart-card-content">
|
|
|
<div class="beatport-chart-name">${chart.name || 'Unknown Chart'}</div>
|
|
|
<div class="beatport-chart-creator">${chart.creator || 'Unknown Creator'}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
|
|
|
// Create slide HTML
|
|
|
const slideHtml = `
|
|
|
<div class="beatport-charts-slide ${slideIndex === 0 ? 'active' : ''}">
|
|
|
<div class="beatport-charts-grid">
|
|
|
${gridHtml}
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
sliderTrack.innerHTML += slideHtml;
|
|
|
|
|
|
// Create indicator
|
|
|
const indicatorHtml = `<button class="beatport-charts-indicator ${slideIndex === 0 ? 'active' : ''}" data-slide="${slideIndex}"></button>`;
|
|
|
indicatorsContainer.innerHTML += indicatorHtml;
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Created ${totalSlides} chart slides`);
|
|
|
|
|
|
// Add click handlers for individual chart discovery (matching chart pattern)
|
|
|
const chartCards = sliderTrack.querySelectorAll('.beatport-chart-card[data-url]');
|
|
|
chartCards.forEach((card) => {
|
|
|
const chartUrl = card.getAttribute('data-url');
|
|
|
if (chartUrl && chartUrl !== '') {
|
|
|
// Find the corresponding chart data
|
|
|
const chartData = charts.find(chart => chart.url === chartUrl);
|
|
|
if (chartData) {
|
|
|
card.addEventListener('click', () => handleBeatportChartCardClick(card, chartData));
|
|
|
card.style.cursor = 'pointer';
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up navigation functionality (copied from releases slider with button cloning)
|
|
|
*/
|
|
|
function setupBeatportChartsSliderNavigation() {
|
|
|
const prevBtn = document.getElementById('beatport-charts-prev-btn');
|
|
|
const nextBtn = document.getElementById('beatport-charts-next-btn');
|
|
|
|
|
|
if (prevBtn) {
|
|
|
// Clone button to remove all existing event listeners
|
|
|
const newPrevBtn = prevBtn.cloneNode(true);
|
|
|
prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn);
|
|
|
|
|
|
newPrevBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Previous charts button clicked, current slide:', beatportChartsSliderState.currentSlide);
|
|
|
goToBeatportChartsSlide(beatportChartsSliderState.currentSlide - 1);
|
|
|
resetBeatportChartsSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (nextBtn) {
|
|
|
// Clone button to remove all existing event listeners
|
|
|
const newNextBtn = nextBtn.cloneNode(true);
|
|
|
nextBtn.parentNode.replaceChild(newNextBtn, nextBtn);
|
|
|
|
|
|
newNextBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Next charts button clicked, current slide:', beatportChartsSliderState.currentSlide);
|
|
|
goToBeatportChartsSlide(beatportChartsSliderState.currentSlide + 1);
|
|
|
resetBeatportChartsSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up indicator functionality (copied from releases slider)
|
|
|
*/
|
|
|
function setupBeatportChartsSliderIndicators() {
|
|
|
const indicators = document.querySelectorAll('.beatport-charts-indicator');
|
|
|
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.addEventListener('click', () => {
|
|
|
goToBeatportChartsSlide(index);
|
|
|
resetBeatportChartsSliderAutoPlay();
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Navigate to a specific slide (copied from releases slider)
|
|
|
*/
|
|
|
function goToBeatportChartsSlide(slideIndex) {
|
|
|
console.log('goToBeatportChartsSlide called with:', slideIndex, 'current:', beatportChartsSliderState.currentSlide);
|
|
|
|
|
|
// Wrap around if out of bounds
|
|
|
if (slideIndex < 0) {
|
|
|
slideIndex = beatportChartsSliderState.totalSlides - 1;
|
|
|
} else if (slideIndex >= beatportChartsSliderState.totalSlides) {
|
|
|
slideIndex = 0;
|
|
|
}
|
|
|
|
|
|
console.log('After wrapping, slideIndex:', slideIndex);
|
|
|
|
|
|
// Update current slide
|
|
|
beatportChartsSliderState.currentSlide = slideIndex;
|
|
|
|
|
|
// Update slide visibility
|
|
|
const slides = document.querySelectorAll('.beatport-charts-slide');
|
|
|
slides.forEach((slide, index) => {
|
|
|
slide.classList.remove('active', 'prev', 'next');
|
|
|
|
|
|
if (index === slideIndex) {
|
|
|
slide.classList.add('active');
|
|
|
} else if (index < slideIndex) {
|
|
|
slide.classList.add('prev');
|
|
|
} else {
|
|
|
slide.classList.add('next');
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Update indicators
|
|
|
const indicators = document.querySelectorAll('.beatport-charts-indicator');
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.classList.toggle('active', index === slideIndex);
|
|
|
});
|
|
|
|
|
|
console.log('Charts slide updated to:', beatportChartsSliderState.currentSlide);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Start auto-play functionality (copied from releases slider)
|
|
|
*/
|
|
|
function startBeatportChartsSliderAutoPlay() {
|
|
|
if (beatportChartsSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportChartsSliderState.autoPlayInterval);
|
|
|
}
|
|
|
|
|
|
beatportChartsSliderState.autoPlayInterval = setInterval(() => {
|
|
|
goToBeatportChartsSlide(beatportChartsSliderState.currentSlide + 1);
|
|
|
}, beatportChartsSliderState.autoPlayDelay);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Reset auto-play timer (copied from releases slider)
|
|
|
*/
|
|
|
function resetBeatportChartsSliderAutoPlay() {
|
|
|
startBeatportChartsSliderAutoPlay();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up hover pause functionality (copied from releases slider)
|
|
|
*/
|
|
|
function setupBeatportChartsSliderHoverPause() {
|
|
|
const sliderContainer = document.querySelector('.beatport-charts-slider-container');
|
|
|
|
|
|
if (sliderContainer) {
|
|
|
sliderContainer.addEventListener('mouseenter', () => {
|
|
|
if (beatportChartsSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportChartsSliderState.autoPlayInterval);
|
|
|
beatportChartsSliderState.autoPlayInterval = null;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
sliderContainer.addEventListener('mouseleave', () => {
|
|
|
startBeatportChartsSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Clean up charts slider when switching away (copied from releases slider)
|
|
|
*/
|
|
|
function cleanupBeatportChartsSlider() {
|
|
|
if (beatportChartsSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportChartsSliderState.autoPlayInterval);
|
|
|
beatportChartsSliderState.autoPlayInterval = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===================================
|
|
|
// BEATPORT DJ CHARTS SLIDER
|
|
|
// ===================================
|
|
|
|
|
|
// State management for DJ charts slider (3 cards per slide)
|
|
|
let beatportDJSliderState = {
|
|
|
currentSlide: 0,
|
|
|
totalSlides: 0,
|
|
|
autoPlayInterval: null,
|
|
|
autoPlayDelay: 12000, // Longer auto-play for DJ charts
|
|
|
isInitialized: false
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Initialize the beatport DJ charts slider functionality (based on charts slider)
|
|
|
*/
|
|
|
function initializeBeatportDJSlider() {
|
|
|
console.log('🎧 Initializing beatport DJ charts slider...');
|
|
|
|
|
|
const slider = document.getElementById('beatport-dj-slider');
|
|
|
if (!slider) {
|
|
|
console.warn('Beatport DJ slider not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Prevent double initialization
|
|
|
if (slider.dataset.initialized === 'true') {
|
|
|
console.log('DJ slider already initialized');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const sliderTrack = document.getElementById('beatport-dj-slider-track');
|
|
|
const indicatorsContainer = document.getElementById('beatport-dj-slider-indicators');
|
|
|
|
|
|
if (!sliderTrack || !indicatorsContainer) {
|
|
|
console.warn('DJ slider elements not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Load data and initialize
|
|
|
loadBeatportDJCharts().then(success => {
|
|
|
if (success) {
|
|
|
setupBeatportDJSliderNavigation();
|
|
|
setupBeatportDJSliderIndicators();
|
|
|
setupBeatportDJSliderHoverPause();
|
|
|
startBeatportDJSliderAutoPlay();
|
|
|
slider.dataset.initialized = 'true';
|
|
|
beatportDJSliderState.isInitialized = true;
|
|
|
console.log('✅ DJ charts slider initialized successfully');
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Load DJ charts data from API
|
|
|
*/
|
|
|
async function loadBeatportDJCharts() {
|
|
|
try {
|
|
|
console.log('🎧 Loading DJ charts data...');
|
|
|
const signal = getBeatportContentSignal();
|
|
|
const response = await fetch('/api/beatport/dj-charts', signal ? { signal } : undefined);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.charts && data.charts.length > 0) {
|
|
|
console.log(`📈 Loaded ${data.charts.length} DJ charts`);
|
|
|
createBeatportDJSlides(data.charts);
|
|
|
return true;
|
|
|
} else {
|
|
|
console.warn('No DJ charts data available');
|
|
|
return false;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
if (error && error.name === 'AbortError') return false;
|
|
|
console.error('❌ Error loading DJ charts:', error);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Create DJ chart slides with 3 cards per slide layout
|
|
|
*/
|
|
|
function createBeatportDJSlides(charts) {
|
|
|
const sliderTrack = document.getElementById('beatport-dj-slider-track');
|
|
|
const indicatorsContainer = document.getElementById('beatport-dj-slider-indicators');
|
|
|
|
|
|
if (!sliderTrack || !indicatorsContainer) {
|
|
|
console.error('DJ slider elements not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const cardsPerSlide = 3; // 3 cards per slide for DJ charts
|
|
|
const totalSlides = Math.ceil(charts.length / cardsPerSlide);
|
|
|
|
|
|
// Clear existing content
|
|
|
sliderTrack.innerHTML = '';
|
|
|
indicatorsContainer.innerHTML = '';
|
|
|
|
|
|
// Update state
|
|
|
beatportDJSliderState.totalSlides = totalSlides;
|
|
|
beatportDJSliderState.currentSlide = 0;
|
|
|
|
|
|
console.log(`🎯 Creating ${totalSlides} DJ chart slides with ${cardsPerSlide} cards each`);
|
|
|
|
|
|
// Generate slides HTML
|
|
|
for (let slideIndex = 0; slideIndex < totalSlides; slideIndex++) {
|
|
|
const startIndex = slideIndex * cardsPerSlide;
|
|
|
const endIndex = Math.min(startIndex + cardsPerSlide, charts.length);
|
|
|
const slideCharts = charts.slice(startIndex, endIndex);
|
|
|
|
|
|
// Create grid HTML for this slide
|
|
|
const gridHtml = slideCharts.map(chart => {
|
|
|
const bgImageStyle = chart.image ? `--dj-bg-image: url('${chart.image}')` : '';
|
|
|
return `
|
|
|
<div class="beatport-dj-card" style="${bgImageStyle}" data-url="${chart.url || ''}">
|
|
|
<div class="beatport-dj-card-content">
|
|
|
<div class="beatport-dj-name">${chart.name || 'Unknown Chart'}</div>
|
|
|
<div class="beatport-dj-creator">${chart.creator || 'Unknown Creator'}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
|
|
|
// Create slide HTML
|
|
|
const slideHtml = `
|
|
|
<div class="beatport-dj-slide ${slideIndex === 0 ? 'active' : ''}">
|
|
|
<div class="beatport-dj-grid">
|
|
|
${gridHtml}
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
sliderTrack.innerHTML += slideHtml;
|
|
|
|
|
|
// Create indicator
|
|
|
const indicatorHtml = `<button class="beatport-dj-indicator ${slideIndex === 0 ? 'active' : ''}" data-slide="${slideIndex}"></button>`;
|
|
|
indicatorsContainer.innerHTML += indicatorHtml;
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Created ${totalSlides} DJ chart slides`);
|
|
|
|
|
|
// Add click handlers for individual DJ chart discovery (matching chart pattern)
|
|
|
const djChartCards = sliderTrack.querySelectorAll('.beatport-dj-card[data-url]');
|
|
|
djChartCards.forEach((card) => {
|
|
|
const chartUrl = card.getAttribute('data-url');
|
|
|
if (chartUrl && chartUrl !== '') {
|
|
|
// Find the corresponding chart data
|
|
|
const chartData = charts.find(chart => chart.url === chartUrl);
|
|
|
if (chartData) {
|
|
|
card.addEventListener('click', () => handleBeatportDJChartCardClick(card, chartData));
|
|
|
card.style.cursor = 'pointer';
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up navigation functionality (copied from charts slider with button cloning)
|
|
|
*/
|
|
|
function setupBeatportDJSliderNavigation() {
|
|
|
const prevBtn = document.getElementById('beatport-dj-prev-btn');
|
|
|
const nextBtn = document.getElementById('beatport-dj-next-btn');
|
|
|
|
|
|
if (prevBtn) {
|
|
|
// Clone button to remove all existing event listeners
|
|
|
const newPrevBtn = prevBtn.cloneNode(true);
|
|
|
prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn);
|
|
|
|
|
|
newPrevBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Previous DJ button clicked, current slide:', beatportDJSliderState.currentSlide);
|
|
|
goToBeatportDJSlide(beatportDJSliderState.currentSlide - 1);
|
|
|
resetBeatportDJSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (nextBtn) {
|
|
|
// Clone button to remove all existing event listeners
|
|
|
const newNextBtn = nextBtn.cloneNode(true);
|
|
|
nextBtn.parentNode.replaceChild(newNextBtn, nextBtn);
|
|
|
|
|
|
newNextBtn.addEventListener('click', (e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
console.log('Next DJ button clicked, current slide:', beatportDJSliderState.currentSlide);
|
|
|
goToBeatportDJSlide(beatportDJSliderState.currentSlide + 1);
|
|
|
resetBeatportDJSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up indicator functionality (copied from charts slider)
|
|
|
*/
|
|
|
function setupBeatportDJSliderIndicators() {
|
|
|
const indicators = document.querySelectorAll('.beatport-dj-indicator');
|
|
|
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.addEventListener('click', () => {
|
|
|
goToBeatportDJSlide(index);
|
|
|
resetBeatportDJSliderAutoPlay();
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Navigate to a specific slide (copied from charts slider)
|
|
|
*/
|
|
|
function goToBeatportDJSlide(slideIndex) {
|
|
|
console.log('goToBeatportDJSlide called with:', slideIndex, 'current:', beatportDJSliderState.currentSlide);
|
|
|
|
|
|
// Wrap around if out of bounds
|
|
|
if (slideIndex < 0) {
|
|
|
slideIndex = beatportDJSliderState.totalSlides - 1;
|
|
|
} else if (slideIndex >= beatportDJSliderState.totalSlides) {
|
|
|
slideIndex = 0;
|
|
|
}
|
|
|
|
|
|
console.log('After wrapping, slideIndex:', slideIndex);
|
|
|
|
|
|
// Update current slide
|
|
|
beatportDJSliderState.currentSlide = slideIndex;
|
|
|
|
|
|
// Update slide visibility
|
|
|
const slides = document.querySelectorAll('.beatport-dj-slide');
|
|
|
slides.forEach((slide, index) => {
|
|
|
slide.classList.remove('active', 'prev', 'next');
|
|
|
|
|
|
if (index === slideIndex) {
|
|
|
slide.classList.add('active');
|
|
|
} else if (index < slideIndex) {
|
|
|
slide.classList.add('prev');
|
|
|
} else {
|
|
|
slide.classList.add('next');
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Update indicators
|
|
|
const indicators = document.querySelectorAll('.beatport-dj-indicator');
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.classList.toggle('active', index === slideIndex);
|
|
|
});
|
|
|
|
|
|
console.log('DJ slide updated to:', beatportDJSliderState.currentSlide);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Start auto-play functionality (copied from charts slider)
|
|
|
*/
|
|
|
function startBeatportDJSliderAutoPlay() {
|
|
|
if (beatportDJSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportDJSliderState.autoPlayInterval);
|
|
|
}
|
|
|
|
|
|
beatportDJSliderState.autoPlayInterval = setInterval(() => {
|
|
|
goToBeatportDJSlide(beatportDJSliderState.currentSlide + 1);
|
|
|
}, beatportDJSliderState.autoPlayDelay);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Reset auto-play timer (copied from charts slider)
|
|
|
*/
|
|
|
function resetBeatportDJSliderAutoPlay() {
|
|
|
startBeatportDJSliderAutoPlay();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Set up hover pause functionality (copied from charts slider)
|
|
|
*/
|
|
|
function setupBeatportDJSliderHoverPause() {
|
|
|
const sliderContainer = document.querySelector('.beatport-dj-slider-container');
|
|
|
|
|
|
if (sliderContainer) {
|
|
|
sliderContainer.addEventListener('mouseenter', () => {
|
|
|
if (beatportDJSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportDJSliderState.autoPlayInterval);
|
|
|
beatportDJSliderState.autoPlayInterval = null;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
sliderContainer.addEventListener('mouseleave', () => {
|
|
|
startBeatportDJSliderAutoPlay();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Clean up DJ slider when switching away (copied from charts slider)
|
|
|
*/
|
|
|
function cleanupBeatportDJSlider() {
|
|
|
if (beatportDJSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportDJSliderState.autoPlayInterval);
|
|
|
beatportDJSliderState.autoPlayInterval = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Load top 10 lists data from API and populate both lists
|
|
|
*/
|
|
|
async function loadBeatportTop10Lists() {
|
|
|
try {
|
|
|
console.log('🏆 Loading top 10 lists data...');
|
|
|
const signal = getBeatportContentSignal();
|
|
|
const response = await fetch('/api/beatport/homepage/top-10-lists', signal ? { signal } : undefined);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
console.log(`🎵 Loaded ${data.beatport_count} Beatport Top 10 + ${data.hype_count} Hype Top 10 tracks`);
|
|
|
|
|
|
// Populate both lists
|
|
|
populateBeatportTop10List(data.beatport_top10);
|
|
|
populateHypeTop10List(data.hype_top10);
|
|
|
return true;
|
|
|
} else {
|
|
|
console.error('Failed to load top 10 lists:', data.error);
|
|
|
showTop10ListsError(data.error || 'No data available');
|
|
|
return false;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
if (error && error.name === 'AbortError') return false;
|
|
|
console.error('Error loading top 10 lists:', error);
|
|
|
showTop10ListsError('Failed to load top 10 lists');
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Clean track/artist text for proper spacing
|
|
|
*/
|
|
|
function cleanTrackText(text) {
|
|
|
if (!text) return text;
|
|
|
|
|
|
// Fix common spacing issues
|
|
|
text = text.replace(/([a-z$!@#%&*])([A-Z])/g, '$1 $2'); // Add space between lowercase/symbols and uppercase
|
|
|
text = text.replace(/([a-zA-Z]),([a-zA-Z])/g, '$1, $2'); // Add space after comma
|
|
|
text = text.replace(/([a-zA-Z])(Mix|Remix|Extended|Version)\b/g, '$1 $2'); // Fix mix types
|
|
|
text = text.replace(/\s+/g, ' '); // Collapse multiple spaces
|
|
|
text = text.trim();
|
|
|
|
|
|
return text;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Populate Beatport Top 10 list with data
|
|
|
*/
|
|
|
function populateBeatportTop10List(tracks) {
|
|
|
const container = document.getElementById('beatport-top10-list');
|
|
|
if (!container || !tracks || tracks.length === 0) return;
|
|
|
|
|
|
// Generate HTML for the tracks
|
|
|
let tracksHtml = `
|
|
|
<div class="beatport-top10-list-header">
|
|
|
<h3 class="beatport-top10-list-title">🎵 Beatport Top 10</h3>
|
|
|
<p class="beatport-top10-list-subtitle">Most popular tracks on Beatport</p>
|
|
|
</div>
|
|
|
<div class="beatport-top10-tracks">
|
|
|
`;
|
|
|
|
|
|
tracks.forEach((track, index) => {
|
|
|
// Clean the text data before injection
|
|
|
const cleanTitle = cleanTrackText(track.title || 'Unknown Title');
|
|
|
const cleanArtist = cleanTrackText(track.artist || 'Unknown Artist');
|
|
|
const cleanLabel = cleanTrackText(track.label || 'Unknown Label');
|
|
|
|
|
|
tracksHtml += `
|
|
|
<div class="beatport-top10-card" data-url="${track.url || '#'}">
|
|
|
<div class="beatport-top10-card-rank">${track.rank || index + 1}</div>
|
|
|
<div class="beatport-top10-card-artwork">
|
|
|
${track.artwork_url ?
|
|
|
`<img src="${track.artwork_url}" alt="${cleanTitle}" loading="lazy">` :
|
|
|
'<div class="beatport-top10-card-placeholder">🎵</div>'
|
|
|
}
|
|
|
</div>
|
|
|
<div class="beatport-top10-card-info">
|
|
|
<h4 class="beatport-top10-card-title">${cleanTitle}</h4>
|
|
|
<p class="beatport-top10-card-artist">${cleanArtist}</p>
|
|
|
<p class="beatport-top10-card-label">${cleanLabel}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
});
|
|
|
|
|
|
tracksHtml += '</div>';
|
|
|
container.innerHTML = tracksHtml;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Populate Hype Top 10 list with data
|
|
|
*/
|
|
|
function populateHypeTop10List(tracks) {
|
|
|
const container = document.getElementById('beatport-hype10-list');
|
|
|
if (!container || !tracks || tracks.length === 0) return;
|
|
|
|
|
|
// Generate HTML for the tracks
|
|
|
let tracksHtml = `
|
|
|
<div class="beatport-hype10-list-header">
|
|
|
<h3 class="beatport-hype10-list-title">🔥 Hype Top 10</h3>
|
|
|
<p class="beatport-hype10-list-subtitle">Editor's trending picks</p>
|
|
|
</div>
|
|
|
<div class="beatport-hype10-tracks">
|
|
|
`;
|
|
|
|
|
|
tracks.forEach((track, index) => {
|
|
|
// Clean the text data before injection
|
|
|
const cleanTitle = cleanTrackText(track.title || 'Unknown Title');
|
|
|
const cleanArtist = cleanTrackText(track.artist || 'Unknown Artist');
|
|
|
const cleanLabel = cleanTrackText(track.label || 'Unknown Label');
|
|
|
|
|
|
tracksHtml += `
|
|
|
<div class="beatport-hype10-card" data-url="${track.url || '#'}">
|
|
|
<div class="beatport-hype10-card-rank">${track.rank || index + 1}</div>
|
|
|
<div class="beatport-hype10-card-artwork">
|
|
|
${track.artwork_url ?
|
|
|
`<img src="${track.artwork_url}" alt="${cleanTitle}" loading="lazy">` :
|
|
|
'<div class="beatport-hype10-card-placeholder">🔥</div>'
|
|
|
}
|
|
|
</div>
|
|
|
<div class="beatport-hype10-card-info">
|
|
|
<h4 class="beatport-hype10-card-title">${cleanTitle}</h4>
|
|
|
<p class="beatport-hype10-card-artist">${cleanArtist}</p>
|
|
|
<p class="beatport-hype10-card-label">${cleanLabel}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
});
|
|
|
|
|
|
tracksHtml += '</div>';
|
|
|
container.innerHTML = tracksHtml;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Show error message for top 10 lists
|
|
|
*/
|
|
|
function showTop10ListsError(errorMessage) {
|
|
|
const beatportContainer = document.getElementById('beatport-top10-list');
|
|
|
const hypeContainer = document.getElementById('beatport-hype10-list');
|
|
|
|
|
|
const errorHtml = `
|
|
|
<div class="beatport-top10-error">
|
|
|
<h3>❌ Error Loading Data</h3>
|
|
|
<p>${errorMessage}</p>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
if (beatportContainer) beatportContainer.innerHTML = errorHtml;
|
|
|
if (hypeContainer) hypeContainer.innerHTML = errorHtml;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Load top 10 releases data from API and populate the list
|
|
|
*/
|
|
|
async function loadBeatportTop10Releases() {
|
|
|
try {
|
|
|
console.log('💿 Loading top 10 releases data...');
|
|
|
const signal = getBeatportContentSignal();
|
|
|
const response = await fetch('/api/beatport/homepage/top-10-releases-cards', signal ? { signal } : undefined);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
console.log(`💿 Loaded ${data.releases_count} Top 10 Releases`);
|
|
|
populateBeatportTop10Releases(data.releases);
|
|
|
return true;
|
|
|
} else {
|
|
|
console.error('Failed to load top 10 releases:', data.error);
|
|
|
showTop10ReleasesError(data.error || 'No data available');
|
|
|
return false;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
if (error && error.name === 'AbortError') return false;
|
|
|
console.error('Error loading top 10 releases:', error);
|
|
|
showTop10ReleasesError('Failed to load top 10 releases');
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Populate Top 10 Releases list with data
|
|
|
*/
|
|
|
function populateBeatportTop10Releases(releases) {
|
|
|
const container = document.getElementById('beatport-releases-top10-list');
|
|
|
if (!container || !releases || releases.length === 0) return;
|
|
|
|
|
|
// Generate HTML for the releases
|
|
|
let releasesHtml = `
|
|
|
<div class="beatport-releases-top10-tracks">
|
|
|
`;
|
|
|
|
|
|
releases.forEach((release, index) => {
|
|
|
releasesHtml += `
|
|
|
<div class="beatport-releases-top10-card" data-url="${release.url || '#'}" data-bg-image="${release.image_url || ''}">
|
|
|
<div class="beatport-releases-top10-card-rank">${release.rank || index + 1}</div>
|
|
|
<div class="beatport-releases-top10-card-artwork">
|
|
|
${release.image_url ?
|
|
|
`<img src="${release.image_url}" alt="${release.title}" loading="lazy">` :
|
|
|
'<div class="beatport-releases-top10-card-placeholder">💿</div>'
|
|
|
}
|
|
|
</div>
|
|
|
<div class="beatport-releases-top10-card-info">
|
|
|
<h4 class="beatport-releases-top10-card-title">${release.title || 'Unknown Title'}</h4>
|
|
|
<p class="beatport-releases-top10-card-artist">${release.artist || 'Unknown Artist'}</p>
|
|
|
<p class="beatport-releases-top10-card-label">${release.label || 'Unknown Label'}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
});
|
|
|
|
|
|
releasesHtml += '</div>';
|
|
|
container.innerHTML = releasesHtml;
|
|
|
|
|
|
// Set background images for cards
|
|
|
const cards = container.querySelectorAll('.beatport-releases-top10-card[data-bg-image]');
|
|
|
cards.forEach(card => {
|
|
|
const bgImage = card.getAttribute('data-bg-image');
|
|
|
if (bgImage) {
|
|
|
// Transform image URL from 95x95 to 500x500 for higher quality background
|
|
|
const highResImage = bgImage.replace('/image_size/95x95/', '/image_size/500x500/');
|
|
|
card.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.8)), url('${highResImage}')`;
|
|
|
card.style.backgroundSize = 'cover';
|
|
|
card.style.backgroundPosition = 'center';
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Add click handlers for individual release discovery
|
|
|
const releaseCards = container.querySelectorAll('.beatport-releases-top10-card[data-url]');
|
|
|
releaseCards.forEach((card, index) => {
|
|
|
card.addEventListener('click', () => handleBeatportReleaseCardClick(card, releases[index]));
|
|
|
card.style.cursor = 'pointer';
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Show error message for top 10 releases
|
|
|
*/
|
|
|
function showTop10ReleasesError(errorMessage) {
|
|
|
const container = document.getElementById('beatport-releases-top10-list');
|
|
|
|
|
|
const errorHtml = `
|
|
|
<div class="beatport-releases-top10-error">
|
|
|
<h3>❌ Error Loading Releases</h3>
|
|
|
<p>${errorMessage}</p>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
if (container) container.innerHTML = errorHtml;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle click on individual Top 10 Release card - create discovery process for single release
|
|
|
*/
|
|
|
async function handleBeatportReleaseCardClick(cardElement, release) {
|
|
|
if (_beatportModalOpening) return;
|
|
|
_beatportModalOpening = true;
|
|
|
|
|
|
console.log(`💿 Individual release card clicked: ${release.title} by ${release.artist}`);
|
|
|
|
|
|
if (!release.url || release.url === '#') {
|
|
|
_beatportModalOpening = false;
|
|
|
showToast('No release URL available', 'error');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
showToast(`Loading ${release.title}...`, 'info');
|
|
|
showLoadingOverlay(`Getting tracks from ${release.title}...`);
|
|
|
|
|
|
// Fetch structured release metadata for direct download modal
|
|
|
console.log(`🎵 Fetching release metadata: ${release.url}`);
|
|
|
const response = await fetch('/api/beatport/release-metadata', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ release_url: release.url })
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
|
|
throw new Error(data.error || 'No tracks found in this release');
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Got ${data.tracks.length} tracks from ${data.album.name}`);
|
|
|
|
|
|
// Format artists as array of strings for compatibility with download modal
|
|
|
const formattedTracks = data.tracks.map(track => ({
|
|
|
...track,
|
|
|
artists: track.artists.map(a => typeof a === 'object' ? a.name : a)
|
|
|
}));
|
|
|
|
|
|
const virtualPlaylistId = `beatport_release_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
const playlistName = data.album.name;
|
|
|
|
|
|
// Open download modal directly - same as clicking an album on the Artists page
|
|
|
await openDownloadMissingModalForArtistAlbum(
|
|
|
virtualPlaylistId,
|
|
|
playlistName,
|
|
|
formattedTracks,
|
|
|
data.album,
|
|
|
data.artist,
|
|
|
false
|
|
|
);
|
|
|
|
|
|
// Register Beatport download bubble for releases (albums, EPs, singles)
|
|
|
const releaseImage = (data.album.images && data.album.images.length > 0) ? data.album.images[0].url : (release.image_url || '');
|
|
|
registerBeatportDownload(playlistName, releaseImage, virtualPlaylistId);
|
|
|
|
|
|
hideLoadingOverlay();
|
|
|
_beatportModalOpening = false;
|
|
|
console.log(`✅ Opened download modal for ${playlistName}`);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error(`❌ Error handling release click for ${release.title}:`, error);
|
|
|
hideLoadingOverlay();
|
|
|
_beatportModalOpening = false;
|
|
|
showToast(`Error loading ${release.title}: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Convert scraped Beatport tracks into download-modal-compatible format and open the modal.
|
|
|
* Used by all chart/playlist handlers (Top 100, Hype 100, Featured Charts, DJ Charts, genre charts).
|
|
|
* Charts open as compilations — each track is searched independently on Soulseek.
|
|
|
*/
|
|
|
// Guard against multiple rapid clicks opening duplicate modals
|
|
|
let _beatportModalOpening = false;
|
|
|
|
|
|
/**
|
|
|
* Enrich tracks via a single batch request to the backend.
|
|
|
* Progress is reported via WebSocket (beatport:enrich_progress) and updates the loading overlay.
|
|
|
* Returns the enriched tracks array.
|
|
|
*/
|
|
|
async function _enrichTracksWithProgress(tracks, chartName) {
|
|
|
const enrichmentId = `enrich_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
|
|
|
|
|
try {
|
|
|
const resp = await fetch('/api/beatport/enrich-tracks', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ tracks, enrichment_id: enrichmentId })
|
|
|
});
|
|
|
const data = await resp.json();
|
|
|
|
|
|
// Synchronous path — all tracks were cached, results returned inline
|
|
|
if (data.success && data.tracks) {
|
|
|
return data.tracks;
|
|
|
}
|
|
|
|
|
|
// Async path — poll for progress until done
|
|
|
if (data.success && data.async) {
|
|
|
while (true) {
|
|
|
await new Promise(r => setTimeout(r, 800));
|
|
|
try {
|
|
|
const progressResp = await fetch(`/api/beatport/enrich-progress/${enrichmentId}?_=${Date.now()}`);
|
|
|
const progress = await progressResp.json();
|
|
|
if (!progress.success) break;
|
|
|
|
|
|
// Update loading overlay with live progress
|
|
|
const overlayText = document.querySelector('#loading-overlay .loading-message');
|
|
|
if (overlayText) {
|
|
|
overlayText.textContent = `Fetching track metadata... (${progress.completed}/${progress.total}) ${progress.current_track || ''}`;
|
|
|
}
|
|
|
|
|
|
if (progress.done) {
|
|
|
if (progress.tracks) {
|
|
|
return progress.tracks;
|
|
|
}
|
|
|
console.warn('⚠️ Async enrichment failed:', progress.error);
|
|
|
return tracks;
|
|
|
}
|
|
|
} catch (pollErr) {
|
|
|
console.warn('⚠️ Progress poll error:', pollErr);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
console.warn('⚠️ Enrichment failed, returning original tracks');
|
|
|
return tracks;
|
|
|
} catch (e) {
|
|
|
console.warn('⚠️ Failed to enrich tracks:', e);
|
|
|
return tracks;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function parseBeatportDuration(raw) {
|
|
|
if (!raw) return 0;
|
|
|
if (typeof raw === 'string' && raw.includes(':')) {
|
|
|
const parts = raw.split(':');
|
|
|
return (parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10)) * 1000 || 0;
|
|
|
}
|
|
|
return (parseInt(raw, 10) || 0) * 1000;
|
|
|
}
|
|
|
|
|
|
function openBeatportChartAsDownloadModal(tracks, chartName, chartImage) {
|
|
|
// Note: callers already guard against double-clicks via _beatportModalOpening.
|
|
|
// Reset the flag here so the modal can open even after fast (cached) enrichment.
|
|
|
_beatportModalOpening = false;
|
|
|
|
|
|
const albumObj = {
|
|
|
id: `beatport_chart_${Date.now()}`,
|
|
|
name: chartName,
|
|
|
album_type: 'compilation',
|
|
|
images: chartImage ? [{ url: chartImage }] : [],
|
|
|
total_tracks: tracks.length
|
|
|
};
|
|
|
|
|
|
const formattedTracks = tracks.map((track, index) => {
|
|
|
// Use per-track release metadata if available (from JSON extraction)
|
|
|
const hasRelease = track.release_name && track.release_name.length > 0;
|
|
|
const trackAlbum = hasRelease ? {
|
|
|
id: `beatport_release_${track.release_id || index}`,
|
|
|
name: cleanTrackText(track.release_name),
|
|
|
album_type: 'single',
|
|
|
images: track.release_image ? [{ url: track.release_image }] : [],
|
|
|
release_date: track.release_date || '',
|
|
|
total_tracks: 1
|
|
|
} : albumObj;
|
|
|
|
|
|
// Combine title + mix_name
|
|
|
let trackName = cleanTrackText(track.title || 'Unknown Title');
|
|
|
if (track.mix_name && track.mix_name.toLowerCase() !== 'original mix') {
|
|
|
trackName = `${trackName} (${cleanTrackText(track.mix_name)})`;
|
|
|
}
|
|
|
|
|
|
// Split combined artist string into individual names for proper folder structure
|
|
|
const rawArtist = cleanTrackText(track.artist || 'Unknown Artist');
|
|
|
const artistList = rawArtist.includes(',')
|
|
|
? rawArtist.split(',').map(a => a.trim()).filter(a => a)
|
|
|
: [rawArtist];
|
|
|
|
|
|
return {
|
|
|
id: `beatport_chart_${index}`,
|
|
|
name: trackName,
|
|
|
artists: artistList,
|
|
|
duration_ms: parseBeatportDuration(track.duration),
|
|
|
track_number: index + 1,
|
|
|
disc_number: 1,
|
|
|
album: trackAlbum
|
|
|
};
|
|
|
});
|
|
|
|
|
|
const virtualPlaylistId = `beatport_chart_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
|
// Compilation artist
|
|
|
const artistObj = { id: 'beatport_various', name: 'Various Artists' };
|
|
|
|
|
|
openDownloadMissingModalForArtistAlbum(
|
|
|
virtualPlaylistId,
|
|
|
chartName,
|
|
|
formattedTracks,
|
|
|
albumObj,
|
|
|
artistObj,
|
|
|
false,
|
|
|
'playlist'
|
|
|
);
|
|
|
|
|
|
// Register Beatport download bubble
|
|
|
registerBeatportDownload(chartName, chartImage, virtualPlaylistId);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle click on individual chart card - open download modal directly
|
|
|
*/
|
|
|
async function handleBeatportChartCardClick(cardElement, chart) {
|
|
|
console.log(`📊 Individual chart card clicked: ${chart.name} by ${chart.creator}`);
|
|
|
|
|
|
if (!chart.url || chart.url === '') {
|
|
|
showToast('No chart URL available', 'error');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const chartName = `${chart.name} - ${chart.creator}`;
|
|
|
showToast(`Loading ${chart.name}...`, 'info');
|
|
|
showLoadingOverlay(`Scraping ${chart.name}...`);
|
|
|
|
|
|
const response = await fetch('/api/beatport/chart/extract', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
|
chart_url: chart.url,
|
|
|
chart_name: `Featured Chart: ${chart.name}`,
|
|
|
limit: 100,
|
|
|
enrich: false
|
|
|
})
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
|
|
throw new Error('No tracks found in this chart');
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Fetched ${data.tracks.length} raw tracks from ${chart.name}, enriching...`);
|
|
|
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, chartName);
|
|
|
|
|
|
hideLoadingOverlay();
|
|
|
openBeatportChartAsDownloadModal(enrichedTracks, chartName, chart.image);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error(`❌ Error handling chart click for ${chart.name}:`, error);
|
|
|
hideLoadingOverlay();
|
|
|
showToast(`Error loading ${chart.name}: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle click on individual DJ chart card - open download modal directly
|
|
|
*/
|
|
|
async function handleBeatportDJChartCardClick(cardElement, chart) {
|
|
|
console.log(`🎧 Individual DJ chart card clicked: ${chart.name} by ${chart.creator}`);
|
|
|
|
|
|
if (!chart.url || chart.url === '') {
|
|
|
showToast('No DJ chart URL available', 'error');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const chartName = `${chart.name} - ${chart.creator}`;
|
|
|
showToast(`Loading ${chart.name}...`, 'info');
|
|
|
showLoadingOverlay(`Scraping ${chart.name}...`);
|
|
|
|
|
|
const response = await fetch('/api/beatport/chart/extract', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
|
chart_url: chart.url,
|
|
|
chart_name: `DJ Chart: ${chart.name}`,
|
|
|
limit: 100,
|
|
|
enrich: false
|
|
|
})
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
|
|
throw new Error('No tracks found in this DJ chart');
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Fetched ${data.tracks.length} raw tracks from ${chart.name}, enriching...`);
|
|
|
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, chartName);
|
|
|
|
|
|
hideLoadingOverlay();
|
|
|
openBeatportChartAsDownloadModal(enrichedTracks, chartName, chart.image);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error(`❌ Error handling DJ chart click for ${chart.name}:`, error);
|
|
|
hideLoadingOverlay();
|
|
|
showToast(`Error loading ${chart.name}: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle click on Beatport Top 100 button - open download modal directly
|
|
|
*/
|
|
|
async function handleBeatportTop100Click() {
|
|
|
if (_beatportModalOpening) return;
|
|
|
_beatportModalOpening = true;
|
|
|
setTimeout(() => { _beatportModalOpening = false; }, 2000);
|
|
|
|
|
|
console.log('💯 Beatport Top 100 button clicked');
|
|
|
|
|
|
try {
|
|
|
showLoadingOverlay('Scraping Beatport Top 100...');
|
|
|
|
|
|
// Fetch track list without enrichment (fast)
|
|
|
const response = await fetch('/api/beatport/top-100?enrich=false', { method: 'GET' });
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
|
|
throw new Error('No tracks found in Beatport Top 100');
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Fetched ${data.tracks.length} tracks, enriching one-by-one...`);
|
|
|
|
|
|
// Enrich one-by-one with live progress
|
|
|
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, 'Beatport Top 100');
|
|
|
|
|
|
hideLoadingOverlay();
|
|
|
openBeatportChartAsDownloadModal(enrichedTracks, 'Beatport Top 100', null);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('❌ Error handling Beatport Top 100 click:', error);
|
|
|
hideLoadingOverlay();
|
|
|
showToast(`Error loading Beatport Top 100: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle click on Hype Top 100 button - open download modal directly
|
|
|
*/
|
|
|
async function handleHypeTop100Click() {
|
|
|
if (_beatportModalOpening) return;
|
|
|
_beatportModalOpening = true;
|
|
|
setTimeout(() => { _beatportModalOpening = false; }, 2000);
|
|
|
|
|
|
console.log('🔥 Hype Top 100 button clicked');
|
|
|
|
|
|
try {
|
|
|
showLoadingOverlay('Scraping Hype Top 100...');
|
|
|
|
|
|
// Fetch track list without enrichment (fast)
|
|
|
const response = await fetch('/api/beatport/hype-top-100?enrich=false', { method: 'GET' });
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
|
|
throw new Error('No tracks found in Hype Top 100');
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Fetched ${data.tracks.length} tracks, enriching one-by-one...`);
|
|
|
|
|
|
// Enrich one-by-one with live progress
|
|
|
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, 'Hype Top 100');
|
|
|
|
|
|
hideLoadingOverlay();
|
|
|
openBeatportChartAsDownloadModal(enrichedTracks, 'Hype Top 100', null);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('❌ Error handling Hype Top 100 click:', error);
|
|
|
hideLoadingOverlay();
|
|
|
showToast(`Error loading Hype Top 100: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ================================= //
|
|
|
// GENRE BROWSER MODAL FUNCTIONS //
|
|
|
// ================================= //
|
|
|
|
|
|
// Cache for genre browser data to avoid re-loading
|
|
|
let genreBrowserCache = {
|
|
|
genres: null,
|
|
|
imagesLoaded: false,
|
|
|
lastLoaded: null,
|
|
|
imageLoadingActive: false,
|
|
|
imageWorkers: null
|
|
|
};
|
|
|
|
|
|
function initializeGenreBrowserModal() {
|
|
|
console.log('🎵 Initializing Genre Browser Modal...');
|
|
|
|
|
|
// Browse by Genre button click handler
|
|
|
const browseByGenreBtn = document.getElementById('browse-by-genre-btn');
|
|
|
if (browseByGenreBtn) {
|
|
|
browseByGenreBtn.addEventListener('click', () => {
|
|
|
console.log('🎵 Browse by Genre button clicked');
|
|
|
openGenreBrowserModal();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Modal close button handler
|
|
|
const modalCloseBtn = document.getElementById('genre-browser-modal-close');
|
|
|
if (modalCloseBtn) {
|
|
|
modalCloseBtn.addEventListener('click', closeGenreBrowserModal);
|
|
|
}
|
|
|
|
|
|
// Click outside modal to close
|
|
|
const modalOverlay = document.getElementById('genre-browser-modal');
|
|
|
if (modalOverlay) {
|
|
|
modalOverlay.addEventListener('click', (e) => {
|
|
|
if (e.target === modalOverlay) {
|
|
|
closeGenreBrowserModal();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Search functionality
|
|
|
const searchInput = document.getElementById('genre-browser-search');
|
|
|
if (searchInput) {
|
|
|
searchInput.addEventListener('input', (e) => {
|
|
|
filterGenreBrowserCards(e.target.value);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// ESC key to close modal
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
if (e.key === 'Escape' && isGenreBrowserModalOpen()) {
|
|
|
closeGenreBrowserModal();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
console.log('✅ Genre Browser Modal initialized');
|
|
|
}
|
|
|
|
|
|
function openGenreBrowserModal() {
|
|
|
console.log('🎵 Opening Genre Browser Modal...');
|
|
|
|
|
|
const modal = document.getElementById('genre-browser-modal');
|
|
|
if (modal) {
|
|
|
modal.classList.add('active');
|
|
|
document.body.style.overflow = 'hidden'; // Prevent background scrolling
|
|
|
|
|
|
// Check cache before loading genres
|
|
|
if (genreBrowserCache.genres && genreBrowserCache.genres.length > 0) {
|
|
|
console.log('💾 Using cached genres data');
|
|
|
displayCachedGenres();
|
|
|
} else {
|
|
|
console.log('🔄 No cached data, loading genres...');
|
|
|
loadGenreBrowserGenres();
|
|
|
}
|
|
|
|
|
|
console.log('✅ Genre Browser Modal opened');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function closeGenreBrowserModal() {
|
|
|
console.log('🎵 Closing Genre Browser Modal...');
|
|
|
|
|
|
const modal = document.getElementById('genre-browser-modal');
|
|
|
if (modal) {
|
|
|
modal.classList.remove('active');
|
|
|
document.body.style.overflow = ''; // Restore scrolling
|
|
|
|
|
|
// Clear search input but keep the genre data cached
|
|
|
const searchInput = document.getElementById('genre-browser-search');
|
|
|
if (searchInput) {
|
|
|
searchInput.value = '';
|
|
|
// Also reset the display filter to show all genres
|
|
|
filterGenreBrowserCards('');
|
|
|
}
|
|
|
|
|
|
// Pause image loading workers if they're running
|
|
|
if (genreBrowserCache.imageLoadingActive) {
|
|
|
console.log('⏸️ Pausing image loading workers...');
|
|
|
genreBrowserCache.imageLoadingActive = false;
|
|
|
}
|
|
|
|
|
|
console.log('✅ Genre Browser Modal closed (data preserved in cache)');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function isGenreBrowserModalOpen() {
|
|
|
const modal = document.getElementById('genre-browser-modal');
|
|
|
return modal && modal.classList.contains('active');
|
|
|
}
|
|
|
|
|
|
async function loadGenreBrowserGenres() {
|
|
|
console.log('🔍 Loading genres for Genre Browser Modal...');
|
|
|
|
|
|
const genresGrid = document.getElementById('genre-browser-genres-grid');
|
|
|
if (!genresGrid) {
|
|
|
console.error('❌ Genre browser grid not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Show loading state
|
|
|
genresGrid.innerHTML = `
|
|
|
<div class="genre-browser-loading-container">
|
|
|
<div class="genre-browser-loading-spinner"></div>
|
|
|
<p class="genre-browser-loading-text">🔍 Discovering current Beatport genres...</p>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
try {
|
|
|
// First, fetch genres quickly without images
|
|
|
console.log('🚀 Fetching genres without images for fast loading...');
|
|
|
const fastResponse = await fetch('/api/beatport/genres');
|
|
|
if (!fastResponse.ok) {
|
|
|
throw new Error(`API returned ${fastResponse.status}: ${fastResponse.statusText}`);
|
|
|
}
|
|
|
|
|
|
const fastData = await fastResponse.json();
|
|
|
const genres = fastData.genres || [];
|
|
|
|
|
|
if (genres.length === 0) {
|
|
|
genresGrid.innerHTML = `
|
|
|
<div class="genre-browser-loading-container">
|
|
|
<p style="color: rgba(255, 255, 255, 0.7);">⚠️ No genres available</p>
|
|
|
<button onclick="loadGenreBrowserGenres()" style="margin-top: 10px; padding: 10px 20px; border: 1px solid rgba(255, 255, 255, 0.3); background: rgba(20, 20, 20, 0.8); color: white; border-radius: 8px; cursor: pointer;">🔄 Retry</button>
|
|
|
</div>
|
|
|
`;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Filter out unwanted genres (section titles, etc.)
|
|
|
const filteredGenres = genres.filter(genre => {
|
|
|
const name = genre.name.toLowerCase().trim();
|
|
|
const unwantedGenres = [
|
|
|
'open format',
|
|
|
'electronic',
|
|
|
'genres',
|
|
|
'browse',
|
|
|
'charts',
|
|
|
'new releases',
|
|
|
'trending',
|
|
|
'featured',
|
|
|
'popular'
|
|
|
];
|
|
|
|
|
|
const isUnwanted = unwantedGenres.includes(name);
|
|
|
if (isUnwanted) {
|
|
|
console.log(`🚫 Filtered out unwanted genre: "${genre.name}"`);
|
|
|
}
|
|
|
return !isUnwanted;
|
|
|
});
|
|
|
|
|
|
console.log(`📋 Filtered genres: ${genres.length} → ${filteredGenres.length} (removed ${genres.length - filteredGenres.length} unwanted)`);
|
|
|
|
|
|
// Generate genre cards dynamically (without images first)
|
|
|
const genreCardsHTML = filteredGenres.map(genre => `
|
|
|
<div class="genre-browser-card genre-browser-card-fallback"
|
|
|
data-genre-slug="${genre.slug}"
|
|
|
data-genre-id="${genre.id}"
|
|
|
data-genre-name="${genre.name}"
|
|
|
data-url="${genre.url}">
|
|
|
<div class="genre-browser-card-image">🎵</div>
|
|
|
<div class="genre-browser-card-content">
|
|
|
<h3 class="genre-browser-card-title">${genre.name}</h3>
|
|
|
<p class="genre-browser-card-subtitle">Top 10 & Top 100 Charts</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`).join('');
|
|
|
|
|
|
genresGrid.innerHTML = genreCardsHTML;
|
|
|
|
|
|
// Add click event listeners to genre cards
|
|
|
addGenreBrowserCardClickListeners();
|
|
|
|
|
|
// Cache the filtered genres data
|
|
|
genreBrowserCache.genres = filteredGenres;
|
|
|
genreBrowserCache.lastLoaded = new Date();
|
|
|
genreBrowserCache.imagesLoaded = false;
|
|
|
|
|
|
console.log(`✅ Loaded ${filteredGenres.length} Beatport genres for modal (fast mode)`);
|
|
|
console.log(`💾 Cached ${filteredGenres.length} genres for future use`);
|
|
|
showToast(`Loaded ${filteredGenres.length} genres for browsing`, 'success');
|
|
|
|
|
|
// Now fetch images progressively in the background
|
|
|
if (filteredGenres.length > 5) {
|
|
|
console.log('🖼️ Loading genre images progressively for modal...');
|
|
|
loadGenreBrowserImagesProgressively(filteredGenres);
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('❌ Error loading genres for modal:', error);
|
|
|
genresGrid.innerHTML = `
|
|
|
<div class="genre-browser-loading-container">
|
|
|
<p style="color: rgba(255, 255, 255, 0.7);">❌ Failed to load genres: ${error.message}</p>
|
|
|
<button onclick="loadGenreBrowserGenres()" style="margin-top: 10px; padding: 10px 20px; border: 1px solid rgba(255, 255, 255, 0.3); background: rgba(20, 20, 20, 0.8); color: white; border-radius: 8px; cursor: pointer;">🔄 Retry</button>
|
|
|
</div>
|
|
|
`;
|
|
|
showToast(`Error loading genres: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function displayCachedGenres() {
|
|
|
console.log('💾 Displaying cached genres...');
|
|
|
|
|
|
const genresGrid = document.getElementById('genre-browser-genres-grid');
|
|
|
if (!genresGrid) {
|
|
|
console.error('❌ Genre browser grid not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const genres = genreBrowserCache.genres;
|
|
|
if (!genres || genres.length === 0) {
|
|
|
console.error('❌ No cached genres available');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Generate genre cards from cached data
|
|
|
const genreCardsHTML = genres.map(genre => `
|
|
|
<div class="genre-browser-card genre-browser-card-fallback"
|
|
|
data-genre-slug="${genre.slug}"
|
|
|
data-genre-id="${genre.id}"
|
|
|
data-genre-name="${genre.name}"
|
|
|
data-url="${genre.url}">
|
|
|
<div class="genre-browser-card-image">🎵</div>
|
|
|
<div class="genre-browser-card-content">
|
|
|
<h3 class="genre-browser-card-title">${genre.name}</h3>
|
|
|
<p class="genre-browser-card-subtitle">Top 10 & Top 100 Charts</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`).join('');
|
|
|
|
|
|
genresGrid.innerHTML = genreCardsHTML;
|
|
|
|
|
|
// Add click event listeners to genre cards
|
|
|
addGenreBrowserCardClickListeners();
|
|
|
|
|
|
console.log(`✅ Displayed ${genres.length} cached genres instantly`);
|
|
|
|
|
|
// Handle image loading based on current state
|
|
|
if (genreBrowserCache.imagesLoaded) {
|
|
|
console.log('🖼️ Images already loaded, restoring them...');
|
|
|
restoreCachedImages(genres);
|
|
|
} else if (!genreBrowserCache.imageLoadingActive && genres.length > 5) {
|
|
|
// Resume or start image loading
|
|
|
const cachedCount = genres.filter(g => g.imageUrl).length;
|
|
|
if (cachedCount > 0) {
|
|
|
console.log(`🔄 Resuming image loading (${cachedCount}/${genres.length} already cached)...`);
|
|
|
restoreCachedImages(genres); // Show already cached images
|
|
|
} else {
|
|
|
console.log('🖼️ Starting fresh image loading for cached genres...');
|
|
|
}
|
|
|
loadGenreBrowserImagesProgressively(genres);
|
|
|
} else {
|
|
|
console.log('📷 Image loading in progress, showing cached images...');
|
|
|
restoreCachedImages(genres);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function restoreCachedImages(genres) {
|
|
|
// Restore images that were already loaded in previous sessions
|
|
|
genres.forEach(genre => {
|
|
|
if (genre.imageUrl) {
|
|
|
const genreCard = document.querySelector(
|
|
|
`.genre-browser-card[data-genre-slug="${genre.slug}"][data-genre-id="${genre.id}"]`
|
|
|
);
|
|
|
|
|
|
if (genreCard) {
|
|
|
const imageElement = genreCard.querySelector('.genre-browser-card-image');
|
|
|
if (imageElement) {
|
|
|
imageElement.innerHTML = `<img src="${genre.imageUrl}" alt="${genre.name}" loading="lazy" style="width: 100%; height: 100%; object-fit: cover;">`;
|
|
|
genreCard.classList.remove('genre-browser-card-fallback');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
async function loadGenreBrowserImagesProgressively(genres) {
|
|
|
// Load genre images with 2 concurrent workers for faster loading
|
|
|
// Only process genres that don't already have cached images
|
|
|
const imageQueue = genres.filter(genre => !genre.imageUrl);
|
|
|
let imagesLoaded = 0;
|
|
|
const maxWorkers = 2;
|
|
|
|
|
|
// Mark loading as active
|
|
|
genreBrowserCache.imageLoadingActive = true;
|
|
|
|
|
|
console.log(`🖼️ Starting progressive image loading for modal with ${maxWorkers} workers for ${imageQueue.length} remaining genres (${genres.length - imageQueue.length} already cached)`);
|
|
|
|
|
|
// If all images are already cached, mark as complete
|
|
|
if (imageQueue.length === 0) {
|
|
|
console.log('✅ All images already cached, marking as complete');
|
|
|
genreBrowserCache.imagesLoaded = true;
|
|
|
genreBrowserCache.imageLoadingActive = false;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Function to process a single image
|
|
|
async function processImage(genre) {
|
|
|
try {
|
|
|
// Fetch individual genre image from backend
|
|
|
const response = await fetch(`/api/beatport/genre-image/${genre.slug}/${genre.id}`);
|
|
|
|
|
|
if (response.ok) {
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.image_url) {
|
|
|
// Cache the image URL in the genre object
|
|
|
genre.imageUrl = data.image_url;
|
|
|
|
|
|
// Find the genre card in the modal
|
|
|
const genreCard = document.querySelector(
|
|
|
`.genre-browser-card[data-genre-slug="${genre.slug}"][data-genre-id="${genre.id}"]`
|
|
|
);
|
|
|
|
|
|
if (genreCard) {
|
|
|
const imageElement = genreCard.querySelector('.genre-browser-card-image');
|
|
|
if (imageElement) {
|
|
|
// Replace the fallback emoji with the actual image
|
|
|
imageElement.innerHTML = `<img src="${data.image_url}" alt="${genre.name}" loading="lazy" style="width: 100%; height: 100%; object-fit: cover;">`;
|
|
|
genreCard.classList.remove('genre-browser-card-fallback');
|
|
|
|
|
|
console.log(`✅ Loaded and cached image for ${genre.name} in modal`);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
imagesLoaded++;
|
|
|
console.log(`📷 Progress: ${imagesLoaded}/${genres.length} images loaded for modal`);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.log(`⚠️ Could not load image for ${genre.name} in modal: ${error.message}`);
|
|
|
imagesLoaded++;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Worker function to process images from the queue
|
|
|
async function worker() {
|
|
|
while (imageQueue.length > 0 && genreBrowserCache.imageLoadingActive) {
|
|
|
const genre = imageQueue.shift();
|
|
|
if (genre) {
|
|
|
await processImage(genre);
|
|
|
// Small delay to prevent overwhelming the server
|
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
}
|
|
|
|
|
|
// Check if we should pause
|
|
|
if (!genreBrowserCache.imageLoadingActive) {
|
|
|
console.log('⏸️ Worker paused - modal closed');
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Start the workers
|
|
|
const workers = [];
|
|
|
for (let i = 0; i < maxWorkers; i++) {
|
|
|
workers.push(worker());
|
|
|
}
|
|
|
|
|
|
// Wait for all workers to complete
|
|
|
await Promise.all(workers);
|
|
|
|
|
|
// Check if loading was completed or paused
|
|
|
if (genreBrowserCache.imageLoadingActive) {
|
|
|
// Completed successfully
|
|
|
genreBrowserCache.imagesLoaded = true;
|
|
|
genreBrowserCache.imageLoadingActive = false;
|
|
|
console.log(`🎉 Completed loading all genre images for modal (${imagesLoaded}/${genres.length})`);
|
|
|
console.log(`💾 Marked images as loaded in cache`);
|
|
|
} else {
|
|
|
// Was paused
|
|
|
console.log(`⏸️ Image loading paused (${imagesLoaded}/${genres.length} completed)`);
|
|
|
console.log(`💾 Partial progress saved in cache`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function filterGenreBrowserCards(searchTerm) {
|
|
|
const genreCards = document.querySelectorAll('.genre-browser-card');
|
|
|
const searchLower = searchTerm.toLowerCase();
|
|
|
|
|
|
genreCards.forEach(card => {
|
|
|
const genreName = card.dataset.genreName?.toLowerCase() || '';
|
|
|
const shouldShow = genreName.includes(searchLower);
|
|
|
|
|
|
card.style.display = shouldShow ? 'block' : 'none';
|
|
|
});
|
|
|
|
|
|
console.log(`🔍 Filtered genre cards with search term: "${searchTerm}"`);
|
|
|
}
|
|
|
|
|
|
// === GENRE BROWSER CARD CLICK HANDLERS ===
|
|
|
|
|
|
function addGenreBrowserCardClickListeners() {
|
|
|
const genreCards = document.querySelectorAll('.genre-browser-card');
|
|
|
genreCards.forEach(card => {
|
|
|
card.addEventListener('click', () => {
|
|
|
const genreSlug = card.dataset.genreSlug;
|
|
|
const genreId = card.dataset.genreId;
|
|
|
const genreName = card.dataset.genreName;
|
|
|
|
|
|
console.log(`🎵 Genre card clicked: ${genreName} (${genreSlug})`);
|
|
|
handleGenreBrowserCardClick(genreSlug, genreId, genreName);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
console.log(`🔗 Added click listeners to ${genreCards.length} genre browser cards`);
|
|
|
}
|
|
|
|
|
|
async function handleGenreBrowserCardClick(genreSlug, genreId, genreName) {
|
|
|
console.log(`🎠 Loading hero slider for ${genreName}...`);
|
|
|
|
|
|
try {
|
|
|
// Show the genre page view
|
|
|
showGenrePageView(genreSlug, genreId, genreName);
|
|
|
|
|
|
// Load the hero slider data
|
|
|
// Load hero slider, Top 10 lists, and Top 10 releases in parallel
|
|
|
await Promise.all([
|
|
|
loadGenreHeroSlider(genreSlug, genreId, genreName),
|
|
|
loadGenreTop10Lists(genreSlug, genreId, genreName),
|
|
|
loadGenreTop10Releases(genreSlug, genreId, genreName)
|
|
|
]);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error(`❌ Error loading genre page for ${genreName}:`, error);
|
|
|
showToast(`Error loading ${genreName}: ${error.message}`, 'error');
|
|
|
|
|
|
// Return to genre list on error
|
|
|
showGenreListView();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function showGenrePageView(genreSlug, genreId, genreName) {
|
|
|
console.log(`🎯 Showing genre page view for ${genreName}`);
|
|
|
|
|
|
// CRITICAL: Stop all other slider auto-play to prevent conflicts
|
|
|
if (typeof beatportRebuildSliderState !== 'undefined' && beatportRebuildSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportRebuildSliderState.autoPlayInterval);
|
|
|
console.log('🛑 Stopped main slider auto-play to prevent conflicts');
|
|
|
}
|
|
|
|
|
|
const modal = document.getElementById('genre-browser-modal');
|
|
|
if (!modal) return;
|
|
|
|
|
|
// Hide genre list elements
|
|
|
const searchSection = modal.querySelector('.genre-browser-search-section');
|
|
|
const genresSection = modal.querySelector('.genre-browser-genres-section');
|
|
|
|
|
|
if (searchSection) searchSection.style.display = 'none';
|
|
|
if (genresSection) genresSection.style.display = 'none';
|
|
|
|
|
|
// Create or show genre page content
|
|
|
let genrePageContent = modal.querySelector('.genre-page-content');
|
|
|
if (!genrePageContent) {
|
|
|
genrePageContent = document.createElement('div');
|
|
|
genrePageContent.className = 'genre-page-content';
|
|
|
genrePageContent.innerHTML = `
|
|
|
<div class="genre-page-header">
|
|
|
<button class="genre-back-button" id="genre-back-button">
|
|
|
<span class="back-icon">←</span> Back to Genres
|
|
|
</button>
|
|
|
<h2 class="genre-page-title"></h2>
|
|
|
</div>
|
|
|
<div class="genre-hero-slider-container" id="genre-hero-slider-container">
|
|
|
<div class="genre-loading-container">
|
|
|
<div class="genre-loading-spinner"></div>
|
|
|
<p class="genre-loading-text">🎠 Loading hero releases...</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="genre-nav-buttons-section">
|
|
|
<div class="genre-nav-buttons-container">
|
|
|
<button class="beatport-nav-button" id="genre-top100-btn">
|
|
|
<span class="beatport-nav-icon top100-icon"></span>
|
|
|
<span class="beatport-nav-text">Beatport Top 100</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="genre-top10-lists-container" id="genre-top10-lists-container">
|
|
|
<div class="genre-top10-loading-container">
|
|
|
<div class="genre-loading-spinner"></div>
|
|
|
<p class="genre-loading-text">🎵 Loading Top 10 lists...</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="genre-top10-releases-container" id="genre-top10-releases-container">
|
|
|
<div class="genre-top10-releases-loading-container">
|
|
|
<div class="genre-loading-spinner"></div>
|
|
|
<p class="genre-loading-text">💿 Loading Top 10 releases...</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
modal.querySelector('.genre-browser-modal-content').appendChild(genrePageContent);
|
|
|
|
|
|
// Add back button listener
|
|
|
const backButton = genrePageContent.querySelector('#genre-back-button');
|
|
|
if (backButton) {
|
|
|
backButton.addEventListener('click', showGenreListView);
|
|
|
}
|
|
|
|
|
|
// Add genre top 100 button listener
|
|
|
const genreTop100Button = genrePageContent.querySelector('#genre-top100-btn');
|
|
|
if (genreTop100Button) {
|
|
|
genreTop100Button.addEventListener('click', () => {
|
|
|
handleGenreTop100Click(genreSlug, genreId, genreName);
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Update title and show genre page
|
|
|
const titleElement = genrePageContent.querySelector('.genre-page-title');
|
|
|
if (titleElement) titleElement.textContent = genreName;
|
|
|
|
|
|
genrePageContent.style.display = 'block';
|
|
|
|
|
|
// Store current genre info for potential back navigation
|
|
|
genrePageContent.dataset.genreSlug = genreSlug;
|
|
|
genrePageContent.dataset.genreId = genreId;
|
|
|
genrePageContent.dataset.genreName = genreName;
|
|
|
}
|
|
|
|
|
|
function showGenreListView() {
|
|
|
console.log(`🔙 Returning to genre list view`);
|
|
|
|
|
|
// Clean up genre hero slider
|
|
|
if (window.genreHeroSliderState && window.genreHeroSliderState.autoPlayInterval) {
|
|
|
clearInterval(window.genreHeroSliderState.autoPlayInterval);
|
|
|
console.log('🧹 Cleaned up genre hero slider auto-play');
|
|
|
}
|
|
|
|
|
|
// CRITICAL: Restart main slider auto-play
|
|
|
if (typeof beatportRebuildSliderState !== 'undefined' && !beatportRebuildSliderState.autoPlayInterval) {
|
|
|
if (typeof startBeatportRebuildSliderAutoPlay === 'function') {
|
|
|
startBeatportRebuildSliderAutoPlay();
|
|
|
console.log('🔄 Restarted main slider auto-play');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const modal = document.getElementById('genre-browser-modal');
|
|
|
if (!modal) return;
|
|
|
|
|
|
// Show genre list elements
|
|
|
const searchSection = modal.querySelector('.genre-browser-search-section');
|
|
|
const genresSection = modal.querySelector('.genre-browser-genres-section');
|
|
|
const genrePageContent = modal.querySelector('.genre-page-content');
|
|
|
|
|
|
if (searchSection) searchSection.style.display = 'block';
|
|
|
if (genresSection) genresSection.style.display = 'block';
|
|
|
if (genrePageContent) genrePageContent.style.display = 'none';
|
|
|
}
|
|
|
|
|
|
async function loadGenreHeroSlider(genreSlug, genreId, genreName) {
|
|
|
console.log(`🎠 Loading hero slider data for ${genreName}...`);
|
|
|
|
|
|
const container = document.getElementById('genre-hero-slider-container');
|
|
|
if (!container) return;
|
|
|
|
|
|
try {
|
|
|
// Show loading state
|
|
|
container.innerHTML = `
|
|
|
<div class="genre-loading-container">
|
|
|
<div class="genre-loading-spinner"></div>
|
|
|
<p class="genre-loading-text">🎠 Loading ${genreName} hero releases...</p>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
// Fetch hero slider data from API
|
|
|
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/hero`);
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`API returned ${response.status}: ${response.statusText}`);
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success || !data.releases || data.releases.length === 0) {
|
|
|
throw new Error(data.message || 'No hero releases found');
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Loaded ${data.count} hero releases for ${genreName} (cached: ${data.cached})`);
|
|
|
|
|
|
// Create hero slider HTML
|
|
|
const heroSliderHTML = createGenreHeroSliderHTML(data.releases, genreName);
|
|
|
container.innerHTML = heroSliderHTML;
|
|
|
|
|
|
// Add click handlers to individual releases (for future download functionality)
|
|
|
addGenreHeroReleaseClickHandlers(data.releases);
|
|
|
|
|
|
showToast(`Loaded ${data.count} ${genreName} releases`, 'success');
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error(`❌ Error loading hero slider for ${genreName}:`, error);
|
|
|
|
|
|
container.innerHTML = `
|
|
|
<div class="genre-error-container">
|
|
|
<p class="genre-error-text">❌ Failed to load ${genreName} releases</p>
|
|
|
<p class="genre-error-details">${error.message}</p>
|
|
|
<button class="genre-retry-button" onclick="loadGenreHeroSlider('${genreSlug}', '${genreId}', '${genreName}')">
|
|
|
🔄 Retry
|
|
|
</button>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function createGenreHeroSliderHTML(releases, genreName) {
|
|
|
const slidesHTML = releases.map((release, index) => {
|
|
|
// Convert relative URL to absolute URL
|
|
|
const absoluteUrl = release.url.startsWith('http')
|
|
|
? release.url
|
|
|
: `https://www.beatport.com${release.url}`;
|
|
|
|
|
|
return `
|
|
|
<div class="beatport-rebuild-slide ${index === 0 ? 'active' : ''}"
|
|
|
data-slide="${index}"
|
|
|
data-url="${absoluteUrl}"
|
|
|
data-image="${release.image_url}"
|
|
|
style="--slide-bg-image: url('${release.image_url}')">
|
|
|
<div class="beatport-rebuild-slide-background">
|
|
|
<div class="beatport-rebuild-slide-gradient"></div>
|
|
|
</div>
|
|
|
<div class="beatport-rebuild-slide-content">
|
|
|
<div class="beatport-rebuild-track-info">
|
|
|
<h2 class="beatport-rebuild-track-title">${release.title}</h2>
|
|
|
<p class="beatport-rebuild-artist-name">${release.artists_string}</p>
|
|
|
<p class="beatport-rebuild-album-name">${release.label || genreName + ' Hero Release'}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>`;
|
|
|
}).join('');
|
|
|
|
|
|
const indicatorsHTML = releases.map((_, index) => `
|
|
|
<button class="beatport-rebuild-indicator ${index === 0 ? 'active' : ''}" data-slide="${index}"></button>
|
|
|
`).join('');
|
|
|
|
|
|
return `
|
|
|
<div class="beatport-rebuild-slider-container">
|
|
|
<div class="beatport-rebuild-slider" id="genre-hero-slider">
|
|
|
<div class="beatport-rebuild-slider-track" id="genre-hero-slider-track">
|
|
|
${slidesHTML}
|
|
|
</div>
|
|
|
|
|
|
<!-- Slider Navigation -->
|
|
|
<div class="beatport-rebuild-slider-nav">
|
|
|
<button class="beatport-rebuild-nav-btn beatport-rebuild-prev-btn" id="genre-hero-prev-btn">‹</button>
|
|
|
<button class="beatport-rebuild-nav-btn beatport-rebuild-next-btn" id="genre-hero-next-btn">›</button>
|
|
|
</div>
|
|
|
|
|
|
<!-- Slider Indicators -->
|
|
|
<div class="beatport-rebuild-slider-indicators">
|
|
|
${indicatorsHTML}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
function addGenreHeroReleaseClickHandlers(releases) {
|
|
|
// Clear any existing intervals first
|
|
|
if (window.genreHeroSliderState && window.genreHeroSliderState.autoPlayInterval) {
|
|
|
clearInterval(window.genreHeroSliderState.autoPlayInterval);
|
|
|
console.log('🧹 Cleared previous genre hero auto-play interval');
|
|
|
}
|
|
|
|
|
|
// CRITICAL: Clear ALL possible conflicting intervals
|
|
|
if (typeof beatportRebuildSliderState !== 'undefined' && beatportRebuildSliderState.autoPlayInterval) {
|
|
|
clearInterval(beatportRebuildSliderState.autoPlayInterval);
|
|
|
console.log('🛑 Cleared main rebuild slider auto-play interval');
|
|
|
}
|
|
|
|
|
|
// Initialize global slider state for genre hero slider
|
|
|
window.genreHeroSliderState = {
|
|
|
currentSlide: 0,
|
|
|
totalSlides: releases.length,
|
|
|
autoPlayInterval: null
|
|
|
};
|
|
|
|
|
|
console.log(`🎠 Initializing genre hero slider with ${releases.length} slides`);
|
|
|
|
|
|
// Set up navigation button handlers
|
|
|
const prevBtn = document.getElementById('genre-hero-prev-btn');
|
|
|
const nextBtn = document.getElementById('genre-hero-next-btn');
|
|
|
|
|
|
if (prevBtn) {
|
|
|
prevBtn.addEventListener('click', () => {
|
|
|
window.genreHeroSliderState.currentSlide = window.genreHeroSliderState.currentSlide > 0
|
|
|
? window.genreHeroSliderState.currentSlide - 1
|
|
|
: window.genreHeroSliderState.totalSlides - 1;
|
|
|
updateGenreHeroSlide(window.genreHeroSliderState.currentSlide);
|
|
|
console.log(`⬅️ Previous: Moving to slide ${window.genreHeroSliderState.currentSlide}`);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (nextBtn) {
|
|
|
nextBtn.addEventListener('click', () => {
|
|
|
window.genreHeroSliderState.currentSlide = (window.genreHeroSliderState.currentSlide + 1) % window.genreHeroSliderState.totalSlides;
|
|
|
updateGenreHeroSlide(window.genreHeroSliderState.currentSlide);
|
|
|
console.log(`➡️ Next: Moving to slide ${window.genreHeroSliderState.currentSlide}`);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Set up indicator handlers
|
|
|
const indicators = document.querySelectorAll('#genre-hero-slider .beatport-rebuild-indicator');
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.addEventListener('click', () => {
|
|
|
window.genreHeroSliderState.currentSlide = index;
|
|
|
updateGenreHeroSlide(index);
|
|
|
console.log(`🎯 Indicator: Jumping to slide ${index}`);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// Set up individual slide click handlers (like the main hero slider)
|
|
|
const slides = document.querySelectorAll('#genre-hero-slider .beatport-rebuild-slide[data-url]');
|
|
|
console.log(`🔗 Found ${slides.length} slides to set up click handlers for`);
|
|
|
|
|
|
slides.forEach((slide, index) => {
|
|
|
const releaseUrl = slide.getAttribute('data-url');
|
|
|
if (releaseUrl && releaseUrl !== '#' && releaseUrl !== '') {
|
|
|
const release = releases[index];
|
|
|
if (release) {
|
|
|
// Ensure we use the absolute URL and match the expected data structure
|
|
|
const releaseData = {
|
|
|
url: releaseUrl, // This is already the absolute URL from data-url
|
|
|
title: release.title || 'Unknown Title',
|
|
|
artist: release.artists_string || 'Unknown Artist', // handleBeatportReleaseCardClick expects 'artist'
|
|
|
label: release.label || 'Unknown Label',
|
|
|
image_url: release.image_url || '',
|
|
|
// Include all original data for completeness
|
|
|
artists_string: release.artists_string,
|
|
|
type: release.type,
|
|
|
source: release.source,
|
|
|
badges: release.badges || []
|
|
|
};
|
|
|
|
|
|
slide.addEventListener('click', async (event) => {
|
|
|
// Prevent navigation button clicks from triggering this
|
|
|
if (event.target.closest('.beatport-rebuild-nav-btn') ||
|
|
|
event.target.closest('.beatport-rebuild-indicator')) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
console.log(`🎵 Genre hero slide clicked: ${releaseData.title} by ${releaseData.artist}`);
|
|
|
|
|
|
// Use the exact same functionality as the main hero slider
|
|
|
await handleBeatportReleaseCardClick(slide, releaseData);
|
|
|
});
|
|
|
|
|
|
slide.style.cursor = 'pointer';
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Ensure first slide is active BEFORE starting auto-play
|
|
|
updateGenreHeroSlide(0);
|
|
|
|
|
|
// Delay auto-play start to let DOM settle
|
|
|
setTimeout(() => {
|
|
|
startGenreHeroSliderAutoPlay();
|
|
|
}, 100);
|
|
|
|
|
|
// Pause on hover
|
|
|
const sliderContainer = document.querySelector('#genre-hero-slider');
|
|
|
if (sliderContainer) {
|
|
|
sliderContainer.addEventListener('mouseenter', () => {
|
|
|
if (window.genreHeroSliderState.autoPlayInterval) {
|
|
|
clearInterval(window.genreHeroSliderState.autoPlayInterval);
|
|
|
console.log('⏸️ Paused auto-play on hover');
|
|
|
}
|
|
|
});
|
|
|
|
|
|
sliderContainer.addEventListener('mouseleave', () => {
|
|
|
// Delay restart to avoid rapid state changes
|
|
|
setTimeout(() => {
|
|
|
startGenreHeroSliderAutoPlay();
|
|
|
}, 100);
|
|
|
console.log('▶️ Resumed auto-play after hover');
|
|
|
});
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Set up slider functionality for ${releases.length} genre hero releases`);
|
|
|
}
|
|
|
|
|
|
function updateGenreHeroSlide(slideIndex) {
|
|
|
if (!window.genreHeroSliderState) {
|
|
|
console.error('❌ Genre hero slider state not initialized');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// First update the state
|
|
|
window.genreHeroSliderState.currentSlide = slideIndex;
|
|
|
|
|
|
// Update slide visibility - use the exact same logic as main slider
|
|
|
const slides = document.querySelectorAll('#genre-hero-slider .beatport-rebuild-slide');
|
|
|
console.log(`🔄 Updating slide to index ${slideIndex}, found ${slides.length} slides`);
|
|
|
|
|
|
if (slideIndex >= slides.length || slideIndex < 0) {
|
|
|
console.error(`❌ Invalid slide index ${slideIndex}, max is ${slides.length - 1}`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
slides.forEach((slide, index) => {
|
|
|
slide.classList.remove('active', 'prev', 'next');
|
|
|
|
|
|
if (index === slideIndex) {
|
|
|
slide.classList.add('active');
|
|
|
console.log(`✅ Activated slide ${index}: ${slide.getAttribute('data-slide')} - Title: ${slide.querySelector('.beatport-rebuild-track-title')?.textContent}`);
|
|
|
} else if (index < slideIndex) {
|
|
|
slide.classList.add('prev');
|
|
|
} else {
|
|
|
slide.classList.add('next');
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Update indicators
|
|
|
const indicators = document.querySelectorAll('#genre-hero-slider .beatport-rebuild-indicator');
|
|
|
indicators.forEach((indicator, index) => {
|
|
|
indicator.classList.toggle('active', index === slideIndex);
|
|
|
});
|
|
|
|
|
|
console.log(`Genre slide updated to: ${window.genreHeroSliderState.currentSlide}`);
|
|
|
}
|
|
|
|
|
|
function startGenreHeroSliderAutoPlay() {
|
|
|
if (!window.genreHeroSliderState) {
|
|
|
console.error('❌ Cannot start auto-play: Genre hero slider state not initialized');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Clear any existing intervals first
|
|
|
if (window.genreHeroSliderState.autoPlayInterval) {
|
|
|
clearInterval(window.genreHeroSliderState.autoPlayInterval);
|
|
|
console.log('🧹 Cleared existing auto-play interval');
|
|
|
}
|
|
|
|
|
|
window.genreHeroSliderState.autoPlayInterval = setInterval(() => {
|
|
|
if (!window.genreHeroSliderState) {
|
|
|
console.error('❌ Auto-play fired but state is gone, clearing interval');
|
|
|
clearInterval(window.genreHeroSliderState.autoPlayInterval);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const currentSlide = window.genreHeroSliderState.currentSlide;
|
|
|
const totalSlides = window.genreHeroSliderState.totalSlides;
|
|
|
const nextSlide = (currentSlide + 1) % totalSlides;
|
|
|
|
|
|
console.log(`⏰ Auto-play: Current=${currentSlide}, Total=${totalSlides}, Next=${nextSlide}`);
|
|
|
|
|
|
// Validate the next slide index
|
|
|
if (nextSlide >= 0 && nextSlide < totalSlides) {
|
|
|
updateGenreHeroSlide(nextSlide);
|
|
|
} else {
|
|
|
console.error(`❌ Invalid nextSlide calculated: ${nextSlide}, resetting to 0`);
|
|
|
updateGenreHeroSlide(0);
|
|
|
}
|
|
|
}, 5000); // 5 second intervals like the main slider
|
|
|
|
|
|
console.log(`▶️ Started auto-play for genre hero slider (${window.genreHeroSliderState.totalSlides} slides)`);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Load Top 10 lists for a specific genre (Beatport + Hype)
|
|
|
*/
|
|
|
async function loadGenreTop10Lists(genreSlug, genreId, genreName) {
|
|
|
console.log(`🎵 Loading Top 10 lists for ${genreName}...`);
|
|
|
|
|
|
const container = document.getElementById('genre-top10-lists-container');
|
|
|
if (!container) {
|
|
|
console.error('❌ Genre Top 10 lists container not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/top-10-lists`);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success) {
|
|
|
throw new Error(data.error || 'Failed to load Top 10 lists');
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Loaded ${data.beatport_count} Beatport + ${data.hype_count} Hype Top 10 tracks for ${genreName}`);
|
|
|
|
|
|
// Generate HTML using exact same structure as main page (but unique IDs)
|
|
|
const top10ListsHTML = createGenreTop10ListsHTML(data, genreName);
|
|
|
container.innerHTML = top10ListsHTML;
|
|
|
|
|
|
// Add container-level click handlers exactly like main page
|
|
|
addGenreTop10ClickHandlers();
|
|
|
|
|
|
console.log(`✅ Successfully populated genre Top 10 lists for ${genreName}`);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error(`❌ Error loading Top 10 lists for ${genreName}:`, error);
|
|
|
|
|
|
// Show error state
|
|
|
container.innerHTML = `
|
|
|
<div class="genre-top10-error">
|
|
|
<h3>❌ Error Loading Top 10 Lists</h3>
|
|
|
<p>Could not load Top 10 tracks for ${genreName}</p>
|
|
|
<p class="error-detail">${error.message}</p>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Create HTML for genre Top 10 lists (exact structure as main page, unique IDs)
|
|
|
*/
|
|
|
function createGenreTop10ListsHTML(data, genreName) {
|
|
|
const { beatport_top10, hype_top10, has_hype_section } = data;
|
|
|
|
|
|
// Use exact same structure as main page but with genre-specific IDs
|
|
|
let html = `
|
|
|
<div class="beatport-top10-section">
|
|
|
<div class="beatport-top10-header">
|
|
|
<h2 class="beatport-top10-title">🏆 ${genreName} Top 10 Lists</h2>
|
|
|
<p class="beatport-top10-subtitle">Current trending ${genreName.toLowerCase()} tracks</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="beatport-top10-container"${!has_hype_section ? ' style="grid-template-columns: 1fr; justify-items: center; max-width: 700px;"' : ''}>
|
|
|
<!-- Beatport Top 10 List (same classes, unique ID) -->
|
|
|
<div class="beatport-top10-list" id="genre-beatport-top10-list">
|
|
|
<div class="beatport-top10-list-header">
|
|
|
<h3 class="beatport-top10-list-title">🎵 Beatport Top 10</h3>
|
|
|
<p class="beatport-top10-list-subtitle">Most popular ${genreName.toLowerCase()} tracks</p>
|
|
|
</div>
|
|
|
<div class="beatport-top10-tracks">
|
|
|
`;
|
|
|
|
|
|
// Add Beatport Top 10 tracks (same classes as main page)
|
|
|
beatport_top10.forEach((track, index) => {
|
|
|
const cleanTitle = cleanTrackText(track.title || 'Unknown Title');
|
|
|
const cleanArtist = cleanTrackText(track.artist || 'Unknown Artist');
|
|
|
const cleanLabel = cleanTrackText(track.label || 'Unknown Label');
|
|
|
|
|
|
html += `
|
|
|
<div class="beatport-top10-card" data-url="${track.url || '#'}">
|
|
|
<div class="beatport-top10-card-rank">${track.rank || index + 1}</div>
|
|
|
<div class="beatport-top10-card-artwork">
|
|
|
${track.artwork_url ?
|
|
|
`<img src="${track.artwork_url}" alt="${cleanTitle}" loading="lazy">` :
|
|
|
'<div class="beatport-top10-card-placeholder">🎵</div>'
|
|
|
}
|
|
|
</div>
|
|
|
<div class="beatport-top10-card-info">
|
|
|
<h4 class="beatport-top10-card-title">${cleanTitle}</h4>
|
|
|
<p class="beatport-top10-card-artist">${cleanArtist}</p>
|
|
|
<p class="beatport-top10-card-label">${cleanLabel}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
});
|
|
|
|
|
|
html += `
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
// Add Hype Top 10 section (same classes, unique ID)
|
|
|
if (has_hype_section && hype_top10.length > 0) {
|
|
|
html += `
|
|
|
<!-- Hype Top 10 List (same classes, unique ID) -->
|
|
|
<div class="beatport-hype10-list" id="genre-beatport-hype10-list">
|
|
|
<div class="beatport-hype10-list-header">
|
|
|
<h3 class="beatport-hype10-list-title">🔥 Hype Top 10</h3>
|
|
|
<p class="beatport-hype10-list-subtitle">Editor's trending ${genreName.toLowerCase()} picks</p>
|
|
|
</div>
|
|
|
<div class="beatport-hype10-tracks">
|
|
|
`;
|
|
|
|
|
|
// Add Hype Top 10 tracks (same classes as main page)
|
|
|
hype_top10.forEach((track, index) => {
|
|
|
const cleanTitle = cleanTrackText(track.title || 'Unknown Title');
|
|
|
const cleanArtist = cleanTrackText(track.artist || 'Unknown Artist');
|
|
|
const cleanLabel = cleanTrackText(track.label || 'Unknown Label');
|
|
|
|
|
|
html += `
|
|
|
<div class="beatport-hype10-card" data-url="${track.url || '#'}">
|
|
|
<div class="beatport-hype10-card-rank">${track.rank || index + 1}</div>
|
|
|
<div class="beatport-hype10-card-artwork">
|
|
|
${track.artwork_url ?
|
|
|
`<img src="${track.artwork_url}" alt="${cleanTitle}" loading="lazy">` :
|
|
|
'<div class="beatport-hype10-card-placeholder">🔥</div>'
|
|
|
}
|
|
|
</div>
|
|
|
<div class="beatport-hype10-card-info">
|
|
|
<h4 class="beatport-hype10-card-title">${cleanTitle}</h4>
|
|
|
<p class="beatport-hype10-card-artist">${cleanArtist}</p>
|
|
|
<p class="beatport-hype10-card-label">${cleanLabel}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
});
|
|
|
|
|
|
html += `
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
// No else block - completely hide hype section when no hype tracks available
|
|
|
|
|
|
html += `
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
return html;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Add container-level click handlers for genre Top 10 lists (exact parity with main page)
|
|
|
*/
|
|
|
function addGenreTop10ClickHandlers() {
|
|
|
console.log('🔗 Adding container-level click handlers for genre Top 10 lists...');
|
|
|
|
|
|
// Add container-level click handler for Beatport Top 10 (exact match to main page)
|
|
|
const beatportContainer = document.getElementById('genre-beatport-top10-list');
|
|
|
if (beatportContainer) {
|
|
|
beatportContainer.addEventListener('click', () => {
|
|
|
console.log('🎵 Genre Beatport Top 10 container clicked');
|
|
|
handleGenreBeatportTop10Click();
|
|
|
});
|
|
|
console.log('✅ Added Beatport Top 10 container click handler');
|
|
|
}
|
|
|
|
|
|
// Add container-level click handler for Hype Top 10 (exact match to main page)
|
|
|
const hypeContainer = document.getElementById('genre-beatport-hype10-list');
|
|
|
if (hypeContainer) {
|
|
|
hypeContainer.addEventListener('click', () => {
|
|
|
console.log('🔥 Genre Hype Top 10 container clicked');
|
|
|
handleGenreHypeTop10Click();
|
|
|
});
|
|
|
console.log('✅ Added Hype Top 10 container click handler');
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Set up container-level click handlers for genre Top 10 lists`);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle genre Beatport Top 10 container click (exact parity with main page)
|
|
|
*/
|
|
|
async function handleGenreBeatportTop10Click() {
|
|
|
console.log('🎵 Handling Genre Beatport Top 10 click');
|
|
|
|
|
|
// Get the actual genre name from the page title
|
|
|
const genreName = document.querySelector('.genre-page-title')?.textContent?.trim() || 'Genre';
|
|
|
|
|
|
// Use actual genre name in chart title
|
|
|
await handleGenreChartClick('genre_beatport_top10', `${genreName} Beatport Top 10`, 'genre_beatport_top10');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle genre Hype Top 10 container click (exact parity with main page)
|
|
|
*/
|
|
|
async function handleGenreHypeTop10Click() {
|
|
|
console.log('🔥 Handling Genre Hype Top 10 click');
|
|
|
|
|
|
// Get the actual genre name from the page title
|
|
|
const genreName = document.querySelector('.genre-page-title')?.textContent?.trim() || 'Genre';
|
|
|
|
|
|
// Use actual genre name in chart title
|
|
|
await handleGenreChartClick('genre_hype_top10', `${genreName} Hype Top 10`, 'genre_hype_top10');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle genre chart click (based on main page handleRebuildChartClick)
|
|
|
*/
|
|
|
async function handleGenreChartClick(trackDataKey, chartName, chartType) {
|
|
|
if (_beatportModalOpening) return;
|
|
|
_beatportModalOpening = true;
|
|
|
setTimeout(() => { _beatportModalOpening = false; }, 2000);
|
|
|
|
|
|
try {
|
|
|
// Extract track data from DOM cards
|
|
|
const trackData = await getGenrePageTrackData(trackDataKey);
|
|
|
if (!trackData || trackData.length === 0) {
|
|
|
throw new Error(`No track data found for ${chartName}`);
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Got ${trackData.length} tracks from ${chartName}, enriching one-by-one...`);
|
|
|
showLoadingOverlay(`Fetching track metadata... (0/${trackData.length})`);
|
|
|
|
|
|
const enrichedTracks = await _enrichTracksWithProgress(trackData, chartName);
|
|
|
|
|
|
console.log(`✅ Enriched ${enrichedTracks.length} tracks`);
|
|
|
hideLoadingOverlay();
|
|
|
openBeatportChartAsDownloadModal(enrichedTracks, chartName, null);
|
|
|
|
|
|
} catch (error) {
|
|
|
hideLoadingOverlay();
|
|
|
console.error(`❌ Error handling ${chartName} click:`, error);
|
|
|
showToast(`Error loading ${chartName}: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Extract track data from genre page DOM (based on main page getRebuildPageTrackData)
|
|
|
*/
|
|
|
async function getGenrePageTrackData(trackDataKey) {
|
|
|
console.log(`🔍 Extracting ${trackDataKey} data from genre page DOM`);
|
|
|
|
|
|
let containerSelector, cardSelector;
|
|
|
if (trackDataKey === 'genre_beatport_top10') {
|
|
|
containerSelector = '#genre-beatport-top10-list';
|
|
|
cardSelector = '.beatport-top10-card[data-url]';
|
|
|
} else if (trackDataKey === 'genre_hype_top10') {
|
|
|
containerSelector = '#genre-beatport-hype10-list';
|
|
|
cardSelector = '.beatport-hype10-card[data-url]';
|
|
|
} else {
|
|
|
throw new Error(`Unknown track data key: ${trackDataKey}`);
|
|
|
}
|
|
|
|
|
|
const container = document.querySelector(containerSelector);
|
|
|
if (!container) {
|
|
|
throw new Error(`Container ${containerSelector} not found`);
|
|
|
}
|
|
|
|
|
|
const trackCards = container.querySelectorAll(cardSelector);
|
|
|
if (trackCards.length === 0) {
|
|
|
throw new Error(`No track cards found in ${containerSelector}`);
|
|
|
}
|
|
|
|
|
|
// Extract track data from DOM cards (exact same pattern as main page)
|
|
|
const tracks = Array.from(trackCards).map(card => {
|
|
|
const title = card.querySelector('.beatport-top10-card-title, .beatport-hype10-card-title')?.textContent?.trim() || 'Unknown Title';
|
|
|
const artist = card.querySelector('.beatport-top10-card-artist, .beatport-hype10-card-artist')?.textContent?.trim() || 'Unknown Artist';
|
|
|
const label = card.querySelector('.beatport-top10-card-label, .beatport-hype10-card-label')?.textContent?.trim() || 'Unknown Label';
|
|
|
const url = card.getAttribute('data-url') || '';
|
|
|
const rank = card.querySelector('.beatport-top10-card-rank, .beatport-hype10-card-rank')?.textContent?.trim() || '';
|
|
|
|
|
|
return {
|
|
|
title: title,
|
|
|
artist: artist,
|
|
|
label: label,
|
|
|
url: url,
|
|
|
rank: rank
|
|
|
};
|
|
|
});
|
|
|
|
|
|
console.log(`📋 Extracted ${tracks.length} tracks from ${containerSelector}`);
|
|
|
return tracks;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle genre-specific Top 100 button click - create discovery process for genre top 100 tracks
|
|
|
*/
|
|
|
async function handleGenreTop100Click(genreSlug, genreId, genreName) {
|
|
|
if (_beatportModalOpening) return;
|
|
|
_beatportModalOpening = true;
|
|
|
setTimeout(() => { _beatportModalOpening = false; }, 2000);
|
|
|
|
|
|
console.log(`💯 Genre Top 100 button clicked for ${genreName}`);
|
|
|
|
|
|
const chartName = `${genreName} Top 100`;
|
|
|
|
|
|
try {
|
|
|
showLoadingOverlay(`Scraping ${chartName}...`);
|
|
|
|
|
|
// Use the genre tracks endpoint without enrichment
|
|
|
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/tracks?enrich=false`, { method: 'GET' });
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
|
|
throw new Error(`No tracks found in ${chartName}`);
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Fetched ${data.tracks.length} tracks, enriching one-by-one...`);
|
|
|
|
|
|
// Enrich one-by-one with live progress
|
|
|
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, chartName);
|
|
|
|
|
|
hideLoadingOverlay();
|
|
|
openBeatportChartAsDownloadModal(enrichedTracks, chartName, null);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error(`❌ Error handling ${chartName} click:`, error);
|
|
|
hideLoadingOverlay();
|
|
|
showToast(`Error loading ${chartName}: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Load Top 10 releases for a specific genre
|
|
|
*/
|
|
|
async function loadGenreTop10Releases(genreSlug, genreId, genreName) {
|
|
|
console.log(`💿 Loading Top 10 releases for ${genreName}...`);
|
|
|
|
|
|
const container = document.getElementById('genre-top10-releases-container');
|
|
|
if (!container) {
|
|
|
console.error('❌ Genre Top 10 releases container not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/top-10-releases`);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success) {
|
|
|
throw new Error(data.error || 'Failed to load Top 10 releases');
|
|
|
}
|
|
|
|
|
|
console.log(`💿 Loaded ${data.releases.length} Top 10 releases for ${genreName}`);
|
|
|
createGenreTop10ReleasesHTML(data.releases, genreName);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error(`❌ Error loading Top 10 releases for ${genreName}:`, error);
|
|
|
showGenreTop10ReleasesError(error.message || 'Failed to load Top 10 releases');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Create HTML for genre Top 10 releases section (exact parity with main page)
|
|
|
*/
|
|
|
function createGenreTop10ReleasesHTML(releases, genreName) {
|
|
|
const container = document.getElementById('genre-top10-releases-container');
|
|
|
if (!container || !releases || releases.length === 0) return;
|
|
|
|
|
|
// Create section with unique ID but exact same structure as main page
|
|
|
const sectionHtml = `
|
|
|
<div class="beatport-releases-top10-section">
|
|
|
<div class="beatport-releases-top10-header">
|
|
|
<h2 class="beatport-releases-top10-title">💿 Top 10 ${genreName} Releases</h2>
|
|
|
<p class="beatport-releases-top10-subtitle">Most popular albums and EPs for ${genreName}</p>
|
|
|
</div>
|
|
|
<div class="beatport-releases-top10-container">
|
|
|
<div class="beatport-releases-top10-list" id="genre-beatport-releases-top10-list">
|
|
|
${createGenreTop10ReleasesCardsHTML(releases)}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
container.innerHTML = sectionHtml;
|
|
|
|
|
|
// Add background images and click handlers
|
|
|
addGenreTop10ReleasesInteractivity(releases);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Create release cards HTML for genre Top 10 releases
|
|
|
*/
|
|
|
function createGenreTop10ReleasesCardsHTML(releases) {
|
|
|
let cardsHtml = '<div class="beatport-releases-top10-tracks">';
|
|
|
|
|
|
releases.forEach((release, index) => {
|
|
|
cardsHtml += `
|
|
|
<div class="beatport-releases-top10-card" data-url="${release.url || '#'}" data-bg-image="${release.image_url || ''}">
|
|
|
<div class="beatport-releases-top10-card-rank">${release.rank || index + 1}</div>
|
|
|
<div class="beatport-releases-top10-card-artwork">
|
|
|
${release.image_url ?
|
|
|
`<img src="${release.image_url}" alt="${release.title}" loading="lazy">` :
|
|
|
'<div class="beatport-releases-top10-card-placeholder">💿</div>'
|
|
|
}
|
|
|
</div>
|
|
|
<div class="beatport-releases-top10-card-info">
|
|
|
<h4 class="beatport-releases-top10-card-title">${release.title || 'Unknown Title'}</h4>
|
|
|
<p class="beatport-releases-top10-card-artist">${release.artist || 'Unknown Artist'}</p>
|
|
|
<p class="beatport-releases-top10-card-label">${release.label || 'Unknown Label'}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
});
|
|
|
|
|
|
cardsHtml += '</div>';
|
|
|
return cardsHtml;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Add interactivity to genre Top 10 releases cards
|
|
|
*/
|
|
|
function addGenreTop10ReleasesInteractivity(releases) {
|
|
|
const container = document.getElementById('genre-beatport-releases-top10-list');
|
|
|
if (!container) return;
|
|
|
|
|
|
// Set background images for cards
|
|
|
const cards = container.querySelectorAll('.beatport-releases-top10-card[data-bg-image]');
|
|
|
cards.forEach(card => {
|
|
|
const bgImage = card.getAttribute('data-bg-image');
|
|
|
if (bgImage) {
|
|
|
// Transform image URL from 95x95 to 500x500 for higher quality background
|
|
|
const highResImage = bgImage.replace('/image_size/95x95/', '/image_size/500x500/');
|
|
|
card.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.8)), url('${highResImage}')`;
|
|
|
card.style.backgroundSize = 'cover';
|
|
|
card.style.backgroundPosition = 'center';
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Add click handlers for individual release discovery (exact same pattern as main page)
|
|
|
const releaseCards = container.querySelectorAll('.beatport-releases-top10-card[data-url]');
|
|
|
releaseCards.forEach((card, index) => {
|
|
|
card.addEventListener('click', () => handleGenreReleaseCardClick(card, releases[index]));
|
|
|
card.style.cursor = 'pointer';
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle click on individual genre Top 10 Release card (exact parity with main page)
|
|
|
*/
|
|
|
async function handleGenreReleaseCardClick(cardElement, release) {
|
|
|
if (_beatportModalOpening) return;
|
|
|
_beatportModalOpening = true;
|
|
|
|
|
|
console.log(`💿 Individual genre release card clicked: ${release.title} by ${release.artist}`);
|
|
|
|
|
|
if (!release.url || release.url === '#') {
|
|
|
_beatportModalOpening = false;
|
|
|
showToast('No release URL available', 'error');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
showToast(`Loading ${release.title}...`, 'info');
|
|
|
showLoadingOverlay(`Getting tracks from ${release.title}...`);
|
|
|
|
|
|
// Fetch structured release metadata for direct download modal
|
|
|
console.log(`🎵 Fetching release metadata: ${release.url}`);
|
|
|
const response = await fetch('/api/beatport/release-metadata', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ release_url: release.url })
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
|
|
throw new Error(data.error || 'No tracks found in this release');
|
|
|
}
|
|
|
|
|
|
console.log(`✅ Got ${data.tracks.length} tracks from ${data.album.name}`);
|
|
|
|
|
|
const formattedTracks = data.tracks.map(track => ({
|
|
|
...track,
|
|
|
artists: track.artists.map(a => typeof a === 'object' ? a.name : a)
|
|
|
}));
|
|
|
|
|
|
const virtualPlaylistId = `beatport_release_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
const playlistName = data.album.name;
|
|
|
|
|
|
await openDownloadMissingModalForArtistAlbum(
|
|
|
virtualPlaylistId,
|
|
|
playlistName,
|
|
|
formattedTracks,
|
|
|
data.album,
|
|
|
data.artist,
|
|
|
false
|
|
|
);
|
|
|
|
|
|
hideLoadingOverlay();
|
|
|
_beatportModalOpening = false;
|
|
|
console.log(`✅ Opened download modal for ${playlistName}`);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error(`❌ Error handling release click for ${release.title}:`, error);
|
|
|
hideLoadingOverlay();
|
|
|
_beatportModalOpening = false;
|
|
|
showToast(`Error loading ${release.title}: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Show error message for genre Top 10 releases
|
|
|
*/
|
|
|
function showGenreTop10ReleasesError(errorMessage) {
|
|
|
const container = document.getElementById('genre-top10-releases-container');
|
|
|
|
|
|
const errorHtml = `
|
|
|
<div class="beatport-releases-top10-section">
|
|
|
<div class="beatport-releases-top10-header">
|
|
|
<h2 class="beatport-releases-top10-title">💿 Top 10 Releases</h2>
|
|
|
<p class="beatport-releases-top10-subtitle">Error loading releases</p>
|
|
|
</div>
|
|
|
<div class="beatport-releases-top10-container">
|
|
|
<div class="beatport-releases-top10-error">
|
|
|
<h3>❌ Error Loading Releases</h3>
|
|
|
<p>${errorMessage}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
if (container) container.innerHTML = errorHtml;
|
|
|
}
|
|
|
|
|
|
// Initialize the Genre Browser Modal when the page loads
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
initializeGenreBrowserModal();
|
|
|
});
|
|
|
|
|
|
// ============ Plex Music Library Selection ============
|
|
|
|
|
|
async function loadPlexMusicLibraries() {
|
|
|
try {
|
|
|
const response = await fetch('/api/plex/music-libraries');
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.libraries && data.libraries.length > 0) {
|
|
|
const selector = document.getElementById('plex-music-library');
|
|
|
const container = document.getElementById('plex-library-selector-container');
|
|
|
|
|
|
// Clear existing options
|
|
|
selector.innerHTML = '';
|
|
|
|
|
|
// Add options for each library
|
|
|
data.libraries.forEach(library => {
|
|
|
const option = document.createElement('option');
|
|
|
option.value = library.title;
|
|
|
option.textContent = library.title;
|
|
|
|
|
|
// Mark the currently selected library
|
|
|
if (library.title === data.current || library.title === data.selected) {
|
|
|
option.selected = true;
|
|
|
}
|
|
|
|
|
|
selector.appendChild(option);
|
|
|
});
|
|
|
|
|
|
// Show the container
|
|
|
container.style.display = 'block';
|
|
|
} else {
|
|
|
// Hide if no libraries found or not connected
|
|
|
document.getElementById('plex-library-selector-container').style.display = 'none';
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading Plex music libraries:', error);
|
|
|
document.getElementById('plex-library-selector-container').style.display = 'none';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function selectPlexLibrary() {
|
|
|
const selector = document.getElementById('plex-music-library');
|
|
|
const selectedLibrary = selector.value;
|
|
|
|
|
|
if (!selectedLibrary) return;
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/plex/select-music-library', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify({
|
|
|
library_name: selectedLibrary
|
|
|
})
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
console.log(`Plex music library switched to: ${selectedLibrary}`);
|
|
|
} else {
|
|
|
console.error('Failed to switch library:', data.error);
|
|
|
alert(`Failed to switch library: ${data.error}`);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error selecting Plex library:', error);
|
|
|
alert('Error selecting library. Please try again.');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ============ Jellyfin User Selection ============
|
|
|
|
|
|
async function loadJellyfinUsers() {
|
|
|
try {
|
|
|
const response = await fetch('/api/jellyfin/users');
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.users && data.users.length > 0) {
|
|
|
const selector = document.getElementById('jellyfin-user');
|
|
|
const container = document.getElementById('jellyfin-user-selector-container');
|
|
|
|
|
|
// Clear existing options
|
|
|
selector.innerHTML = '';
|
|
|
|
|
|
// Add options for each user
|
|
|
data.users.forEach(user => {
|
|
|
const option = document.createElement('option');
|
|
|
option.value = user.name;
|
|
|
option.textContent = user.name;
|
|
|
|
|
|
// Mark the currently selected user
|
|
|
if (user.name === data.current || user.name === data.selected) {
|
|
|
option.selected = true;
|
|
|
}
|
|
|
|
|
|
selector.appendChild(option);
|
|
|
});
|
|
|
|
|
|
// Show the container
|
|
|
container.style.display = 'block';
|
|
|
} else {
|
|
|
// Hide if no users found or not connected
|
|
|
document.getElementById('jellyfin-user-selector-container').style.display = 'none';
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading Jellyfin users:', error);
|
|
|
document.getElementById('jellyfin-user-selector-container').style.display = 'none';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function selectJellyfinUser() {
|
|
|
const selector = document.getElementById('jellyfin-user');
|
|
|
const selectedUser = selector.value;
|
|
|
|
|
|
if (!selectedUser) return;
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/jellyfin/select-user', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify({
|
|
|
username: selectedUser
|
|
|
})
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
console.log(`Jellyfin user switched to: ${selectedUser}`);
|
|
|
// Refresh library dropdown for the new user
|
|
|
loadJellyfinMusicLibraries();
|
|
|
} else {
|
|
|
console.error('Failed to switch user:', data.error);
|
|
|
alert(`Failed to switch user: ${data.error}`);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error selecting Jellyfin user:', error);
|
|
|
alert('Error selecting user. Please try again.');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ============ Jellyfin Music Library Selection ============
|
|
|
|
|
|
async function loadJellyfinMusicLibraries() {
|
|
|
try {
|
|
|
const response = await fetch('/api/jellyfin/music-libraries');
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.libraries && data.libraries.length > 0) {
|
|
|
const selector = document.getElementById('jellyfin-music-library');
|
|
|
const container = document.getElementById('jellyfin-library-selector-container');
|
|
|
|
|
|
// Clear existing options
|
|
|
selector.innerHTML = '';
|
|
|
|
|
|
// Add options for each library
|
|
|
data.libraries.forEach(library => {
|
|
|
const option = document.createElement('option');
|
|
|
option.value = library.title;
|
|
|
option.textContent = library.title;
|
|
|
|
|
|
// Mark the currently selected library
|
|
|
if (library.title === data.current || library.title === data.selected) {
|
|
|
option.selected = true;
|
|
|
}
|
|
|
|
|
|
selector.appendChild(option);
|
|
|
});
|
|
|
|
|
|
// Show the container
|
|
|
container.style.display = 'block';
|
|
|
} else {
|
|
|
// Hide if no libraries found or not connected
|
|
|
document.getElementById('jellyfin-library-selector-container').style.display = 'none';
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading Jellyfin music libraries:', error);
|
|
|
document.getElementById('jellyfin-library-selector-container').style.display = 'none';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function selectJellyfinLibrary() {
|
|
|
const selector = document.getElementById('jellyfin-music-library');
|
|
|
const selectedLibrary = selector.value;
|
|
|
|
|
|
if (!selectedLibrary) return;
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/jellyfin/select-music-library', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify({
|
|
|
library_name: selectedLibrary
|
|
|
})
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
console.log(`Jellyfin music library switched to: ${selectedLibrary}`);
|
|
|
} else {
|
|
|
console.error('Failed to switch library:', data.error);
|
|
|
alert(`Failed to switch library: ${data.error}`);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error selecting Jellyfin library:', error);
|
|
|
alert('Error selecting library. Please try again.');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ============ Navidrome Music Folder Selection ============
|
|
|
|
|
|
async function loadNavidromeMusicFolders() {
|
|
|
try {
|
|
|
const response = await fetch('/api/navidrome/music-folders');
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && data.folders && data.folders.length > 0) {
|
|
|
const selector = document.getElementById('navidrome-music-folder');
|
|
|
const container = document.getElementById('navidrome-folder-selector-container');
|
|
|
|
|
|
selector.innerHTML = '<option value="">All Libraries</option>';
|
|
|
|
|
|
data.folders.forEach(folder => {
|
|
|
const option = document.createElement('option');
|
|
|
option.value = folder.title;
|
|
|
option.textContent = folder.title;
|
|
|
|
|
|
if (folder.title === data.current || folder.title === data.selected) {
|
|
|
option.selected = true;
|
|
|
}
|
|
|
|
|
|
selector.appendChild(option);
|
|
|
});
|
|
|
|
|
|
container.style.display = 'block';
|
|
|
} else {
|
|
|
document.getElementById('navidrome-folder-selector-container').style.display = 'none';
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading Navidrome music folders:', error);
|
|
|
document.getElementById('navidrome-folder-selector-container').style.display = 'none';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function selectNavidromeMusicFolder() {
|
|
|
const selector = document.getElementById('navidrome-music-folder');
|
|
|
const selectedFolder = selector.value;
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/navidrome/select-music-folder', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ folder_name: selectedFolder })
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
showToast(data.message, 'success');
|
|
|
} else {
|
|
|
console.error('Failed to set music folder:', data.error);
|
|
|
showToast(`Failed to set music folder: ${data.error}`, 'error', 'set-media');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error selecting Navidrome music folder:', error);
|
|
|
showToast('Error selecting music folder. Please try again.', 'error', 'set-media');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ============================================
|
|
|
|