Add pagination and WebSocket support for track listings

This commit introduces pagination and real-time updates to the music site, improving performance and user experience for browsing tracks across genres. Key changes include:

music_site.js:

Added Socket.IO for real-time communication between client and server.

Implemented pagination with TRACKS_PER_PAGE (4 tracks per page) in getTracks function, returning paginated tracks, current page, total pages, and total tracks.

Enhanced slug generation with usedSlugs Map to ensure uniqueness within genres.

Updated routes and WebSocket handlers to support pagination:

Modified getTracks to handle page parameters and return paginated data.

Added io.on('connection') to handle request_page events and emit page_data or error.

Updated / and /sitemap.xml to work with paginated track data.

Adjusted track retrieval in /:genre/track/:slug and /json/:genre to use allTracks for consistency.

index.ejs:

Added pagination controls (Previous/Next buttons) for each genre section, displaying current page and total pages.

Introduced page-container divs to manage paginated track displays, with active class for the current page.

Implemented client-side WebSocket logic with Socket.IO to request and render new pages dynamically.

Added page caching (pageCache) to store rendered pages and reduce server requests.

Enhanced lazy loading of iframes to apply to dynamically loaded pages.

Updated styling for page-container and iframes to ensure proper layout and responsiveness.

These changes enable users to navigate through tracks efficiently without loading all tracks at once, reduce server load, and provide a smoother browsing experience with real-time updates.
This commit is contained in:
2025-06-22 02:54:41 -04:00
parent 2f55294c3d
commit b12b3882f3
4 changed files with 339 additions and 82 deletions

View File

