diff --git a/manual.js b/manual.js new file mode 100644 index 0000000..3c883ca --- /dev/null +++ b/manual.js @@ -0,0 +1,108 @@ +const Soundcloud = require('soundcloud.ts').default; +const fs = require('fs'); +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 + +// Helper function to create a slug from track title +function generateSlug(title) { + try { + const slug = slugify(title, { + lower: true, + strict: true, + remove: /[*+~.()'"!:@]/g + }); + if (!slug || slug.trim() === '') { + console.warn(`Empty slug for title: "${title}". Using fallback.`); + return 'track-' + Date.now(); + } + console.log(`Generated slug for "${title}": "${slug}"`); + return slug; + } catch (err) { + console.error(`Error generating slug for title: "${title}"`, err); + return 'track-' + Date.now(); + } +} + +// Helper function to sanitize track data +function sanitizeTrackData(track) { + return { + title: typeof track.title === 'string' ? track.title : 'Unknown Title', + description: typeof track.description === 'string' ? track.description : 'No description available', + url: typeof track.permalink_url === 'string' ? track.permalink_url : '', + playCount: Number.isFinite(track.playback_count) ? track.playback_count : 0, + publishedAt: typeof track.created_at === 'string' ? track.created_at : new Date().toISOString(), + slug: generateSlug(track.title || 'Unknown') + }; +} + +// Helper function to save cache +function saveCache(cacheFile, data) { + try { + // Filter out invalid tracks + 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 all tracks from a SoundCloud playlist +async function fetchPlaylist(playlistUrl) { + try { + const soundcloud = new Soundcloud(); // No client ID or OAuth token + const playlist = await soundcloud.playlists.getAlt(playlistUrl); + console.log(`Fetched playlist with ${playlist.tracks.length} tracks:`, 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); + tracks.push(trackData); + console.log(`Successfully processed track: "${track.title || 'Unknown'}"`); + } catch (err) { + console.error(`Error processing track "${track.title || 'Unknown'}":`, err); + continue; + } + } + + console.log(`Total fetched tracks: ${tracks.length}`); + if (tracks.length < 56) { + console.warn(`Expected ~56 tracks, but only ${tracks.length} were fetched. Verify playlist URL or check for private tracks.`); + } + return tracks; + } catch (err) { + console.error(`Error fetching playlist ${playlistUrl}:`, err); + return []; + } +} + +// Main function to generate cache +async function generateCache() { + const tracks = await fetchPlaylist(PLAYLIST_URL); + saveCache(CACHE_FILE, { tracks, timestamp: Date.now() }); +} + +// Run the cache generation +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 9d8cee7..70dda91 100644 --- a/music_site.js +++ b/music_site.js @@ -1,9 +1,9 @@ const express = require('express'); const app = express(); const path = require('path'); -const SoundCloud = require('soundcloud-scraper'); -const client = new SoundCloud.Client(); +const Soundcloud = require('soundcloud.ts').default; const fs = require('fs'); +const slugify = require('slugify'); const PORT = process.env.PORT || 6767; // Define genre playlists @@ -27,43 +27,137 @@ const playlists = { 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: [] } }; // Helper function to create a slug from track title function generateSlug(title) { - return title - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, ''); + try { + const slug = slugify(title, { + lower: true, + strict: true, + remove: /[*+~.()'"!:@]/g + }); + if (!slug || slug.trim() === '') { + console.warn(`Empty slug for title: "${title}". Using fallback.`); + return 'track-' + Date.now(); + } + console.log(`Generated slug for "${title}": "${slug}"`); + return slug; + } catch (err) { + console.error(`Error generating slug for title: "${title}"`, err); + return 'track-' + Date.now(); + } +} + +// 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)); + + // 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'}"`); + } + + return { + 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 + 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') + }; } // Helper function to read the cache function readCache(cacheFile) { if (fs.existsSync(cacheFile)) { - const data = fs.readFileSync(cacheFile, 'utf8'); - return JSON.parse(data); + 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) { - fs.writeFileSync(cacheFile, JSON.stringify(data), 'utf8'); + try { + // Filter out invalid tracks + 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) { - const playlist = await client.getPlaylist(playlistUrl); - - return playlist.tracks.map(track => ({ - title: track.title, - description: track.description || 'No description available', - url: track.url, - playCount: track.playCount || 0, - publishedAt: track.publishedAt || new Date().toISOString(), - slug: generateSlug(track.title) - })); + try { + const soundcloud = new Soundcloud(); // No client ID or OAuth token + const playlist = await soundcloud.playlists.getAlt(playlistUrl); + console.log(`Fetched playlist with ${playlist.tracks.length} tracks:`, 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(`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); + continue; + } + } + + console.log(`Total fetched tracks: ${tracks.length}`); + return tracks; + } catch (err) { + console.error(`Error fetching playlist ${playlistUrl}:`, err); + return []; + } } // Get tracks for a specific genre @@ -77,17 +171,32 @@ async function getTracks(genre, fetch = false) { playlist.tracks = await fetchPlaylist(playlist.url); saveCache(playlist.cacheFile, { tracks: playlist.tracks, timestamp: now }); } else { - playlist.tracks = cache.tracks.map(track => ({ + playlist.tracks = cache.tracks.map(track => sanitizeTrackData({ ...track, slug: track.slug || generateSlug(track.title) })); } - // Sort by playCount first, then by 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); - }); + // Sort tracks + 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 + }; + const epA = getEpNumber(a.title); + const epB = getEpNumber(b.title); + return epA - epB; // Ascending order + }); + } 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; } @@ -125,12 +234,11 @@ app.get('/sitemap.xml', async (req, res) => { } sitemap += ``; - + res.header('Content-Type', 'application/xml'); res.send(sitemap); }); - // Redirect /genre to /#genre app.get('/:genre', async (req, res) => { const { genre } = req.params; @@ -147,7 +255,7 @@ app.get('/:genre/track/:slug', async (req, res) => { if (!playlists[genre]) { return res.status(404).send('Genre not found'); } - + const tracks = await getTracks(genre); const track = tracks.find(t => t.slug === slug); @@ -168,7 +276,6 @@ app.get('/json/:genre', async (req, res) => { res.json(tracks); }); - // Listen on the specified port app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); diff --git a/package.json b/package.json index b718d05..d377249 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,15 @@ "dependencies": { "axios": "^0.27.2", "cheerio": "^1.0.0-rc.10", + "compression": "^1.8.0", "ejs": "^3.1.8", - "express": "^4.18.1", + "express": "^4.21.2", "puppeteer": "^23.6.0", "rss-to-json": "^2.1.1", "sitemap": "^8.0.0", - "soundcloud-scraper": "^5.0.3" + "slugify": "^1.6.6", + "soundcloud-scraper": "^5.0.3", + "soundcloud.ts": "^0.6.5", + "transliteration": "^2.3.5" } } diff --git a/views/genre.ejs b/views/genre.ejs new file mode 100644 index 0000000..d7c453c --- /dev/null +++ b/views/genre.ejs @@ -0,0 +1,41 @@ + + +
+ + +