moving to soundcloud.ts

This commit is contained in:
2025-06-22 02:13:47 -04:00
parent 3292de107a
commit 2f55294c3d
5 changed files with 292 additions and 32 deletions

108
manual.js Normal file
View File

@ -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);
});

View File

@ -1,9 +1,9 @@
const express = require('express'); const express = require('express');
const app = express(); const app = express();
const path = require('path'); const path = require('path');
const SoundCloud = require('soundcloud-scraper'); const Soundcloud = require('soundcloud.ts').default;
const client = new SoundCloud.Client();
const fs = require('fs'); const fs = require('fs');
const slugify = require('slugify');
const PORT = process.env.PORT || 6767; const PORT = process.env.PORT || 6767;
// Define genre playlists // Define genre playlists
@ -27,43 +27,137 @@ const playlists = {
url: 'https://soundcloud.com/snxraven/sets/raven-scott-lofi', url: 'https://soundcloud.com/snxraven/sets/raven-scott-lofi',
cacheFile: path.join(__dirname, 'cache_lofi.json'), cacheFile: path.join(__dirname, 'cache_lofi.json'),
tracks: [] 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 // Helper function to create a slug from track title
function generateSlug(title) { function generateSlug(title) {
return title try {
.toLowerCase() const slug = slugify(title, {
.replace(/[^a-z0-9]+/g, '-') lower: true,
.replace(/(^-|-$)/g, ''); 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 // Helper function to read the cache
function readCache(cacheFile) { function readCache(cacheFile) {
if (fs.existsSync(cacheFile)) { if (fs.existsSync(cacheFile)) {
const data = fs.readFileSync(cacheFile, 'utf8'); try {
return JSON.parse(data); 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; return null;
} }
// Helper function to save cache // Helper function to save cache
function saveCache(cacheFile, data) { 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 // Fetch playlist tracks from SoundCloud
async function fetchPlaylist(playlistUrl) { async function fetchPlaylist(playlistUrl) {
const playlist = await client.getPlaylist(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));
return playlist.tracks.map(track => ({ const tracks = [];
title: track.title, for (const track of playlist.tracks) {
description: track.description || 'No description available', try {
url: track.url, console.log(`Processing track: "${track.title || 'Unknown'}"`);
playCount: track.playCount || 0, const trackData = sanitizeTrackData(track);
publishedAt: track.publishedAt || new Date().toISOString(), console.log(`Generated embed URL: "${trackData.embedUrl}"`);
slug: generateSlug(track.title) 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 // Get tracks for a specific genre
@ -77,17 +171,32 @@ async function getTracks(genre, fetch = false) {
playlist.tracks = await fetchPlaylist(playlist.url); playlist.tracks = await fetchPlaylist(playlist.url);
saveCache(playlist.cacheFile, { tracks: playlist.tracks, timestamp: now }); saveCache(playlist.cacheFile, { tracks: playlist.tracks, timestamp: now });
} else { } else {
playlist.tracks = cache.tracks.map(track => ({ playlist.tracks = cache.tracks.map(track => sanitizeTrackData({
...track, ...track,
slug: track.slug || generateSlug(track.title) slug: track.slug || generateSlug(track.title)
})); }));
} }
// Sort by playCount first, then by publishedAt // Sort tracks
playlist.tracks.sort((a, b) => { if (genre === 'cuts') {
if (b.playCount !== a.playCount) return b.playCount - a.playCount; // Sort "cuts" playlist by EP number extracted from title
return new Date(b.publishedAt) - new Date(a.publishedAt); 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; return playlist.tracks;
} }
@ -130,7 +239,6 @@ app.get('/sitemap.xml', async (req, res) => {
res.send(sitemap); res.send(sitemap);
}); });
// Redirect /genre to /#genre // Redirect /genre to /#genre
app.get('/:genre', async (req, res) => { app.get('/:genre', async (req, res) => {
const { genre } = req.params; const { genre } = req.params;
@ -168,7 +276,6 @@ app.get('/json/:genre', async (req, res) => {
res.json(tracks); res.json(tracks);
}); });
// Listen on the specified port // Listen on the specified port
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`); console.log(`Server is running on port ${PORT}`);

View File

@ -9,11 +9,15 @@
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
"cheerio": "^1.0.0-rc.10", "cheerio": "^1.0.0-rc.10",
"compression": "^1.8.0",
"ejs": "^3.1.8", "ejs": "^3.1.8",
"express": "^4.18.1", "express": "^4.21.2",
"puppeteer": "^23.6.0", "puppeteer": "^23.6.0",
"rss-to-json": "^2.1.1", "rss-to-json": "^2.1.1",
"sitemap": "^8.0.0", "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"
} }
} }

41
views/genre.ejs Normal file
View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= genre.charAt(0).toUpperCase() + genre.slice(1) %> Tracks - Raven Scott Music</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f4f4f4; }
h1 { text-align: center; }
.track { margin: 10px 0; }
.track-player { width: 100%; max-width: 600px; }
.track-player img { width: 100%; height: 166px; object-fit: cover; }
.back-link { display: block; text-align: center; margin: 10px 0; color: #007bff; }
</style>
</head>
<body>
<h1><%= genre.charAt(0).toUpperCase() + genre.slice(1) %> Tracks</h1>
<% tracks.forEach(track => { %>
<div class="track">
<h3><%= track.title %></h3>
<div class="track-player" data-url="<%= track.url %>">
<img src="/placeholder.jpg" alt="<%= track.title %>">
</div>
</div>
<% }); %>
<a class="back-link" href="/">Back to Home</a>
<script>
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const container = entry.target;
const url = container.dataset.url;
container.innerHTML = `<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay" src="https://w.soundcloud.com/player/?url=${encodeURIComponent(url)}&show_comments=false&show_teaser=false&visual=false"></iframe>`;
observer.unobserve(container);
}
});
}, { rootMargin: '100px' });
document.querySelectorAll('.track-player').forEach(player => observer.observe(player));
</script>
</body>
</html>

View File

@ -127,7 +127,7 @@
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark fixed-top"> <nav class="navbar navbar-expand-lg navbar-dark fixed-top">
<a class="navbar-brand" href="https://raven-scott.fyi">Raven Scott Music</a> <a class="navbar-brand" href="https://raven-scott.rocks">Raven Scott Music</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>