discord-media-server
Version:
Self-hosted Discord bot for scanning and serving information on local media files.
421 lines (331 loc) • 12.3 kB
JavaScript
// server.js (ES module version with database.js integration)
import express from 'express';
import path from 'path';
import fs from 'fs';
import { getBotStatus } from './botStatus.js';
import { resetMovieTable } from './reset.js';
import { CONFIG_DIR, DATA_DIR } from './setupConfig.js';
import dotenv from 'dotenv';
dotenv.config();
let botRunning = false;
let botInstance = null;
const MEDIA_DIR = process.env.MEDIA_DIR;
const DB_NAME = process.env.DB_NAME || 'media';
if (!MEDIA_DIR) {
console.error('MEDIA_DIR is not set in .env');
process.exit(1);
}
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
import { initDatabase, query, close } from './database.js';
await initDatabase(); // Ensure DB is connected
const app = express();
const PORT = process.env.PORT || 8080;
const DB_PATH = path.join(DATA_DIR, `${DB_NAME}.json`);
let mediaIndex = [];
function getStuff(text) {
document.getElementById('status').innerHTML = `<p>the word is ${text}</p>`;
}
function loadIndex() {
if (fs.existsSync(DB_PATH)) {
mediaIndex = JSON.parse(fs.readFileSync(DB_PATH));
console.log(`Loaded ${mediaIndex.length} entries from ${DB_NAME}.json`);
} else {
mediaIndex = [];
console.warn(`${DB_NAME}.json not found — running empty.`);
}
}
loadIndex();
// Serve static files from mediaDirectory (movies and posters)
app.use('/movies', express.static(MEDIA_DIR));
app.use(express.static(path.join(__dirname, 'public'))); // ✅ This line serves /public
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('public'));
app.get('/', (req, res) => {
// Redirect to the /setup route
//res.redirect('/setup');
const env = process.env;
let data='';
let response='';
res.send(`
<h1>Discord Media Server</h1>
<form method="POST" action="/edit">
<label><b>Discord Token:</b> ${env.DISCORD_TOKEN || ''}</label><br>
<label>TMDb API Key: ${env.TMDB_API_KEY || ''}</label><br>
<label>Media Directory: ${env.MEDIA_DIR || ''}</label><br>
<label>DB Type: ${env.DB_TYPE || ''}</label><br>
<label>DB Name: ${env.DB_NAME || ''}</label><br>
<button type="submit">Edit</button>
</form>
<form action="/launch" method="get">
<button type="submit" id='getBot' name='startBot' value=${botRunning ? 'reset' : 'start'}>${botRunning ? 'Restart Bot' : 'Turn on Bot'}</button>
</form>
<button type="submit" id='startBot' value=${botRunning ? 'reset' : 'start'}>${botRunning ? 'Restart' : 'Off'}</button>
<div id='status'></div>
<script>
document.getElementById('startBot').addEventListener('click', async () => {
try {
response = await fetch('/launch'); // Make an AJAX request to your Node.js endpoint
data = await response.json(); // Assuming your server sends JSON
// Update a div with new data
document.getElementById('status').innerHTML = '<p>${data.message}</p>';
// Optionally, update the button's state
document.getElementById('getBot').textContent = 'Updated!';
} catch (error) {
console.error('Error fetching data:', error);
}
});
</script>
<p id="botStatus">Checking bot status...</p>
<script>
document.getElementById('botStatus').innerText =
${botRunning} ? 'Bot is running' : 'Bot is NOT running';
</script>
`);
});
app.get('/setup', (req, res) => {
res.redirect('/');
});
app.post('/edit', (req, res) => {
const env = process.env;
res.send(`
<h1>Discord Media Server Setup</h1>
<form method="POST" action="/submit">
<label>Discord Token: <input name="discordToken" value="${env.DISCORD_TOKEN || ''}" required></label><br>
<label>TMDb API Key: <input name="tmdbKey" value="${env.TMDB_API_KEY || ''}"></label><br>
<label>Media Directory: <input name="mediaDirectory" value="${env.MEDIA_DIR || ''}" required></label><br>
<label>DB Type:
<select name="dbType">
<option value="sqlite" ${env.DB_TYPE === 'sqlite' ? 'selected' : ''}>SQLite</option>
<option value="mysql" ${env.DB_TYPE === 'mysql' ? 'selected' : ''}>MySQL</option>
</select>
</label><br>
<label>DB Name: <input name="dbName" value="${env.DB_NAME || ''}"></label><br>
<div id="mysqlFields" style="display:${env.DB_TYPE === 'mysql' ? 'block' : 'none'};">
<label>DB Host: <input name="dbHost" value="${env.DB_HOST || ''}"></label><br>
<label>DB User: <input name="dbUser" value="${env.DB_USER || ''}"></label><br>
<label>DB Pass: <input name="dbPass" value="${env.DB_PASS || ''}"></label><br>
</div>
<button type="submit">Save</button>
</form>
<form action="/launch" method="post">
<button type="submit">Start Server & Bot</button>
</form>
<script>
const dbTypeSelect = document.querySelector('select[name="dbType"]');
const mysqlFields = document.getElementById('mysqlFields');
dbTypeSelect.addEventListener('change', () => {
mysqlFields.style.display = dbTypeSelect.value === 'mysql' ? 'block' : 'none';
});
</script>
`);
});
app.get('/reset-db', async (req, res) => {
try {
await resetMovieTable();
res.status(200).send('Movie_Info table reset successfully');
} catch (err) {
res.status(500).send('Error resetting DB: ' + err.message);
}
});
app.get('/bot-status', (req, res) => {
const status = getBotStatus();
res.json({ running: status });
});
app.get('/dashboard', (req, res) => {
res.sendFile(path.join(__dirname, '../public', 'dashboard.html'));
});
// --- Web UI ---
app.get('/dashboard/media', (req, res) => {
const recent = [...mediaIndex]
.sort((a, b) => new Date(b.added) - new Date(a.added))
.slice(0, 20);
//res.render('media-dashboard', { media: recent, total: mediaIndex.length });
res.json({ media: recent, total: mediaIndex.length });
});
app.get('/dashboard/movies', (req, res) => {
const { title, year, format, imdb } = req.query;
let results = mediaIndex;
if (title) {
results = results.filter(m =>
m.title?.toLowerCase().includes(title.toLowerCase())
);
}
if (year) {
results = results.filter(m => String(m.year) === String(year));
}
if (format) {
results = results.filter(m => m.format?.toLowerCase() === format.toLowerCase());
}
if (imdb) {
results = results.filter(m => m.imdb?.toLowerCase().includes(imdb.toLowerCase()));
}
res.json({ total: results.length, results });
});
app.get('/dashboard/search', async (req, res) => {
const { title, year, format, imdb } = req.query;
let sql = 'SELECT * FROM Movie_Info';
let conditions = [];
let params = [];
if (title) {
conditions.push('title LIKE ?');
params.push(`%${title}%`);
}
if (year) {
conditions.push('year = ?');
params.push(year);
}
if (format) {
conditions.push('format like ?');
params.push(`%${format}%`);
}
if (imdb) {
conditions.push('imdb LIKE ?');
params.push(`%${imdb}%`);
}
if (conditions.length) {
sql += ' WHERE ' + conditions.join(' AND ');
}
sql += ' ORDER BY added_at DESC LIMIT 100';
try {
const results = await query(sql, params);
res.json({ count: results.length, results });
} catch (err) {
console.error('SQL Query Failed:', err.message);
res.status(500).json({ error: 'Query failed' });
}
});
// Search movies
app.get('/api/search', async (req, res) => {
const { title = '', year = '' } = req.query;
let sql = 'SELECT * FROM Movie_Info WHERE 1=1';
const args = [];
if (title) {
sql += ' AND title LIKE ?';
args.push(`%${title}%`);
}
if (year) {
sql += ' AND year = ?';
args.push(year);
}
sql += ' ORDER BY title ASC LIMIT 25';
try {
const rows = await query(sql, args);
const results = rows.map(movie => {
const fullPosterPath = path.join(MEDIA_DIR, movie.poster || '');
const posterURL = fs.existsSync(fullPosterPath)
? `/movies/${encodeURI(movie.poster)}`
: movie.poster_fallback || null;
return {
...movie,
posterURL
};
});
res.json(results);
} catch (err) {
console.error('DB search error:', err);
res.status(500).json({ error: 'Database error' });
}
});
// Return one random movie
app.get('/api/random', async (req, res) => {
try {
const rows = await query('SELECT * FROM Movie_Info ORDER BY RANDOM() LIMIT 1');
if (!rows.length) return res.status(404).json({ error: 'No movies found' });
const movie = rows[0];
const fullPosterPath = path.join(MEDIA_DIR, movie.poster || '');
const posterURL = fs.existsSync(fullPosterPath)
? `/movies/${encodeURI(movie.poster)}`
: movie.poster_fallback || null;
res.json({ ...movie, posterURL });
} catch (err) {
console.error('DB random fetch error:', err);
res.status(500).json({ error: 'Database error' });
}
});
let cachedConfig = null;
app.get('/api/config', (req, res) => {
if (!cachedConfig) {
cachedConfig = {
TMDB_API_KEY: process.env.TMDB_API_KEY || '',
MEDIA_DIR: process.env.MEDIA_DIR || '',
DB_TYPE: process.env.DB_TYPE || '',
DB_NAME: process.env.DB_NAME || ''
};
}
res.json(cachedConfig);
});
import { spawn } from 'child_process';
app.get('/launch', async (req, res) => {
const buttonValue = req.query.startBot; // Access the value using the button's name
console.log('Button value received:', buttonValue);
//res.send(`You clicked the button with value: ${buttonValue}`);
if (botRunning) {
return res.status(200).send('Bot is already running.');
}
try {
console.log('Starting Discord bot...');
// Option 1: Run as child process
botInstance = spawn('node', ['lib/bot.js'], {
stdio: 'inherit',
env: process.env, // pass current .env variables
});
botRunning = true;
// Handle unexpected exit
botInstance.on('exit', (code) => {
console.log(`Bot process exited with code ${code}`);
botRunning = false;
});
res.json({ message: 'Bot started successfully.'});
} catch (err) {
console.error('Failed to start bot:', err);
res.status(500).send('Failed to start bot.');
}
});
// this is to edit the current values
app.post('/submit', async (req, res) => {
const envPath = path.join(CONFIG_DIR, '.env');
// 1. Load existing .env content if it exists
let existingEnv = {};
if (fs.existsSync(envPath)) {
const envLines = fs.readFileSync(envPath, 'utf-8').split('\n');
for (const line of envLines) {
const [key, ...valParts] = line.split('=');
if (key) {
existingEnv[key.trim()] = valParts.join('=').trim();
}
}
}
// 2. Build new environment variables from form
const newEnv = {
MEDIA_DIR: req.body.mediaDirectory,
DISCORD_CLIENT_ID: req.body.discordClientId,
TMDB_API_KEY: req.body.tmdbKey,
DB_TYPE: req.body.dbType,
DB_NAME: req.body.dbName
};
// 3. Merge old with new (new values overwrite old)
const mergedEnv = { ...existingEnv, ...newEnv };
// 4. Write merged values back to .env
const output = Object.entries(mergedEnv)
.map(([key, val]) => `${key}=${val}`)
.join('\n');
fs.writeFileSync(envPath, output);
console.log(`.env updated at: ${envPath}`);
cachedConfig = null;
res.redirect('/');
});
// Graceful shutdown
process.on('SIGINT', async () => {
await close();
console.log('\nServer shutting down.');
process.exit(0);
});
// Start server
app.listen(PORT, () => {
console.log(`\nMedia server running at http://localhost:${PORT}`);
console.log(`Serving media from: ${MEDIA_DIR}`);
});