const express = require('express'); const app = express(); 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; const server = http.createServer(app); const io = socketIo(server); const playlists = { metal: { url: 'https://soundcloud.com/snxraven/sets/raven-scott-metal', cacheFile: path.join(__dirname, 'cache_metal.json'), tracks: [] }, altrock: { url: 'https://soundcloud.com/snxraven/sets/raven-scott-alt-rock', cacheFile: path.join(__dirname, 'cache_altrock.json'), tracks: [] }, rap: { url: 'https://soundcloud.com/snxraven/sets/raven-scott-rap', cacheFile: path.join(__dirname, 'cache_rap.json'), tracks: [] }, lofi: { url: 'https://soundcloud.com/snxraven/sets/raven-scott-lofi', cacheFile: path.join(__dirname, 'cache_lofi.json'), tracks: [] }, edm: { url: 'https://soundcloud.com/snxraven/sets/raven-scott-edm', cacheFile: path.join(__dirname, 'cache_edm.json'), tracks: [] }, cuts: { url: 'https://soundcloud.com/snxraven/sets/lets-cut-it', cacheFile: path.join(__dirname, 'cache_cuts.json'), tracks: [] } }; 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 { let baseSlug = slugify(title || 'unknown', { lower: true, strict: true, remove: /[*+~.()'"!:@]/g }); if (!baseSlug || baseSlug.trim() === '') { console.warn(`Empty slug for title: "${title}". Using fallback.`); baseSlug = 'track-' + Date.now(); } // 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}" 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, genre) { console.log(`Sanitizing track data for "${track.title || 'Unknown'}" in genre "${genre}"`); const permalinkUrl = typeof track.permalink_url === 'string' ? track.permalink_url : typeof track.url === 'string' ? track.url : typeof track.permalink === 'string' ? track.permalink : ''; 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'}" in genre "${genre}"`); } const sanitized = { title: typeof track.title === 'string' ? track.title : 'Unknown Title', description: typeof track.description === 'string' ? track.description : 'No description available', 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 ? 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 function readCache(cacheFile) { if (fs.existsSync(cacheFile)) { try { const data = fs.readFileSync(cacheFile, { encoding: 'utf8' }); return JSON.parse(data); } catch (err) { console.error(`Error reading cache from ${cacheFile}:`, err); return null; } } return null; } // Helper function to save cache function saveCache(cacheFile, data) { try { const validTracks = data.tracks.filter(track => { try { JSON.stringify(track); return true; } catch (err) { console.error(`Invalid track data for "${track.title}":`, err); return false; } }); const cacheData = { tracks: validTracks, timestamp: data.timestamp }; console.log(`Saving cache with ${validTracks.length} tracks to ${cacheFile}`); console.log('Cached track titles:', validTracks.map(t => t.title)); const jsonString = JSON.stringify(cacheData, null, 2); fs.writeFileSync(cacheFile, jsonString, { encoding: 'utf8' }); console.log(`Cache saved to ${cacheFile}`); } catch (err) { console.error(`Error saving cache to ${cacheFile}:`, err); console.error('Problematic cache data:', data); } } // Fetch playlist tracks from SoundCloud async function fetchPlaylist(playlistUrl, genre) { try { const soundcloud = new Soundcloud(); const playlist = await soundcloud.playlists.getAlt(playlistUrl); 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'}" 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'}" in genre "${genre}":`, err); continue; } } console.log(`Total fetched tracks for genre "${genre}": ${tracks.length}`); return tracks; } catch (err) { console.error(`Error fetching playlist ${playlistUrl} for genre "${genre}":`, err); return []; } } // 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, 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 || 'Unknown', genre) }, genre)); } // Sort tracks (all tracks, not just paginated ones) if (genre === 'cuts') { playlist.tracks.sort((a, b) => { const getEpNumber = (title) => { const match = title.match(/EP\s*(\d+)/i); return match ? parseInt(match[1], 10) : Infinity; }; const epA = getEpNumber(a.title); const epB = getEpNumber(b.title); return epA - epB; }); } else { playlist.tracks.sort((a, b) => { if (b.playCount !== a.playCount) return b.playCount - a.playCount; return new Date(b.publishedAt) - new Date(a.publishedAt); }); } // 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 }; } app.use(express.static(path.join(__dirname, 'public'))); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); app.get('/', async (req, res) => { const genreTracks = {}; for (const genre in playlists) { genreTracks[genre] = await getTracks(genre, false, 1); } res.render('index', { genreTracks }); }); app.get('/sitemap.xml', async (req, res) => { let sitemap = `\n`; sitemap += `\n`; sitemap += `\n https://raven-scott.rocks/\n 1.0\n\n`; for (const genre in playlists) { const { allTracks } = await getTracks(genre); allTracks.forEach(track => { sitemap += `\n https://raven-scott.rocks/${genre}/track/${track.slug}\n 0.8\n\n`; }); } sitemap += ``; res.header('Content-Type', 'application/xml'); res.send(sitemap); }); app.get('/:genre', async (req, res) => { const { genre } = req.params; if (playlists[genre]) { res.redirect(`/#${genre}`); } else { res.status(404).send('Genre not found'); } }); 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'); } 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 }); }); app.get('/json/:genre', async (req, res) => { const { genre } = req.params; if (!playlists[genre]) { return res.status(404).json({ error: 'Genre not found' }); } const { allTracks } = await getTracks(genre); res.json(allTracks); }); 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}`); });