first commit
This commit is contained in:
commit
3a5d51c915
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
cache.json
|
||||||
|
package-lock.json
|
148
README.md
Normal file
148
README.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# Raven Scott Metal Site
|
||||||
|
|
||||||
|
Welcome to the **Raven Scott Metal Site**, a dynamic and metal-inspired web application that lists and showcases SoundCloud tracks from the playlist "Raven Scott Metal." The site is designed using **Bootstrap 4** and features a dark, edgy design tailored to a metal aesthetic. Each track is displayed with a SoundCloud player, with links to detailed pages for each individual track.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Features](#features)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [SoundCloud Playlist](#soundcloud-playlist)
|
||||||
|
- [Caching](#caching)
|
||||||
|
- [Routes](#routes)
|
||||||
|
- [Technologies Used](#technologies-used)
|
||||||
|
- [Known Issues](#known-issues)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Displays all tracks from the "Raven Scott Metal" SoundCloud playlist.
|
||||||
|
- Dynamic slug-based routes for each track.
|
||||||
|
- Sorts tracks by the number of plays, followed by publication date.
|
||||||
|
- Embeds SoundCloud players for each track.
|
||||||
|
- Caching system to prevent fetching the playlist too frequently.
|
||||||
|
- Bootstrap 4 responsive design with a dark metal theme.
|
||||||
|
- Fallback message if a track doesn't have a description.
|
||||||
|
- Easy-to-use interface with individual pages for each track.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To get started with the Raven Scott Metal site locally, follow these steps:
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-username/raven-scott-metal-site.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Navigate to the project directory:
|
||||||
|
```bash
|
||||||
|
cd raven-scott-metal-site
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install the dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create a cache file for storing track information:
|
||||||
|
```bash
|
||||||
|
touch cache.json
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Run the application:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Open your browser and navigate to `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Fetching Playlist Data
|
||||||
|
The site uses the **SoundCloud Scraper** to fetch playlist data. By default, the playlist is only fetched once a week and cached in `cache.json`. You can force a fresh fetch using the `--fetch` flag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start -- --fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Viewing Tracks
|
||||||
|
The homepage lists all tracks from the "Raven Scott Metal" playlist. Each track has its own card displaying the following information:
|
||||||
|
- Title of the track
|
||||||
|
- Short description (or a fallback message if no description is available)
|
||||||
|
- Embedded SoundCloud player
|
||||||
|
|
||||||
|
### Track Pages
|
||||||
|
Each track has a dedicated page with its own URL based on a slug generated from the track title. The URL format is:
|
||||||
|
```
|
||||||
|
http://localhost:3000/tracks/<track-slug>
|
||||||
|
```
|
||||||
|
|
||||||
|
On each track page, you'll find:
|
||||||
|
- The track title
|
||||||
|
- The full description of the track (if available), rendered with line breaks
|
||||||
|
- An embedded SoundCloud player to listen to the track
|
||||||
|
|
||||||
|
## SoundCloud Playlist
|
||||||
|
|
||||||
|
The site fetches tracks from the following SoundCloud playlist:
|
||||||
|
- **Playlist URL:** [Raven Scott Metal](https://soundcloud.com/snxraven/sets/raven-scott-metal)
|
||||||
|
|
||||||
|
Each track is fetched and displayed in order of play count (most played first), and then by the publication date.
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
The site uses a caching mechanism to reduce the number of requests to the SoundCloud API. The cache stores:
|
||||||
|
- A list of tracks from the SoundCloud playlist
|
||||||
|
- The timestamp of the last time the playlist was fetched
|
||||||
|
|
||||||
|
By default, the cache is valid for 7 days. After that, the playlist will be fetched again. If you want to force a refresh of the playlist before the cache expires, run the app with the `--fetch` flag.
|
||||||
|
|
||||||
|
### Cache Structure
|
||||||
|
The cache is stored in `cache.json` and looks like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tracks": [
|
||||||
|
{
|
||||||
|
"title": "Ebon Shroud",
|
||||||
|
"description": "A chilling, shadowy realm...",
|
||||||
|
"url": "https://soundcloud.com/snxraven/ebon-shroud",
|
||||||
|
"playCount": 323,
|
||||||
|
"publishedAt": "2024-07-31T07:26:51.000Z",
|
||||||
|
"slug": "ebon-shroud"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"timestamp": 1729150557891
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
### Home Page
|
||||||
|
- **URL:** `/`
|
||||||
|
- **Description:** Lists all tracks from the "Raven Scott Metal" playlist. Tracks are sorted by play count and then by publication date. Each track is displayed in a card with a link to the track page.
|
||||||
|
|
||||||
|
### Track Pages
|
||||||
|
- **URL Format:** `/tracks/<track-slug>`
|
||||||
|
- **Description:** Displays detailed information about a specific track, including its full description and an embedded SoundCloud player.
|
||||||
|
|
||||||
|
### Example Track URL
|
||||||
|
```
|
||||||
|
http://localhost:3000/tracks/ebon-shroud
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Node.js**: JavaScript runtime for the server.
|
||||||
|
- **Express.js**: Web framework for building the server-side application.
|
||||||
|
- **SoundCloud Scraper**: Library for fetching SoundCloud playlist and track data.
|
||||||
|
- **EJS**: Templating engine for rendering HTML views.
|
||||||
|
- **Bootstrap 4**: Frontend CSS framework for responsive design.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Bootstrap 4**: Used for styling the site with a responsive, mobile-first design.
|
||||||
|
- **Custom Styles**: Added custom styles for a dark metal theme.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
- SoundCloud API rate limits may affect data fetching if too many requests are made in a short period.
|
||||||
|
- If a track doesn't have a description, the fallback message is displayed. However, some descriptions may not render properly if they contain unusual formatting.
|
111
app.js
Normal file
111
app.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const path = require('path');
|
||||||
|
const SoundCloud = require('soundcloud-scraper');
|
||||||
|
const client = new SoundCloud.Client();
|
||||||
|
const fs = require('fs');
|
||||||
|
const PORT = process.env.PORT || 6767;
|
||||||
|
|
||||||
|
let tracks = []; // Store the tracks globally
|
||||||
|
const CACHE_FILE = path.join(__dirname, 'cache.json');
|
||||||
|
|
||||||
|
// Helper function to create a slug from track title
|
||||||
|
function generateSlug(title) {
|
||||||
|
return title
|
||||||
|
.toLowerCase() // Ensure lowercase
|
||||||
|
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric characters with hyphens
|
||||||
|
.replace(/(^-|-$)/g, ''); // Remove leading and trailing hyphens
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to read the cache
|
||||||
|
function readCache() {
|
||||||
|
if (fs.existsSync(CACHE_FILE)) {
|
||||||
|
const data = fs.readFileSync(CACHE_FILE, 'utf8');
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to save cache
|
||||||
|
function saveCache(data) {
|
||||||
|
fs.writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch playlist tracks from SoundCloud
|
||||||
|
async function fetchPlaylist() {
|
||||||
|
const playlist = await client.getPlaylist('https://soundcloud.com/snxraven/sets/raven-scott-metal');
|
||||||
|
|
||||||
|
return playlist.tracks.map(track => {
|
||||||
|
const slug = generateSlug(track.title); // Generate slug here
|
||||||
|
return {
|
||||||
|
title: track.title,
|
||||||
|
description: track.description || 'No description available',
|
||||||
|
url: track.url,
|
||||||
|
playCount: track.playCount || 0,
|
||||||
|
publishedAt: track.publishedAt || new Date().toISOString(),
|
||||||
|
slug // Save the slug in the track object
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tracks from cache or SoundCloud
|
||||||
|
async function getTracks(fetch = false) {
|
||||||
|
const cache = readCache();
|
||||||
|
const oneWeekInMs = 7 * 24 * 60 * 60 * 1000; // One week in milliseconds
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (fetch || !cache || (now - cache.timestamp) > oneWeekInMs) {
|
||||||
|
// Fetch fresh tracks from SoundCloud
|
||||||
|
tracks = await fetchPlaylist();
|
||||||
|
saveCache({ tracks, timestamp: now });
|
||||||
|
} else {
|
||||||
|
// Load from cache
|
||||||
|
tracks = cache.tracks;
|
||||||
|
|
||||||
|
// Ensure slug generation in case it is missing from the cached data
|
||||||
|
tracks = tracks.map(track => {
|
||||||
|
if (!track.slug) {
|
||||||
|
track.slug = generateSlug(track.title);
|
||||||
|
}
|
||||||
|
return track;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by playCount first, then by publishedAt
|
||||||
|
tracks.sort((a, b) => {
|
||||||
|
if (b.playCount !== a.playCount) return b.playCount - a.playCount;
|
||||||
|
return new Date(b.publishedAt) - new Date(a.publishedAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
return 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 allTracks = await getTracks();
|
||||||
|
res.render('index', { tracks: allTracks });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Individual track page route
|
||||||
|
app.get('/track/:slug', async (req, res) => {
|
||||||
|
const allTracks = await getTracks();
|
||||||
|
const track = allTracks.find(t => t.slug === req.params.slug);
|
||||||
|
|
||||||
|
if (!track) {
|
||||||
|
return res.status(404).send('Track not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('track', { track });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen on the specified port
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server is running on port ${PORT}`);
|
||||||
|
});
|
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "raven-scott-metal-website",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Metal themed website for Raven Scott's music.",
|
||||||
|
"main": "app.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node app.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"cheerio": "^1.0.0-rc.10",
|
||||||
|
"ejs": "^3.1.8",
|
||||||
|
"express": "^4.18.1",
|
||||||
|
"puppeteer": "^23.6.0",
|
||||||
|
"rss-to-json": "^2.1.1",
|
||||||
|
"sitemap": "^8.0.0",
|
||||||
|
"soundcloud-scraper": "^5.0.3"
|
||||||
|
}
|
||||||
|
}
|
50
views/index.ejs
Normal file
50
views/index.ejs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Raven Scott Metal Playlist</title> <!-- General title for the page -->
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Metal Mania', sans-serif;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #ff5500;
|
||||||
|
border-color: #ff5500;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #ff3300;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<h1 class="text-center mb-5">Raven Scott Metal Tracks</h1>
|
||||||
|
<div class="row">
|
||||||
|
<% tracks.forEach(track => { %>
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><%= track.title %></h5>
|
||||||
|
<p class="card-text"></p>
|
||||||
|
|
||||||
|
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay"
|
||||||
|
src="https://w.soundcloud.com/player/?url=<%= track.url %>&color=%23ff5500&auto_play=false&show_artwork=true" loading="lazy">
|
||||||
|
</iframe>
|
||||||
|
<a href="/track/<%= track.slug %>" class="btn btn-primary mt-3">More Details</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
20
views/layout.ejs
Normal file
20
views/layout.ejs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title || 'Raven Scott Metal' %></title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1><%= title || 'Raven Scott Metal' %></h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<%- include('content') %> <!-- This includes the body content -->
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>© 2024 Raven Scott</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
37
views/track.ejs
Normal file
37
views/track.ejs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= track.title %></title>
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Metal Mania', sans-serif;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="card-title"><%= track.title %></h1>
|
||||||
|
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay"
|
||||||
|
src="https://w.soundcloud.com/player/?url=<%= track.url %>&color=%23ff5500&auto_play=false&show_artwork=true">
|
||||||
|
</iframe>
|
||||||
|
<p class="card-text">
|
||||||
|
<%- (track.description && track.description.trim())
|
||||||
|
? track.description.replace(/\n/g, '<br>')
|
||||||
|
: 'No description available for this track.' %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user