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:
@ -4,8 +4,8 @@ const path = require('path');
|
||||
const slugify = require('slugify');
|
||||
|
||||
// Configuration
|
||||
const PLAYLIST_URL = 'https://soundcloud.com/snxraven/sets/raven-scott-metal'; // Metal playlist URL
|
||||
const CACHE_FILE = path.join(__dirname, 'cache_metal.json'); // Cache file for metal playlist
|
||||
const PLAYLIST_URL = 'https://soundcloud.com/snxraven/sets/raven-scott-rap'; // Metal playlist URL
|
||||
const CACHE_FILE = path.join(__dirname, 'cache_rap.json'); // Cache file for metal playlist
|
||||
|
||||
// Helper function to create a slug from track title
|
||||
function generateSlug(title) {
|
||||
|
185
music_site.js
185
music_site.js
@ -4,9 +4,13 @@ const path = require('path');
|
||||
const Soundcloud = require('soundcloud.ts').default;
|
||||
const fs = require('fs');
|
||||
const slugify = require('slugify');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
const PORT = process.env.PORT || 6767;
|
||||
|
||||
// Define genre playlists
|
||||
const server = http.createServer(app);
|
||||
const io = socketIo(server);
|
||||
|
||||
const playlists = {
|
||||
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
|
||||
function generateSlug(title) {
|
||||
const TRACKS_PER_PAGE = 4;
|
||||
|
||||
// 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 {
|
||||
const slug = slugify(title, {
|
||||
let baseSlug = slugify(title || 'unknown', {
|
||||
lower: true,
|
||||
strict: true,
|
||||
remove: /[*+~.()'"!:@]/g
|
||||
});
|
||||
if (!slug || slug.trim() === '') {
|
||||
|
||||
if (!baseSlug || baseSlug.trim() === '') {
|
||||
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;
|
||||
} catch (err) {
|
||||
console.error(`Error generating slug for title: "${title}"`, err);
|
||||
return 'track-' + Date.now();
|
||||
console.error(`Error generating slug for title: "${title}" in genre "${genre}"`, err);
|
||||
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
|
||||
function sanitizeTrackData(track) {
|
||||
// Log raw track data to debug URL fields
|
||||
console.log(`Raw track data for "${track.title || 'Unknown'}":`, JSON.stringify(track, null, 2));
|
||||
function sanitizeTrackData(track, genre) {
|
||||
console.log(`Sanitizing track data for "${track.title || 'Unknown'}" in genre "${genre}"`);
|
||||
|
||||
// Use permalink_url or url (from cache), with fallback
|
||||
const permalinkUrl = typeof track.permalink_url === 'string' ? track.permalink_url :
|
||||
typeof track.url === 'string' ? track.url :
|
||||
typeof track.permalink === 'string' ? track.permalink : '';
|
||||
|
||||
// Construct embed URL for SoundCloud player
|
||||
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` :
|
||||
'';
|
||||
|
||||
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',
|
||||
description: typeof track.description === 'string' ? track.description : 'No description available',
|
||||
url: permalinkUrl, // Permalink URL for links
|
||||
embedUrl: embedUrl, // For SoundCloud player iframe or widget
|
||||
url: permalinkUrl,
|
||||
embedUrl: embedUrl,
|
||||
playCount: Number.isFinite(track.playback_count) ? track.playback_count :
|
||||
Number.isFinite(track.playCount) ? track.playCount : 0,
|
||||
publishedAt: typeof track.created_at === 'string' ? track.created_at :
|
||||
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
|
||||
@ -109,7 +146,6 @@ function readCache(cacheFile) {
|
||||
// Helper function to save cache
|
||||
function saveCache(cacheFile, data) {
|
||||
try {
|
||||
// Filter out invalid tracks
|
||||
const validTracks = data.tracks.filter(track => {
|
||||
try {
|
||||
JSON.stringify(track);
|
||||
@ -132,103 +168,110 @@ function saveCache(cacheFile, data) {
|
||||
}
|
||||
|
||||
// Fetch playlist tracks from SoundCloud
|
||||
async function fetchPlaylist(playlistUrl) {
|
||||
async function fetchPlaylist(playlistUrl, genre) {
|
||||
try {
|
||||
const soundcloud = new Soundcloud(); // No client ID or OAuth token
|
||||
const soundcloud = new Soundcloud();
|
||||
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 = [];
|
||||
for (const track of playlist.tracks) {
|
||||
try {
|
||||
console.log(`Processing track: "${track.title || 'Unknown'}"`);
|
||||
const trackData = sanitizeTrackData(track);
|
||||
console.log(`Processing track: "${track.title || 'Unknown'}" in genre "${genre}"`);
|
||||
const trackData = sanitizeTrackData(track, genre);
|
||||
console.log(`Generated embed URL: "${trackData.embedUrl}"`);
|
||||
tracks.push(trackData);
|
||||
console.log(`Successfully processed track: "${track.title || 'Unknown'}"`);
|
||||
} catch (err) {
|
||||
console.error(`Error processing track "${track.title || 'Unknown'}":`, err);
|
||||
console.error(`Error processing track "${track.title || 'Unknown'}" in genre "${genre}":`, err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Total fetched tracks: ${tracks.length}`);
|
||||
console.log(`Total fetched tracks for genre "${genre}": ${tracks.length}`);
|
||||
return tracks;
|
||||
} catch (err) {
|
||||
console.error(`Error fetching playlist ${playlistUrl}:`, err);
|
||||
console.error(`Error fetching playlist ${playlistUrl} for genre "${genre}":`, err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get tracks for a specific genre
|
||||
async function getTracks(genre, fetch = false) {
|
||||
// Get tracks for a specific genre with pagination
|
||||
async function getTracks(genre, fetch = false, page = 1) {
|
||||
const playlist = playlists[genre];
|
||||
const cache = readCache(playlist.cacheFile);
|
||||
const oneWeekInMs = 7 * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
// Clear used slugs for this genre before processing
|
||||
usedSlugs.set(genre, new Set());
|
||||
|
||||
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 });
|
||||
} else {
|
||||
// Sanitize all tracks from cache to ensure slugs are assigned
|
||||
playlist.tracks = cache.tracks.map(track => sanitizeTrackData({
|
||||
...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') {
|
||||
// Sort "cuts" playlist by EP number extracted from title
|
||||
playlist.tracks.sort((a, b) => {
|
||||
// Extract EP number from title (e.g., "Lets Cut It EP 1" -> 1)
|
||||
const getEpNumber = (title) => {
|
||||
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 epB = getEpNumber(b.title);
|
||||
return epA - epB; // Ascending order
|
||||
return epA - epB;
|
||||
});
|
||||
} else {
|
||||
// Existing sorting for other genres (by playCount, then publishedAt)
|
||||
playlist.tracks.sort((a, b) => {
|
||||
if (b.playCount !== a.playCount) return b.playCount - a.playCount;
|
||||
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')));
|
||||
|
||||
// Set EJS as templating engine
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
|
||||
// Home page route
|
||||
app.get('/', async (req, res) => {
|
||||
const genreTracks = {};
|
||||
for (const genre in playlists) {
|
||||
genreTracks[genre] = await getTracks(genre);
|
||||
genreTracks[genre] = await getTracks(genre, false, 1);
|
||||
}
|
||||
res.render('index', { genreTracks });
|
||||
});
|
||||
|
||||
// Sitemap endpoint
|
||||
app.get('/sitemap.xml', async (req, res) => {
|
||||
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>\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`;
|
||||
|
||||
// Track pages for each genre
|
||||
for (const genre in playlists) {
|
||||
const tracks = await getTracks(genre);
|
||||
tracks.forEach(track => {
|
||||
const { allTracks } = await getTracks(genre);
|
||||
allTracks.forEach(track => {
|
||||
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);
|
||||
});
|
||||
|
||||
// Redirect /genre to /#genre
|
||||
app.get('/:genre', async (req, res) => {
|
||||
const { genre } = req.params;
|
||||
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) => {
|
||||
const { genre, slug } = req.params;
|
||||
if (!playlists[genre]) {
|
||||
console.error(`Genre not found: ${genre}`);
|
||||
return res.status(404).send('Genre not found');
|
||||
}
|
||||
|
||||
const tracks = await getTracks(genre);
|
||||
const track = tracks.find(t => t.slug === slug);
|
||||
console.log(`Fetching track for genre: ${genre}, slug: ${slug}`);
|
||||
const { allTracks } = await getTracks(genre);
|
||||
const track = allTracks.find(t => t.slug === slug);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
console.log(`Found track: "${track.title}" for slug "${slug}" in genre "${genre}"`);
|
||||
res.render('track', { track, genre });
|
||||
});
|
||||
|
||||
// JSON endpoint for specific genre
|
||||
app.get('/json/:genre', async (req, res) => {
|
||||
const { genre } = req.params;
|
||||
if (!playlists[genre]) {
|
||||
return res.status(404).json({ error: 'Genre not found' });
|
||||
}
|
||||
const tracks = await getTracks(genre);
|
||||
res.json(tracks);
|
||||
const { allTracks } = await getTracks(genre);
|
||||
res.json(allTracks);
|
||||
});
|
||||
|
||||
// Listen on the specified port
|
||||
app.listen(PORT, () => {
|
||||
io.on('connection', (socket) => {
|
||||
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}`);
|
||||
});
|
@ -16,6 +16,7 @@
|
||||
"rss-to-json": "^2.1.1",
|
||||
"sitemap": "^8.0.0",
|
||||
"slugify": "^1.6.6",
|
||||
"socket.io": "^4.8.1",
|
||||
"soundcloud-scraper": "^5.0.3",
|
||||
"soundcloud.ts": "^0.6.5",
|
||||
"transliteration": "^2.3.5"
|
||||
|
205
views/index.ejs
205
views/index.ejs
@ -16,6 +16,12 @@
|
||||
.card {
|
||||
background-color: #222;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@ -28,7 +34,10 @@
|
||||
background-color: #ff3300;
|
||||
}
|
||||
|
||||
/* Navbar Styling */
|
||||
.pagination-btn {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: #1e1e1e;
|
||||
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");
|
||||
}
|
||||
|
||||
/* Custom slim dark mode scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
@ -117,10 +125,36 @@
|
||||
scrollbar-color: #4a4a4a #1e1e1e;
|
||||
}
|
||||
|
||||
/* Fix for section titles being hidden under navbar */
|
||||
section {
|
||||
padding-top: 90px; /* Adjust this value based on navbar height */
|
||||
margin-top: -90px; /* Negative margin to offset the padding */
|
||||
padding-top: 90px;
|
||||
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>
|
||||
</head>
|
||||
@ -150,9 +184,11 @@
|
||||
<h2 class="text-center mb-4">
|
||||
<%= genre.charAt(0).toUpperCase() + genre.slice(1) %> Tracks
|
||||
</h2>
|
||||
<div id="<%= genre %>-tracks">
|
||||
<div class="page-container active" data-page="1" data-total-pages="<%= genreTracks[genre].totalPages %>">
|
||||
<div class="row">
|
||||
<% genreTracks[genre].forEach(track => { %>
|
||||
<div class="col-md-6 mb-4">
|
||||
<% genreTracks[genre].tracks.forEach(track => { %>
|
||||
<div class="col-md-6 mb-4" data-slug="<%= track.slug %>">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
@ -160,15 +196,22 @@
|
||||
</h5>
|
||||
<p class="card-text"></p>
|
||||
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay"
|
||||
src="https://w.soundcloud.com/player/?url=<%= track.url %>&color=%23ff5500&auto_play=false&show_artwork=true"
|
||||
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>
|
||||
<a href="/<%= genre %>/track/<%= track.slug %>" class="btn btn-primary mt-3" target="_blank">More Details</a>
|
||||
</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>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
@ -176,6 +219,150 @@
|
||||
<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://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>
|
||||
|
||||
</html>
|
Reference in New Issue
Block a user