artist-dashboard/

├─ package.json

├─ server.js

├─ db.sqlite (created at runtime)

├─ uploads/            (audio files saved here)

├─ public/

│  ├─ index.html       (artist dashboard)

│  ├─ admin.html       (admin preview)

│  └─ app.js           (frontend JS)

│  └─ styles.css

{

"name": "artist-dashboard",

"version": "1.0.0",

"main": "server.js",

"scripts": {

"start": "node server.js"

},

"dependencies": {

"better-sqlite3": "^8.0.0",

"cors": "^2.8.5",

"express": "^4.18.2",

"express-fileupload": "^1.4.0"

}

}

 

// server.js

const express = require('express');

const fileUpload = require('express-fileupload');

const Database = require('better-sqlite3');

const path = require('path');

const fs = require('fs');

const cors = require('cors');

 

const PORT = process.env.PORT || 3000;

const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'changeme_admin_token';

 

const app = express();

app.use(cors());

app.use(express.json());

app.use(fileUpload());

app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

app.use(express.static(path.join(__dirname, 'public')));

 

// ensure uploads dir

if (!fs.existsSync(path.join(__dirname, 'uploads'))) {

fs.mkdirSync(path.join(__dirname, 'uploads'));

}

 

// initialize DB

const db = new Database(path.join(__dirname, 'db.sqlite'));

db.exec(`

CREATE TABLE IF NOT EXISTS artists (

id INTEGER PRIMARY KEY AUTOINCREMENT,

name TEXT NOT NULL,

email TEXT

);

CREATE TABLE IF NOT EXISTS tracks (

id INTEGER PRIMARY KEY AUTOINCREMENT,

artist_id INTEGER,

title TEXT,

filename TEXT,

uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,

approved INTEGER DEFAULT 0,

plays INTEGER DEFAULT 0,

royalty_rate REAL DEFAULT 0.003, -- example cents per play or $ per play simplified

FOREIGN KEY(artist_id) REFERENCES artists(id)

);

CREATE TABLE IF NOT EXISTS withdrawals (

id INTEGER PRIMARY KEY AUTOINCREMENT,

artist_id INTEGER,

amount REAL,

status TEXT DEFAULT 'pending',

requested_at DATETIME DEFAULT CURRENT_TIMESTAMP,

FOREIGN KEY(artist_id) REFERENCES artists(id)

);

`);

 

// helper queries

const insertArtist = db.prepare('INSERT INTO artists(name,email) VALUES(?,?)');

const getArtistById = db.prepare('SELECT * FROM artists WHERE id = ?');

const insertTrack = db.prepare('INSERT INTO tracks(artist_id,title,filename,royalty_rate) VALUES(?,?,?,?)');

const listTracks = db.prepare('SELECT * FROM tracks WHERE artist_id = ? ORDER BY uploaded_at DESC');

const getTrackById = db.prepare('SELECT * FROM tracks WHERE id = ?');

const updateTrackPlays = db.prepare('UPDATE tracks SET plays = plays + ? WHERE id = ?');

const listWithdrawalsByArtist = db.prepare('SELECT * FROM withdrawals WHERE artist_id = ? ORDER BY requested_at DESC');

const insertWithdrawal = db.prepare('INSERT INTO withdrawals(artist_id,amount) VALUES(?,?)');

const adminListTracks = db.prepare('SELECT * FROM tracks ORDER BY uploaded_at DESC');

const adminListWithdrawals = db.prepare('SELECT * FROM withdrawals ORDER BY requested_at DESC');

const approveTrack = db.prepare('UPDATE tracks SET approved = 1 WHERE id = ?');

 

// Seed a demo artist if no artists exist

const countArtists = db.prepare('SELECT COUNT(*) as c FROM artists').get().c;

let demoArtistId;

if (countArtists === 0) {

const res = insertArtist.run('Demo Artist', 'demo@example.com');

demoArtistId = res.lastInsertRowid;

} else {

demoArtistId = db.prepare('SELECT id FROM artists LIMIT 1').get().id;

}

 

// API endpoints

 

// Get current artist (demo; in prod you'd authenticate)

app.get('/api/me', (req, res) => {

const artist = getArtistById.get(demoArtistId);

res.json({ artist });

});

 

// Upload track

