UNPKG

discord-media-server

Version:

Self-hosted Discord bot for scanning and serving information on local media files.

520 lines (418 loc) 15.4 kB
// 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) => { res.redirect('/dashboard'); // 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 || ''}" type="password" required></label><br> <label>TMDb API Key: <input name="tmdbKey" value="${env.TMDB_API_KEY || ''}" type="password"></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 || ''}" type="password"></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('/done', (req, res) => { res.redirect('/'); }); 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_at) - new Date(a.added_at)) .slice(0, 20); //res.render('media-dashboard', { media: recent, total: mediaIndex.length }); res.json({ media: recent, total: mediaIndex.length }); }); app.get('/dashboard/movies', async (req, res) => { let sql = 'SELECT * FROM Movie_Info ORDER BY added_at DESC LIMIT 100'; try { const results = await query(sql); res.json({ media: results, total: results.length }); } catch (err) { console.error('SQL Query Failed:', err.message); res.status(500).json({ error: 'Query failed' }); } /* 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({ media: results, count: results.length}); } catch (err) { console.error('SQL Query Failed:', err.message); res.status(500).json({ error: 'Query failed' }); } }); // play/download media file app.get('/video/*', (req, res) => { // Get everything after /video/ const relPath = req.params[0]; // Normalize and prevent path traversal const safePath = path.normalize(relPath).replace(/^(\.\.(\/|\\|$))+/, ''); const filePath = path.join(MEDIA_DIR, safePath); //const filePath = path.join(MEDIA_DIR+'/', req.params.filename); const ext = path.extname(filePath).toLowerCase().slice(1); //console.log(filePath+' '+ext); // Decide how to serve if (["mp4", "mkv", "webm"].includes(ext)) { // Stream inline (supports range requests for seeking) const stat = fs.statSync(filePath); const fileSize = stat.size; const range = req.headers.range; if (range) { const parts = range.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const chunkSize = end - start + 1; const fileStream = fs.createReadStream(filePath, { start, end }); res.writeHead(206, { "Content-Range": `bytes ${start}-${end}/${fileSize}`, "Accept-Ranges": "bytes", "Content-Length": chunkSize, "Content-Type": `video/${ext === "mkv" ? "x-matroska" : ext}` }); fileStream.pipe(res); } else { res.writeHead(200, { "Content-Length": fileSize, "Content-Type": `video/${ext === "mkv" ? "x-matroska" : ext}` }); fs.createReadStream(filePath).pipe(res); } } else { // Force download for unsupported formats (avi, etc.) res.setHeader("Content-Disposition", `attachment; filename="${path.basename(relPath)}"`); res.setHeader("Content-Type", "application/octet-stream"); res.download(filePath); } /* switch (ext) { case '.mp4': case '.webm': case '.ogg': case '.mkv': res.setHeader('Content-Type', 'video/mp4'); fs.createReadStream(filePath).pipe(res); break; case '.avi': case '.mov': case '.wmv': // Force download instead of trying to stream res.download(filePath); break; default: res.status(415).send('Unsupported format'); } */ }); // 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 = { MEDIA_DIR: process.env.MEDIA_DIR || '', TMDB_API_KEY: process.env.TMDB_API_KEY || '', DISCORD_TOKEN: process.env.DISCORD_TOKEN || '', DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID || '', DB_TYPE: process.env.DB_TYPE || '', DB_NAME: process.env.DB_NAME || '', DB_HOST: process.env.DB_HOST || '', DB_USER: process.env.DB_USER || '', DB_PASS: process.env.DB_PASS || '' }; //} 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, DISCORD_TOKEN: req.body.discordToken, TMDB_API_KEY: req.body.tmdbKey, DB_TYPE: req.body.dbType, DB_NAME: req.body.dbName, DB_HOST: req.body.dbHost, DB_USER: req.body.dbUser, DB_PASS: req.body.dbPass }; // 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}`); });