Files
ravenscott-rocks/music_site.js
2025-06-22 02:13:47 -04:00

282 lines
9.9 KiB
JavaScript

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 PORT = process.env.PORT || 6767;
// Define genre playlists
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: []
}
};
// 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) {
// 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)) {
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 {
// 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) {
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
async function getTracks(genre, fetch = false) {
const playlist = playlists[genre];
const cache = readCache(playlist.cacheFile);
const oneWeekInMs = 7 * 24 * 60 * 60 * 1000;
const now = Date.now();
if (fetch || !cache || (now - cache.timestamp) > oneWeekInMs) {
playlist.tracks = await fetchPlaylist(playlist.url);
saveCache(playlist.cacheFile, { tracks: playlist.tracks, timestamp: now });
} else {
playlist.tracks = cache.tracks.map(track => sanitizeTrackData({
...track,
slug: track.slug || generateSlug(track.title)
}));
}
// 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;
}
// 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);
}
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 => {
sitemap += `<url>\n <loc>https://raven-scott.rocks/${genre}/track/${track.slug}</loc>\n <priority>0.8</priority>\n</url>\n`;
});
}
sitemap += `</urlset>`;
res.header('Content-Type', 'application/xml');
res.send(sitemap);
});
// Redirect /genre to /#genre
app.get('/:genre', async (req, res) => {
const { genre } = req.params;
if (playlists[genre]) {
res.redirect(`/#${genre}`);
} else {
res.status(404).send('Genre not found');
}
});
// Individual track page route
app.get('/:genre/track/:slug', async (req, res) => {
const { genre, slug } = req.params;
if (!playlists[genre]) {
return res.status(404).send('Genre not found');
}
const tracks = await getTracks(genre);
const track = tracks.find(t => t.slug === slug);
if (!track) {
return res.status(404).send('Track not found');
}
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);
});
// Listen on the specified port
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});