app.post('/api/upload', async (req, res) => {

try {

const { title = 'Untitled', artist_id } = req.body;

const artistId = artist_id ? Number(artist_id) : demoArtistId;

if (!req.files || !req.files.track) return res.status(400).json({ error: 'Missing file "track"' });

const trackFile = req.files.track;

// only allow common audio extensions

const allowed = ['.mp3', '.wav', '.m4a', '.ogg', '.flac'];

const ext = path.extname(trackFile.name).toLowerCase();

if (!allowed.includes(ext)) return res.status(400).json({ error: 'Unsupported file type' });

 

const filename = `${Date.now()}-${trackFile.name.replace(/\s+/g,'_')}`;

const filepath = path.join(__dirname, 'uploads', filename);

await trackFile.mv(filepath);

const info = insertTrack.run(artistId, title, `/uploads/${filename}`, 0.003);

res.json({ success: true, trackId: info.lastInsertRowid });

} catch (err) {

console.error(err);

res.status(500).json({ error: 'upload failed' });

}

});

 

// List tracks for artist

app.get('/api/tracks/:artistId', (req, res) => {

const artistId = Number(req.params.artistId || demoArtistId);

const tracks = listTracks.all(artistId);

res.json({ tracks });

});

 

// Increment plays (called by player)

app.post('/api/plays/:trackId', (req, res) => {

const trackId = Number(req.params.trackId);

const inc = Number(req.body.inc || 1);

updateTrackPlays.run(inc, trackId);

res.json({ success: true });

});

 

// Royalties summary

app.get('/api/royalties/:artistId', (req, res) => {

const artistId = Number(req.params.artistId || demoArtistId);

const rows = listTracks.all(artistId);

// simple royalty = plays * royalty_rate

const royalties = rows.map(r => ({ id: r.id, title: r.title, plays: r.plays, rate: r.royalty_rate, amount: (r.plays * r.royalty_rate) }));

const total = royalties.reduce((s,x)=>s + x.amount, 0);

res.json({ royalties, total });

});

 

// Withdraw request

app.post('/api/withdraw', (req, res) => {

const artistId = Number(req.body.artistId || demoArtistId);

const amount = Number(req.body.amount || 0);

if (amount <= 0) return res.status(400).json({ error: 'Invalid amount' });

insertWithdrawal.run(artistId, amount);

res.json({ success: true });

});

 

// List withdrawals for artist

app.get('/api/withdrawals/:artistId', (req, res) => {

const artistId = Number(req.params.artistId || demoArtistId);

const rows = listWithdrawalsByArtist.all(artistId);

res.json({ withdrawals: rows });

});

 

// Distribution platforms list (static)

app.get('/api/platforms', (req, res) => {

const platforms = [

{ id: 'apple', name: 'Apple Music' },

{ id: 'spotify', name: 'Spotify' },

{ id: 'beatport', name: 'Beatport' },

{ id: 'tidal', name: 'Tidal' },

{ id: 'deezer', name: 'Deezer' },

{ id: 'amazon', name: 'Amazon Music' }

];

res.json({ platforms });

});

 

// Admin - simple token-protected endpoints

function requireAdmin(req, res, next) {

const token = req.headers['x-admin-token'] || req.query.token;

if (token !== ADMIN_TOKEN) return res.status(401).json({ error: 'unauthorized' });

next();

}

 

app.get('/api/admin/tracks', requireAdmin, (req,res) => {

res.json({ tracks: adminListTracks.all() });

});

app.post('/api/admin/approve/:trackId', requireAdmin, (req,res) => {

const id = Number(req.params.trackId);

approveTrack.run(id);

res.json({ success: true });

});

app.get('/api/admin/withdrawals', requireAdmin, (req,res) => {

res.json({ withdrawals: adminListWithdrawals.all() });

});

 

// fallback to index.html

app.get('*', (req,res) => {

if (req.path.startsWith('/api/')) return res.status(404).json({ error: 'not found' });

res.sendFile(path.join(__dirname, 'public', 'index.html'));

});

 

app.listen(PORT, () => {

console.log(`Artist Dashboard running on http://localhost:${PORT}  (Admin token: ${ADMIN_TOKEN})`);

}); <!doctype html>

<html>

<head>

<meta charset="utf-8"/>

<title>Artist Dashboard — Demo</title>

<meta name="viewport" content="width=device-width,initial-scale=1" />

<link rel="stylesheet" href="/styles.css" />

</head>

<body>

<div class="container">

<header><h1>Artist Dashboard (Demo)</h1></header>

 

<section id="artist-info">

<h2>Artist</h2>

<div id="artistName">Loading...</div>

</section>

 

<section id="upload">

<h2>Upload Track</h2>