@ -4,8 +4,8 @@ const path = require('path');
const slugify = require('slugify'); const slugify = require('slugify');
// Configuration // Configuration
const PLAYLIST_URL = 'https://soundcloud.com/snxraven/sets/raven-scott-metal'; // Metal playlist URL const PLAYLIST_URL = 'https://soundcloud.com/snxraven/sets/raven-scott-rap'; // Metal playlist URL
const CACHE_FILE = path.join(__dirname, 'cache_metal.json'); // Cache file for metal playlist const CACHE_FILE = path.join(__dirname, 'cache_rap.json'); // Cache file for metal playlist
// Helper function to create a slug from track title // Helper function to create a slug from track title
function generateSlug(title) { function generateSlug(title) {
@ -105,4 +105,4 @@ async function generateCache() {
generateCache().catch(err => { generateCache().catch(err => {
console.error('Error generating cache:', err); console.error('Error generating cache:', err);
process.exit(1); process.exit(1);
}); });

View File

@ -4,9 +4,13 @@ const path = require('path');
const Soundcloud = require('soundcloud.ts').default; const Soundcloud = require('soundcloud.ts').default;
const fs = require('fs'); const fs = require('fs');
const slugify = require('slugify'); const slugify = require('slugify');
const http = require('http');
const socketIo = require('socket.io');
const PORT = process.env.PORT || 6767; const PORT = process.env.PORT || 6767;
// Define genre playlists const server = http.createServer(app);
const io = socketIo(server);
const playlists = { const playlists = {
metal: { metal: {
url: 'https://soundcloud.com/snxraven/sets/raven-scott-metal', url: 'https://soundcloud.com/snxraven/sets/raven-scott-metal',
@ -40,56 +44,89 @@ const playlists = {
} }
}; };
// Helper function to create a slug from track title const TRACKS_PER_PAGE = 4;
function generateSlug(title) {
// Track used slugs to ensure uniqueness
const usedSlugs = new Map(); // genre -> Set of slugs
// Helper function to create a unique slug from track title
function generateSlug(title, genre) {
try { try {
const slug = slugify(title, { let baseSlug = slugify(title || 'unknown', {
lower: true, lower: true,
strict: true, strict: true,
remove: /[*+~.()'"!:@]/g remove: /[*+~.()'"!:@]/g
}); });
if (!slug || slug.trim() === '') {
if (!baseSlug || baseSlug.trim() === '') {
console.warn(`Empty slug for title: "${title}". Using fallback.`); console.warn(`Empty slug for title: "${title}". Using fallback.`);
return 'track-' + Date.now(); baseSlug = 'track-' + Date.now();
} }
console.log(`Generated slug for "${title}": "${slug}"`);
// Ensure uniqueness within the genre
let slug = baseSlug;
let counter = 1;
const genreSlugs = usedSlugs.get(genre) || new Set();
while (genreSlugs.has(slug)) {
slug = `${baseSlug}-${counter}`;
counter++;
}
genreSlugs.add(slug);
usedSlugs.set(genre, genreSlugs);
console.log(`Generated slug for "${title}" in genre "${genre}": "${slug}"`);
return slug; return slug;
} catch (err) { } catch (err) {
console.error(`Error generating slug for title: "${title}"`, err); console.error(`Error generating slug for title: "${title}" in genre "${genre}"`, err);
return 'track-' + Date.now(); const fallback = 'track-' + Date.now();
const genreSlugs = usedSlugs.get(genre) || new Set();
genreSlugs.add(fallback);
usedSlugs.set(genre, genreSlugs);
return fallback;
} }
} }
// Helper function to sanitize track data // Helper function to sanitize track data
function sanitizeTrackData(track) { function sanitizeTrackData(track, genre) {
// Log raw track data to debug URL fields console.log(`Sanitizing track data for "${track.title || 'Unknown'}" in genre "${genre}"`);
console.log(`Raw track data for "${track.title || 'Unknown'}":`, JSON.stringify(track, null, 2));
// Use permalink_url or url (from cache), with fallback
const permalinkUrl = typeof track.permalink_url === 'string' ? track.permalink_url : const permalinkUrl = typeof track.permalink_url === 'string' ? track.permalink_url :
typeof track.url === 'string' ? track.url : typeof track.url === 'string' ? track.url :
typeof track.permalink === 'string' ? track.permalink : ''; typeof track.permalink === 'string' ? track.permalink : '';
// Construct embed URL for SoundCloud player
const embedUrl = permalinkUrl ? const embedUrl = permalinkUrl ?
`https://w.soundcloud.com/player/?url=${encodeURIComponent(permalinkUrl)}&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true` : `https://w.soundcloud.com/player/?url=${encodeURIComponent(permalinkUrl)}&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true` :
''; '';
if (!embedUrl) { if (!embedUrl) {
console.warn(`No embed URL generated for track "${track.title || 'Unknown'}"`); console.warn(`No embed URL generated for track "${track.title || 'Unknown'}" in genre "${genre}"`);
} }
return { const sanitized = {
title: typeof track.title === 'string' ? track.title : 'Unknown Title', title: typeof track.title === 'string' ? track.title : 'Unknown Title',
description: typeof track.description === 'string' ? track.description : 'No description available', description: typeof track.description === 'string' ? track.description : 'No description available',
url: permalinkUrl, // Permalink URL for links url: permalinkUrl,
embedUrl: embedUrl, // For SoundCloud player iframe or widget embedUrl: embedUrl,
playCount: Number.isFinite(track.playback_count) ? track.playback_count : playCount: Number.isFinite(track.playback_count) ? track.playback_count :
Number.isFinite(track.playCount) ? track.playCount : 0, Number.isFinite(track.playCount) ? track.playCount : 0,
publishedAt: typeof track.created_at === 'string' ? track.created_at : publishedAt: typeof track.created_at === 'string' ? track.created_at :
typeof track.publishedAt === 'string' ? track.publishedAt : new Date().toISOString(), typeof track.publishedAt === 'string' ? track.publishedAt : new Date().toISOString(),
slug: typeof track.slug === 'string' ? track.slug : generateSlug(track.title || 'Unknown') slug: typeof track.slug === 'string' && track.slug ? track.slug : generateSlug(track.title, genre)
}; };
// Validate slug
const genreSlugs = usedSlugs.get(genre) || new Set();
if (!sanitized.slug || genreSlugs.has(sanitized.slug)) {
console.warn(`Invalid or duplicate slug "${sanitized.slug}" for "${sanitized.title}". Regenerating.`);
sanitized.slug = generateSlug(sanitized.title, genre);
} else {
genreSlugs.add(sanitized.slug);
usedSlugs.set(genre, genreSlugs);
}
return sanitized;
} }
// Helper function to read the cache // Helper function to read the cache
@ -109,7 +146,6 @@ function readCache(cacheFile) {
// Helper function to save cache // Helper function to save cache
function saveCache(cacheFile, data) { function saveCache(cacheFile, data) {
try { try {
// Filter out invalid tracks
const validTracks = data.tracks.filter(track => { const validTracks = data.tracks.filter(track => {
try { try {
JSON.stringify(track); JSON.stringify(track);
@ -132,103 +168,110 @@ function saveCache(cacheFile, data) {
} }
// Fetch playlist tracks from SoundCloud // Fetch playlist tracks from SoundCloud
async function fetchPlaylist(playlistUrl) { async function fetchPlaylist(playlistUrl, genre) {
try { try {
const soundcloud = new Soundcloud(); // No client ID or OAuth token const soundcloud = new Soundcloud();
const playlist = await soundcloud.playlists.getAlt(playlistUrl); const playlist = await soundcloud.playlists.getAlt(playlistUrl);
console.log(`Fetched playlist with ${playlist.tracks.length} tracks:`, playlist.tracks.map(t => t.title)); console.log(`Fetched playlist with ${playlist.tracks.length} tracks for genre "${genre}":`, playlist.tracks.map(t => t.title));
const tracks = []; const tracks = [];
for (const track of playlist.tracks) { for (const track of playlist.tracks) {
try { try {
console.log(`Processing track: "${track.title || 'Unknown'}"`); console.log(`Processing track: "${track.title || 'Unknown'}" in genre "${genre}"`);
const trackData = sanitizeTrackData(track); const trackData = sanitizeTrackData(track, genre);
console.log(`Generated embed URL: "${trackData.embedUrl}"`); console.log(`Generated embed URL: "${trackData.embedUrl}"`);
tracks.push(trackData); tracks.push(trackData);
console.log(`Successfully processed track: "${track.title || 'Unknown'}"`); console.log(`Successfully processed track: "${track.title || 'Unknown'}"`);
} catch (err) { } catch (err) {
console.error(`Error processing track "${track.title || 'Unknown'}":`, err); console.error(`Error processing track "${track.title || 'Unknown'}" in genre "${genre}":`, err);
continue; continue;
} }
} }
console.log(`Total fetched tracks: ${tracks.length}`); console.log(`Total fetched tracks for genre "${genre}": ${tracks.length}`);
return tracks; return tracks;
} catch (err) { } catch (err) {
console.error(`Error fetching playlist ${playlistUrl}:`, err); console.error(`Error fetching playlist ${playlistUrl} for genre "${genre}":`, err);
return []; return [];
} }
} }
// Get tracks for a specific genre // Get tracks for a specific genre with pagination
async function getTracks(genre, fetch = false) { async function getTracks(genre, fetch = false, page = 1) {
const playlist = playlists[genre]; const playlist = playlists[genre];
const cache = readCache(playlist.cacheFile); const cache = readCache(playlist.cacheFile);
const oneWeekInMs = 7 * 24 * 60 * 60 * 1000; const oneWeekInMs = 7 * 24 * 60 * 60 * 1000;
const now = Date.now(); const now = Date.now();
// Clear used slugs for this genre before processing
usedSlugs.set(genre, new Set());
if (fetch || !cache || (now - cache.timestamp) > oneWeekInMs) { if (fetch || !cache || (now - cache.timestamp) > oneWeekInMs) {
playlist.tracks = await fetchPlaylist(playlist.url); playlist.tracks = await fetchPlaylist(playlist.url, genre);
saveCache(playlist.cacheFile, { tracks: playlist.tracks, timestamp: now }); saveCache(playlist.cacheFile, { tracks: playlist.tracks, timestamp: now });
} else { } else {
// Sanitize all tracks from cache to ensure slugs are assigned
playlist.tracks = cache.tracks.map(track => sanitizeTrackData({ playlist.tracks = cache.tracks.map(track => sanitizeTrackData({
...track, ...track,
slug: track.slug || generateSlug(track.title) slug: track.slug || generateSlug(track.title || 'Unknown', genre)
})); }, genre));
} }
// Sort tracks // Sort tracks (all tracks, not just paginated ones)
if (genre === 'cuts') { if (genre === 'cuts') {
// Sort "cuts" playlist by EP number extracted from title
playlist.tracks.sort((a, b) => { playlist.tracks.sort((a, b) => {
// Extract EP number from title (e.g., "Lets Cut It EP 1" -> 1)
const getEpNumber = (title) => { const getEpNumber = (title) => {
const match = title.match(/EP\s*(\d+)/i); const match = title.match(/EP\s*(\d+)/i);
return match ? parseInt(match[1], 10) : Infinity; // Fallback for non-matching titles return match ? parseInt(match[1], 10) : Infinity;
}; };
const epA = getEpNumber(a.title); const epA = getEpNumber(a.title);
const epB = getEpNumber(b.title); const epB = getEpNumber(b.title);
return epA - epB; // Ascending order return epA - epB;
}); });
} else { } else {
// Existing sorting for other genres (by playCount, then publishedAt)
playlist.tracks.sort((a, b) => { playlist.tracks.sort((a, b) => {
if (b.playCount !== a.playCount) return b.playCount - a.playCount; if (b.playCount !== a.playCount) return b.playCount - a.playCount;
return new Date(b.publishedAt) - new Date(a.publishedAt); return new Date(b.publishedAt) - new Date(a.publishedAt);
}); });
} }
return playlist.tracks; // Paginate tracks for display
const start = (page - 1) * TRACKS_PER_PAGE;
const end = start + TRACKS_PER_PAGE;
const paginatedTracks = playlist.tracks.slice(start, end);
const totalPages = Math.ceil(playlist.tracks.length / TRACKS_PER_PAGE);
return {
tracks: paginatedTracks,
page,
totalPages,
totalTracks: playlist.tracks.length,
allTracks: playlist.tracks // Include all tracks for slug access
};
} }
// Serve static files from public directory
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
// Set EJS as templating engine
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views')); app.set('views', path.join(__dirname, 'views'));
// Home page route
app.get('/', async (req, res) => { app.get('/', async (req, res) => {
const genreTracks = {}; const genreTracks = {};
for (const genre in playlists) { for (const genre in playlists) {
genreTracks[genre] = await getTracks(genre); genreTracks[genre] = await getTracks(genre, false, 1);
} }
res.render('index', { genreTracks }); res.render('index', { genreTracks });
}); });
// Sitemap endpoint
app.get('/sitemap.xml', async (req, res) => { app.get('/sitemap.xml', async (req, res) => {
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n`; let sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n`;
sitemap += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`; sitemap += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`;
// Home page
sitemap += `<url>\n <loc>https://raven-scott.rocks/</loc>\n <priority>1.0</priority>\n</url>\n`; sitemap += `<url>\n <loc>https://raven-scott.rocks/</loc>\n <priority>1.0</priority>\n</url>\n`;
// Track pages for each genre
for (const genre in playlists) { for (const genre in playlists) {
const tracks = await getTracks(genre); const { allTracks } = await getTracks(genre);
tracks.forEach(track => { allTracks.forEach(track => {
sitemap += `<url>\n <loc>https://raven-scott.rocks/${genre}/track/${track.slug}</loc>\n <priority>0.8</priority>\n</url>\n`; sitemap += `<url>\n <loc>https://raven-scott.rocks/${genre}/track/${track.slug}</loc>\n <priority>0.8</priority>\n</url>\n`;
}); });
} }
@ -239,7 +282,6 @@ app.get('/sitemap.xml', async (req, res) => {
res.send(sitemap); res.send(sitemap);
}); });
// Redirect /genre to /#genre
app.get('/:genre', async (req, res) => { app.get('/:genre', async (req, res) => {
const { genre } = req.params; const { genre } = req.params;
if (playlists[genre]) { if (playlists[genre]) {
@ -249,34 +291,61 @@ app.get('/:genre', async (req, res) => {
} }
}); });
// Individual track page route
app.get('/:genre/track/:slug', async (req, res) => { app.get('/:genre/track/:slug', async (req, res) => {
const { genre, slug } = req.params; const { genre, slug } = req.params;
if (!playlists[genre]) { if (!playlists[genre]) {
console.error(`Genre not found: ${genre}`);
return res.status(404).send('Genre not found'); return res.status(404).send('Genre not found');
} }
const tracks = await getTracks(genre); console.log(`Fetching track for genre: ${genre}, slug: ${slug}`);
const track = tracks.find(t => t.slug === slug); const { allTracks } = await getTracks(genre);
const track = allTracks.find(t => t.slug === slug);
if (!track) { if (!track) {
console.error(`Track not found for slug "${slug}" in genre "${genre}". Available slugs:`, allTracks.map(t => t.slug));
return res.status(404).send('Track not found'); return res.status(404).send('Track not found');
} }
console.log(`Found track: "${track.title}" for slug "${slug}" in genre "${genre}"`);
res.render('track', { track, genre }); res.render('track', { track, genre });
}); });
// JSON endpoint for specific genre
app.get('/json/:genre', async (req, res) => { app.get('/json/:genre', async (req, res) => {
const { genre } = req.params; const { genre } = req.params;
if (!playlists[genre]) { if (!playlists[genre]) {
return res.status(404).json({ error: 'Genre not found' }); return res.status(404).json({ error: 'Genre not found' });
} }
const tracks = await getTracks(genre); const { allTracks } = await getTracks(genre);
res.json(tracks); res.json(allTracks);
}); });
// Listen on the specified port io.on('connection', (socket) => {
app.listen(PORT, () => { console.log('Client connected:', socket.id);
socket.on('request_page', async ({ genre, page }) => {
console.log(`Received request_page for genre: ${genre}, page: ${page}`);
if (!playlists[genre]) {
console.error(`Invalid genre: ${genre}`);
socket.emit('error', { message: 'Genre not found' });
return;
}
try {
const data = await getTracks(genre, false, page);
console.log(`Sending page_data for genre: ${genre}, page: ${page}, tracks: ${data.tracks.length}`);
socket.emit('page_data', { genre, ...data });
} catch (err) {
console.error(`Error fetching page ${page} for genre ${genre}:`, err);
socket.emit('error', { message: 'Error fetching tracks' });
}
});
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`); console.log(`Server is running on port ${PORT}`);
}); });

View File

@ -16,6 +16,7 @@
"rss-to-json": "^2.1.1", "rss-to-json": "^2.1.1",
"sitemap": "^8.0.0", "sitemap": "^8.0.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"socket.io": "^4.8.1",
"soundcloud-scraper": "^5.0.3", "soundcloud-scraper": "^5.0.3",
"soundcloud.ts": "^0.6.5", "soundcloud.ts": "^0.6.5",
"transliteration": "^2.3.5" "transliteration": "^2.3.5"

View File

@ -16,6 +16,12 @@
.card { .card {
background-color: #222; background-color: #222;
border: 1px solid #444; border: 1px solid #444;
border-radius: 8px;
overflow: hidden;
}
.card-body {
padding: 1.5rem;
} }
.btn-primary { .btn-primary {
@ -28,7 +34,10 @@
background-color: #ff3300; background-color: #ff3300;
} }
/* Navbar Styling */ .pagination-btn {
margin: 0 10px;
}
.navbar { .navbar {
background-color: #1e1e1e; background-color: #1e1e1e;
padding: 1rem 2rem; padding: 1rem 2rem;
@ -93,7 +102,6 @@
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23ff5500' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23ff5500' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
} }
/* Custom slim dark mode scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
} }
@ -117,10 +125,36 @@
scrollbar-color: #4a4a4a #1e1e1e; scrollbar-color: #4a4a4a #1e1e1e;
} }
/* Fix for section titles being hidden under navbar */
section { section {
padding-top: 90px; /* Adjust this value based on navbar height */ padding-top: 90px;
margin-top: -90px; /* Negative margin to offset the padding */ margin-top: -90px;
}
/* Page container styling */
.page-container {
display: none;
width: 100%; /* Ensure full width for grid */
}
.page-container.active {
display: block;
}
/* Iframe styling */
iframe {
width: 100%;
height: 166px;
max-width: 100%;
border: none;
border-radius: 4px;
}
/* Ensure row and columns work correctly */
.page-container .row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
} }
</style> </style>
</head> </head>
@ -150,24 +184,33 @@
<h2 class="text-center mb-4"> <h2 class="text-center mb-4">
<%= genre.charAt(0).toUpperCase() + genre.slice(1) %> Tracks <%= genre.charAt(0).toUpperCase() + genre.slice(1) %> Tracks
</h2> </h2>
<div class="row"> <div id="<%= genre %>-tracks">
<% genreTracks[genre].forEach(track => { %> <div class="page-container active" data-page="1" data-total-pages="<%= genreTracks[genre].totalPages %>">
<div class="col-md-6 mb-4"> <div class="row">
<div class="card text-center"> <% genreTracks[genre].tracks.forEach(track => { %>
<div class="card-body"> <div class="col-md-6 mb-4" data-slug="<%= track.slug %>">
<h5 class="card-title"> <div class="card text-center">
<%= track.title %> <div class="card-body">
</h5> <h5 class="card-title">
<p class="card-text"></p> <%= track.title %>
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay" </h5>
src="https://w.soundcloud.com/player/?url=<%= track.url %>&color=%23ff5500&auto_play=false&show_artwork=true" <p class="card-text"></p>
loading="lazy"> <iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay"
</iframe> data-src="https://w.soundcloud.com/player/?url=<%= track.url %>&color=%23ff5500&auto_play=false&show_artwork=true"
<a href="/<%= genre %>/track/<%= track.slug %>" class="btn btn-primary mt-3">More Details</a> loading="lazy">
</iframe>
<a href="/<%= genre %>/track/<%= track.slug %>" class="btn btn-primary mt-3" target="_blank">More Details</a>
</div>
</div>
</div> </div>
</div> <% }) %>
</div> </div>
<% }) %> </div>
</div>
<div class="text-center mb-4">
<button class="btn btn-primary pagination-btn" id="<%= genre %>-prev" data-page="<%= genreTracks[genre].page - 1 %>" <%= genreTracks[genre].page === 1 ? 'disabled' : '' %>>Previous</button>
<span>Page <%= genreTracks[genre].page %> of <%= genreTracks[genre].totalPages %></span>
<button class="btn btn-primary pagination-btn" id="<%= genre %>-next" data-page="<%= genreTracks[genre].page + 1 %>" <%= genreTracks[genre].page === genreTracks[genre].totalPages ? 'disabled' : '' %>>Next</button>
</div> </div>
</section> </section>
<% } %> <% } %>
@ -176,6 +219,150 @@
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
<script>
// Lazy load iframes
function lazyLoadIframes(container = document) {
const iframes = container.querySelectorAll('iframe[data-src]');
iframes.forEach(iframe => {
if (!iframe.src && iframe.getBoundingClientRect().top < window.innerHeight) {
iframe.src = iframe.getAttribute('data-src');
console.log(`Lazy loading iframe for track: ${iframe.closest('[data-slug]').dataset.slug}`);
}
});
}
window.addEventListener('load', () => lazyLoadIframes());
window.addEventListener('scroll', () => lazyLoadIframes());
// Page cache: { genre: { page: { container, totalPages } } }
const pageCache = {};
// Track current page per genre
const currentPages = {};
// WebSocket client
const socket = io();
socket.on('connect', () => {
console.log('Connected to WebSocket server');
});
socket.on('page_data', ({ genre, tracks, page, totalPages }) => {
console.log(`Received page_data for genre: ${genre}, page: ${page}, tracks: ${tracks.length}`);
const trackContainer = document.getElementById(`${genre}-tracks`);
// Initialize cache for genre
if (!pageCache[genre]) {
pageCache[genre] = {};
}
// Update current page
currentPages[genre] = page;
// Create new page if not cached
if (!pageCache[genre][page]) {
console.log(`Creating new page container for genre: ${genre}, page: ${page}`);
const pageContainer = document.createElement('div');
pageContainer.className = 'page-container';
pageContainer.dataset.page = page;
pageContainer.dataset.totalPages = totalPages;
// Create row for grid
const row = document.createElement('div');
row.className = 'row';
// Build track elements
tracks.forEach(track => {
const trackDiv = document.createElement('div');
trackDiv.className = 'col-md-6 mb-4';
trackDiv.dataset.slug = track.slug;
trackDiv.innerHTML = `
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">${track.title}</h5>
<p class="card-text"></p>
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay"
data-src="https://w.soundcloud.com/player/?url=${track.url}&color=%23ff5500&auto_play=false&show_artwork=true"
loading="lazy"></iframe>
<a href="/${genre}/track/${track.slug}" class="btn btn-primary mt-3">More Details</a>
</div>
</div>
`;
row.appendChild(trackDiv);
});
pageContainer.appendChild(row);
trackContainer.appendChild(pageContainer);
pageCache[genre][page] = { container: pageContainer, totalPages };
lazyLoadIframes(pageContainer);
}
// Switch to requested page
const allPages = trackContainer.querySelectorAll('.page-container');
allPages.forEach(p => p.classList.remove('active'));
pageCache[genre][page].container.classList.add('active');
console.log(`Switched to page ${page} for genre: ${genre}`);
// Update pagination controls
const prevButton = document.getElementById(`${genre}-prev`);
const nextButton = document.getElementById(`${genre}-next`);
prevButton.disabled = page === 1;
nextButton.disabled = page === totalPages;
prevButton.dataset.page = page - 1;
nextButton.dataset.page = page + 1;
const pageInfo = document.querySelector(`#${genre}-tracks + .text-center span`);
pageInfo.textContent = `Page ${page} of ${totalPages}`;
lazyLoadIframes(pageCache[genre][page].container);
});
socket.on('error', ({ message }) => {
console.error('WebSocket error:', message);
});
// Pagination button handlers
document.querySelectorAll('.pagination-btn').forEach(button => {
button.addEventListener('click', () => {
const genre = button.id.split('-')[0];
const page = parseInt(button.dataset.page, 10);
console.log(`Button clicked: ${button.id}, genre: ${genre}, page: ${page}`);
if (page > 0 && page !== currentPages[genre]) {
if (pageCache[genre] && pageCache[genre][page]) {
console.log(`Loading cached page ${page} for genre: ${genre}`);
const trackContainer = document.getElementById(`${genre}-tracks`);
const allPages = trackContainer.querySelectorAll('.page-container');
allPages.forEach(p => p.classList.remove('active'));
pageCache[genre][page].container.classList.add('active');
// Update pagination controls
const prevButton = document.getElementById(`${genre}-prev`);
const nextButton = document.getElementById(`${genre}-next`);
const totalPages = parseInt(pageCache[genre][page].totalPages);
prevButton.disabled = page === 1;
nextButton.disabled = page === totalPages;
prevButton.dataset.page = page - 1;
nextButton.dataset.page = page + 1;
const pageInfo = document.querySelector(`#${genre}-tracks + .text-center span`);
pageInfo.textContent = `Page ${page} of ${totalPages}`;
currentPages[genre] = page;
lazyLoadIframes(pageCache[genre][page].container);
} else {
socket.emit('request_page', { genre, page });
console.log(`Emitted request_page for genre: ${genre}, page: ${page}`);
}
} else if (page <= 0) {
console.error(`Invalid page number: ${page}`);
} else {
console.log(`Already on page ${page} for genre: ${genre}`);
}
});
});
</script>
</body> </body>
</html> </html>