diff --git a/manual.js b/manual.js index 3c883ca..35d93b9 100644 --- a/manual.js +++ b/manual.js @@ -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) { @@ -105,4 +105,4 @@ async function generateCache() { generateCache().catch(err => { console.error('Error generating cache:', err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/music_site.js b/music_site.js index 70dda91..b59a600 100644 --- a/music_site.js +++ b/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 = `\n`; sitemap += `\n`; - // Home page sitemap += `\n https://raven-scott.rocks/\n 1.0\n\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 += `\n https://raven-scott.rocks/${genre}/track/${track.slug}\n 0.8\n\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}`); }); \ No newline at end of file diff --git a/package.json b/package.json index d377249..7f249b8 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/views/index.ejs b/views/index.ejs index 6bf744c..455eb45 100644 --- a/views/index.ejs +++ b/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; } @@ -150,24 +184,33 @@

<%= genre.charAt(0).toUpperCase() + genre.slice(1) %> Tracks

-
- <% genreTracks[genre].forEach(track => { %> -
-
-
-
- <%= track.title %> -
-

- - More Details +
+
+
+ <% genreTracks[genre].tracks.forEach(track => { %> +
+
+
+
+ <%= track.title %> +
+

+ + More Details +
+
-
+ <% }) %>
- <% }) %> +
+
+
+ + Page <%= genreTracks[genre].page %> of <%= genreTracks[genre].totalPages %> +
<% } %> @@ -176,6 +219,150 @@ + + \ No newline at end of file