<form id="uploadForm">

<input type="text" name="title" placeholder="Track title" required />

<input type="file" name="track" accept="audio/*" required />

<button type="submit">Upload</button>

</form>

<div id="uploadMsg"></div>

</section>

 

<section id="platforms">

<h2>Distribution Platforms</h2>

<ul id="platformList"></ul>

</section>

 

<section id="tracks">

<h2>Your Tracks</h2>

<div id="tracksList">Loading tracks...</div>

</section>

 

<section id="royalties">

<h2>Royalties</h2>

<div id="royaltiesData">Loading...</div>

<form id="withdrawForm">

<input type="number" name="amount" step="0.01" placeholder="Amount to withdraw" required />

<button type="submit">Request Withdraw</button>

</form>

<div id="withdrawMsg"></div>

<h3>Withdrawal History</h3>

<div id="withdrawals">Loading...</div>

</section>

 

<footer>

<small>Admin preview: <a href="/admin.html" target="_blank">Open admin</a></small>

</footer>

</div>

 

<script src="/app.js"></script>

</body>

</html>

 

<!doctype html>

<html>

<head>

<meta charset="utf-8"/>

<title>Admin Preview</title>

<meta name="viewport" content="width=device-width,initial-scale=1" />

<link rel="stylesheet" href="/styles.css" />

</head>

<body>

<div class="container">

<header><h1>Admin Preview</h1></header>

<p>Enter admin token (from your env ADMIN_TOKEN) to view / approve.</p>

<input id="token" placeholder="Admin token" />

<button id="load">Load</button>

<h2>Uploaded Tracks</h2>

<div id="adminTracks">Not loaded</div>

 

<h2>Withdrawals</h2>

<div id="adminWithdrawals">Not loaded</div>

</div>

<script>

document.getElementById('load').addEventListener('click', async () => {

const token = document.getElementById('token').value;

if (!token) return alert('enter token');

const headers = { 'x-admin-token': token };

const t = await fetch('/api/admin/tracks', { headers }).then(r => r.json());

document.getElementById('adminTracks').innerHTML = t.tracks.map(tr => {

return `<div class="track">

<b>${tr.title}</b> — <a href="${tr.filename}" target="_blank">file</a>

<div>Approved: ${tr.approved}</div>

<button onclick="approve(${tr.id})">Approve</button>

</div>`;

}).join('');

const w = await fetch('/api/admin/withdrawals', { headers }).then(r => r.json());

document.getElementById('adminWithdrawals').innerHTML = w.withdrawals.map(wd => `<div>${wd.id} — artist ${wd.artist_id} — $${wd.amount} — ${wd.status}</div>`).join('');

window.approve = async (id) => {

await fetch('/api/admin/approve/' + id, { method: 'POST', headers });

alert('approved');

};

});

</script>

</body>

</html> // public/app.js

(async function(){

const me = await (await fetch('/api/me')).json();

const artist = me.artist;

const artistId = artist.id;

document.getElementById('artistName').innerText = `${artist.name} (${artist.email})`;

 

// platforms

const platforms = await (await fetch('/api/platforms')).json();

document.getElementById('platformList').innerHTML = platforms.platforms.map(p => `<li>${p.name}</li>`).join('');

 

// upload

document.getElementById('uploadForm').addEventListener('submit', async (e) => {

e.preventDefault();

const form = e.target;

const data = new FormData(form);

data.set('artist_id', artistId);

const res = await fetch('/api/upload', { method: 'POST', body: data });

const j = await res.json();

document.getElementById('uploadMsg').innerText = j.error ? 'Upload failed: ' + j.error : 'Uploaded!';

await refreshTracks();

});

 

async function refreshTracks() {

const res = await fetch(`/api/tracks/${artistId}`);

const data = await res.json();

const list = data.tracks;

if (!list.length) {

document.getElementById('tracksList').innerHTML = '<p>No tracks yet.</p>';

return;

}

const html = list.map(tr => {

return `<div class="track">

<div><strong>${tr.title}</strong> ${tr.approved ? '(approved)' : '(pending)'}</div>

<audio data-id="${tr.id}" src="${tr.filename}" controls preload="none"></audio>

<div>Plays: <span id="plays-${tr.id}">${tr.plays}</span></div>

<button onclick="increasePlay(${tr.id})">Simulate Play +1</button>

</div>`;

}).join('');

document.getElementById('tracksList').innerHTML = html;

 

// attach player loop toggle UI below the first player (global)

attachPlayerControls();

}

 

window.increasePlay = async function(trackId) {

await fetch('/api/plays/' + trackId, { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify({ inc: 1 })});

const resp = await fetch(`/api/tracks/${artistId}`);

const t = (await resp.json()).tracks.find(x => x.id === trackId);

document.getElementById('plays-' + trackId).innerText = t.plays;

await refreshRoyalties();

};

 

// royalties

async function refreshRoyalties(){

const r = await (await fetch(`/api/royalties/${artistId}`)).json();

const rows = r.royalties;

const total = r.total.toFixed(2);

document.getElementById('royaltiesData').innerHTML = `<div>Total: $${total}</div>` + rows.map(x => `<div>${x.title} — plays ${x.plays} — $${x.amount.toFixed(2)}</div>`).join('');

}

await refreshTracks();

await refreshRoyalties();

 

// withdrawals

document.getElementById('withdrawForm').addEventListener('submit', async (e) => {

e.preventDefault();

const amount = e.target.amount.value;

const res = await fetch('/api/withdraw', { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify({ artistId, amount })});

const j = await res.json();

document.getElementById('withdrawMsg').innerText = j.error ? 'Error: ' + j.error : 'Withdrawal requested';

await loadWithdrawals();

});

 

async function loadWithdrawals(){

const r = await (await fetch(`/api/withdrawals/${artistId}`)).json();

document.getElementById('withdrawals').innerHTML = r.withdrawals.map(w => `<div>${w.id} — $${w.amount} — ${w.status} — ${w.requested_at}</div>`).join('');

}

await loadWithdrawals();

 

// Player loop toggle controls (global)

function attachPlayerControls(){

// if already exists, skip

if (document.getElementById('playerControls')) return;

const div = document.createElement('div');

div.id = 'playerControls';

div.innerHTML = `

<h3>Playlist Player</h3>

<label><input type="checkbox" id="loopToggle"> Loop playlist</label>

<button id="playAll">Play all</button>

<button id="stopAll">Stop</button>

`;

document.getElementById('tracks').appendChild(div);

const loopToggle = document.getElementById('loopToggle');

const playAll = document.getElementById('playAll');

const stopAll = document.getElementById('stopAll');

 

let playlist = Array.from(document.querySelectorAll('#tracks audio'));

let currentIndex = 0;

let playing = false;

 

function playIndex(i){

playlist = Array.from(document.querySelectorAll('#tracks audio'));

if (i < 0 || i >= playlist.length) {

if (loopToggle.checked) { currentIndex = 0; playIndex(0); }

else { playing = false; return; }

} else {

currentIndex = i;

playlist.forEach((a, idx) => { if (idx !== i) { a.pause(); a.currentTime = 0; }});

const audio = playlist[i];

// send play count increment when ended or maybe when play starts? we'll increment on ended for realism

audio.play();

playing = true;

audio.onended = async () => {

// increment play count in DB (server)

const id = Number(audio.dataset.id);

await fetch('/api/plays/' + id, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ inc: 1 })});

// update UI plays count

const resp = await fetch(`/api/tracks/${artistId}`);

const data = await resp.json();

const t = data.tracks.find(x => x.id === id);

if (t) document.getElementById('plays-' + id).innerText = t.plays;

// next

playIndex(i + 1);

};

}

}

 

playAll.addEventListener('click', () => {

playlist = Array.from(document.querySelectorAll('#tracks audio'));

if (!playlist.length) return alert('No tracks');

playIndex(0);

});

stopAll.addEventListener('click', () => {

playlist.forEach(a => { a.pause(); a.currentTime = 0; });

playing = false;

});

}

 

})(); /* public/styles.css */

body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial; margin:0; padding:20px; background:#f7f7f8; color:#111; }

.container { max-width:900px; margin:0 auto; background:white; padding:20px; border-radius:8px; box-shadow:0 6px 18px rgba(0,0,0,0.06); }

header { border-bottom:1px solid #eee; padding-bottom:10px; margin-bottom:12px; }

section { margin-bottom:18px; }

.track { padding:8px 0; border-bottom:1px dashed #eee; }

button { padding:8px 12px; border-radius:6px; border:1px solid #ddd; background:#fff; cursor:pointer; }

input[type="text"], input[type="number"] { padding:8px; border-radius:6px; border:1px solid #ddd; width:200px; }

audio { width:100%; max-width:420px; display:block; margin-top:6px; }

footer { margin-top:20px; color:#666; font-size:13px; }