Updated npm on new server

This commit is contained in:
yiekheng 2025-05-14 16:19:08 +08:00
parent cffab8977e
commit 7d0852b9f8
15 changed files with 4767 additions and 210 deletions

View File

@ -2,7 +2,7 @@
const dbConfig = {
mysqldb: {
host: '192.168.0.170',
host: '192.168.0.210',
port: 3306,
database: 'sunnymh_mysql',
user: 'sunnymh_mysql',

13
db.js
View File

@ -1,13 +0,0 @@
const config = require('./config');
class DB {
constructor() {
this.config = null;
}
loadConfig() {
this.config = config;
}
}
module.exports = DB;

View File

@ -1,45 +1,202 @@
const express = require('express');
const MySQLDB = require('./mysqldb');
const databaseService = require('./src/services/database.service');
// Configuration
const PORT = process.env.PORT || 4000;
// Initialize Express app
const app = express();
const port = 4000;
app.use(express.json()); // Middleware to parse JSON
// Initialize MySQL database connection
const mySQLDB = new MySQLDB();
app.use(express.json());
async function initializeDatabases() {
try {
await mySQLDB.createConnection();
await databaseService.createConnection();
} catch (error) {
console.error('Failed to initialize databases:', error);
process.exit(1);
}
}
app.get('/Pic', async (req, res) => {
// Routes
app.get('/manga/:id', async (req, res) => {
const mangaId = req.params.id;
try {
const sqlQuery = `SELECT * FROM manga_table WHERE manga_id = ${mySQLDB.connection.escape(mangaId)}`;
const mySQLResults = await mySQLDB.query(sqlQuery);
const sqlQuery = 'SELECT * FROM mangas WHERE mangaId = ?';
const mySQLResults = await databaseService.query(sqlQuery, [mangaId]);
res.json({ mySQLResults });
if (!mySQLResults || mySQLResults.length === 0) {
return res.status(404).json({ error: 'Manga not found' });
}
res.json({ data: mySQLResults[0] });
} catch (error) {
res.status(500).json({ error: error.message });
console.error('Error fetching manga:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/insert', async (req, res) => {
const { mangaId, mangaName } = req.body;
console.log(mangaId, mangaName);
app.get('/getMangaId/:mangaName', async (req, res) => {
const mangaName = req.params.mangaName;
try {
const sqlQuery = 'SELECT mangaId FROM mangas WHERE mangaName = ?';
const mySQLResults = await databaseService.query(sqlQuery, [mangaName]);
if (!mySQLResults || mySQLResults.length === 0) {
return res.status(404).json({ error: 'Manga not found' });
}
res.json({ data: mySQLResults[0] });
} catch (error) {
console.error('Error getting manga ID:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Start server after initializing databases
initializeDatabases().then(() => {
app.listen(port, () => {
console.log(`API server listening at http://localhost:${port}`);
app.get('/getGenres/:genre', async (req, res) => {
const genre = req.params.genre;
try {
const sqlQuery = 'SELECT genreId FROM genres WHERE genreName = ?';
const mySQLResults = await databaseService.query(sqlQuery, [genre]);
if (!mySQLResults || mySQLResults.length === 0) {
return res.status(404).json({ error: 'Genre not found' });
}
res.json({ data: mySQLResults[0] });
} catch (error) {
console.error('Error getting genre:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/insertGenre', async (req, res) => {
const genre = req.body;
try {
const sqlQuery = 'INSERT INTO genres (genreName) VALUES (?)';
const mySQLResults = await databaseService.query(sqlQuery, [genre.genreName]);
res.json({
success: true,
message: 'Genre inserted successfully',
genreId: mySQLResults.insertId
});
} catch (error) {
console.error('Error inserting genre:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/insertMangaGenres', async (req, res) => {
const genres = req.body;
try {
const sqlQuery = `
INSERT INTO manga_genres (
mangaId,
genreId
) VALUES (?, ?)
`;
const params = [
genres.mangaId,
genres.genreId
];
await databaseService.query(sqlQuery, params);
res.json({
success: true,
message: 'Manga genres inserted successfully',
});
} catch (error) {
console.error('Error inserting manga genres:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/insertMangaChapter', async (req, res) => {
const chapterInfo = req.body;
try {
const sqlQuery = `
INSERT INTO chapters (
mangaId,
chapterOrder,
chapterName
) VALUES (?, ?, ?)
`;
const params = [
chapterInfo.mangaId,
chapterInfo.chapterOrder,
chapterInfo.chapterName
];
await databaseService.query(sqlQuery, params);
res.json({
success: true,
message: 'Chapter inserted successfully',
});
} catch (error) {
console.error('Error inserting chapter:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/insertMangaInfo', async (req, res) => {
const mangaInfo = req.body;
try {
const sqlQuery = `
INSERT INTO mangas (
mangaId,
mangaName,
mangaAuthor,
mangaNickname,
mangaStatus
) VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
mangaName = VALUES(mangaName),
mangaAuthor = VALUES(mangaAuthor),
mangaNickname = VALUES(mangaNickname),
mangaStatus = VALUES(mangaStatus)
`;
const params = [
mangaInfo.mangaId,
mangaInfo.name,
mangaInfo.author,
JSON.stringify(mangaInfo.nickNames),
mangaInfo.status
];
await databaseService.query(sqlQuery, params);
res.json({
success: true,
message: 'Manga details inserted successfully',
});
} catch (error) {
console.error('Error inserting manga details:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({ error: 'Internal server error' });
});
// Start server
initializeDatabases()
.then(() => {
app.listen(PORT, () => {
console.log(`API server listening at http://localhost:${PORT}`);
});
})
.catch(error => {
console.error('Failed to start server:', error);
process.exit(1);
});

View File

@ -1,49 +0,0 @@
const DB = require("./db");
const mysql = require('mysql2');
class MySQLDB extends DB {
constructor() {
super();
this.connection = null;
}
async createConnection() {
this.loadConfig();
const { mysqldb } = this.config;
this.connection = mysql.createConnection({
host: mysqldb.host,
port: mysqldb.port,
user: mysqldb.user,
password: mysqldb.password,
database: mysqldb.database,
connectTimeout: 0 // disable timeout
});
await this.connection.connect();
}
async closeConnection() {
if (this.connection) {
await this.connection.end();
this.connection = null;
}
}
async query(query) {
if (!this.connection) {
throw new Error('Database not connected');
}
const results = await new Promise((resolve, reject) => {
this.connection.query(query, (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
return results;
}
}
module.exports = MySQLDB;

4018
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,23 @@
{
"name": "sunnymh-db",
"name": "sunnymh-backend",
"version": "1.0.0",
"main": "db_api.jsx",
"description": "Backend API for SunnyMH manga application",
"main": "src/app.js",
"scripts": {
"start": "node db_api.jsx",
"test": "echo \"Error: no test specified\" && exit 1"
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "jest"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.2",
"mongodb": "^6.15.0",
"mysql2": "^3.14.0"
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^4.18.2",
"mysql2": "^3.2.0"
},
"devDependencies": {
"jest": "^29.5.0",
"nodemon": "^2.0.22"
}
}

43
src/app.js Normal file
View File

@ -0,0 +1,43 @@
const express = require('express');
const cors = require('cors');
const config = require('./config');
const { errorHandler, notFoundHandler } = require('./middleware/error.middleware');
const mangaRoutes = require('./routes/manga.routes');
const genreRoutes = require('./routes/genre.routes');
const pictureRoutes = require('./routes/picture.routes');
const databaseService = require('./services/database.service');
// Initialize Express app
const app = express();
// Enable CORS for all routes
app.use(cors());
app.use(express.json());
// Routes
app.use('/manga', mangaRoutes);
app.use('/genre', genreRoutes);
app.use('/pic', pictureRoutes);
// Error handling
app.use(notFoundHandler);
app.use(errorHandler);
// Start server
async function startServer() {
try {
// Initialize database connection
await databaseService.createConnection();
console.log('Database connection established');
app.listen(config.port, () => {
console.log(`API server listening at http://localhost:${config.port}`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
startServer();

14
src/config/index.js Normal file
View File

@ -0,0 +1,14 @@
require('dotenv').config();
module.exports = {
port: process.env.PORT || 4000,
nodeEnv: process.env.NODE_ENV || 'development',
mysqldb: {
host: process.env.DB_HOST || '192.168.0.210',
port: parseInt(process.env.DB_PORT || '3306', 10),
user: process.env.DB_USER || 'sunnymh_mysql',
password: process.env.DB_PASSWORD || 'hengserver',
database: process.env.DB_NAME || 'sunnymh_mysql'
},
picturePath: process.env.PICTURE_PATH || 'Y:'
};

View File

@ -0,0 +1,24 @@
// Async handler wrapper to eliminate try-catch blocks
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Error handling middleware
const errorHandler = (err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
};
// Not found middleware
const notFoundHandler = (req, res) => {
res.status(404).json({ error: 'Route not found' });
};
module.exports = {
asyncHandler,
errorHandler,
notFoundHandler
};

View File

@ -0,0 +1,52 @@
const express = require('express');
const router = express.Router();
const databaseService = require('../services/database.service');
// **** For frontend ****
// **** For scraper ****
router.get('/getGenreId/:genreName', async (req, res) => {
const genre = req.params.genreName;
try {
const sqlQuery = 'SELECT genreId FROM genres WHERE genreName = ?';
const results = await databaseService.query(sqlQuery, [genre]);
if (!results || results.length === 0) {
return res.json({
success: false,
message: 'Genre not found'
});
}
res.json({
success: true,
message: 'Genre found',
genreId: results[0].genreId
});
} catch (error) {
console.error('Error getting genre:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Insert new genre
router.post('/insertGenre', async (req, res) => {
const genre = req.body;
try {
const sqlQuery = 'INSERT INTO genres (genreName) VALUES (?)';
const results = await databaseService.query(sqlQuery, [genre.genreName]);
res.json({
success: true,
message: 'Genre inserted successfully',
genreId: results.insertId
});
} catch (error) {
console.error('Error inserting genre:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

181
src/routes/manga.routes.js Normal file
View File

@ -0,0 +1,181 @@
const express = require('express');
const router = express.Router();
const databaseService = require('../services/database.service');
// **** For frontend ****
router.get('/getMangaInfo/:mangaId', async (req, res) => {
const mangaId = req.params.mangaId;
try {
const sqlQuery = `
SELECT m.*, GROUP_CONCAT(g.genreName) as genreNames
FROM mangas m
LEFT JOIN manga_genres mg ON m.mangaId = mg.mangaId
LEFT JOIN genres g ON mg.genreId = g.genreId
WHERE m.mangaId = ?
GROUP BY m.mangaId
`;
const results = await databaseService.query(sqlQuery, [mangaId]);
if (!results[0]) {
return res.status(404).json({
success: false,
message: 'Manga not found'
});
}
// Convert genreNames string to array
const mangaInfo = {
...results[0],
genres: results[0].genreNames ? results[0].genreNames.split(',') : []
};
// Remove the raw genreNames field
delete mangaInfo.genreNames;
return res.json(mangaInfo);
} catch (error) {
console.error('Error getting manga info:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.get('/getMangaChapters/:mangaId', async (req, res) => {
const mangaId = req.params.mangaId;
try {
const sqlQuery = 'SELECT * FROM chapters WHERE mangaId = ?';
const results = await databaseService.query(sqlQuery, [mangaId]);
return res.json(results);
} catch (error) {
console.error('Error getting manga chapters:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// **** For scraper ****
router.get('/getMangaId/:mangaName', async (req, res) => {
const mangaName = req.params.mangaName;
try {
const sqlQuery = 'SELECT mangaId FROM mangas WHERE mangaName = ?';
const results = await databaseService.query(sqlQuery, [mangaName]);
if (!results || results.length === 0) {
return res.json({
success: false,
message: 'Manga not found'
});
}
res.json({
success: true,
message: 'Manga found',
mangaId: results[0].mangaId
});
} catch (error) {
console.error('Error getting manga ID:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Insert manga info
router.post('/insertMangaInfo', async (req, res) => {
const mangaInfo = req.body;
try {
const mangaNickname = Array.isArray(mangaInfo.mangaNickname)
? mangaInfo.mangaNickname.join(',')
: mangaInfo.mangaNickname;
const sqlQuery = `
INSERT INTO mangas (
mangaId,
mangaName,
mangaAuthor,
mangaNickname,
mangaStatus
) VALUES (?, ?, ?, ?, ?)
`;
const params = [
mangaInfo.mangaId,
mangaInfo.mangaName,
mangaInfo.mangaAuthor,
mangaNickname,
mangaInfo.mangaStatus
];
await databaseService.query(sqlQuery, params);
res.json({
success: true,
message: 'Manga details inserted successfully',
mangaId: mangaInfo.mangaId
});
} catch (error) {
console.error('Error inserting manga details:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Insert manga chapter
router.post('/insertMangaChapter', async (req, res) => {
const chapterInfo = req.body;
try {
const sqlQuery = `
INSERT INTO chapters (
mangaId,
chapterOrder,
chapterName
) VALUES (?, ?, ?)
`;
const params = [
chapterInfo.mangaId,
chapterInfo.chapterOrder,
chapterInfo.chapterName
];
await databaseService.query(sqlQuery, params);
res.json({
success: true,
message: 'Chapter inserted successfully',
chapterId: chapterInfo.chapterId
});
} catch (error) {
console.error('Error inserting chapter:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Insert manga genres
router.post('/insertMangaGenres', async (req, res) => {
const genres = req.body;
try {
const sqlQuery = `
INSERT INTO manga_genres (
mangaId,
genreId
) VALUES (?, ?)
`;
const params = [
genres.mangaId,
genres.genreId
];
await databaseService.query(sqlQuery, params);
res.json({
success: true,
message: 'Manga genres inserted successfully'
});
} catch (error) {
console.error('Error inserting manga genres:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

View File

@ -0,0 +1,151 @@
const express = require('express');
const router = express.Router();
const config = require('../config');
const fs = require('fs');
const path = require('path');
const databaseService = require('../services/database.service');
// **** For frontend ****
router.get('/:mangaId', async (req, res) => {
const mangaId = req.params.mangaId;
try {
const sqlQuery = 'SELECT mangaName FROM mangas WHERE mangaId = ?';
const results = await databaseService.query(sqlQuery, [mangaId]);
if (!results[0]) {
return res.status(404).json({
success: false,
message: 'Manga not found'
});
}
const mangaName = results[0].mangaName;
const picturePath = `${config.picturePath}\\${mangaName}/`;
const coverPicture = fs.readFileSync(path.join(picturePath, 'cover.jpg'));
res.setHeader('Content-Type', 'image/jpeg');
return res.send(coverPicture);
} catch (error) {
console.error('Error getting picture:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.get('/:mangaId/:chapterOrder', async (req, res) => {
const mangaId = req.params.mangaId;
const chapterOrder = req.params.chapterOrder;
try {
// Get manga name and chapter name from database
const sqlQuery = `
SELECT m.mangaName, c.chapterName
FROM mangas m
JOIN chapters c ON m.mangaId = c.mangaId
WHERE m.mangaId = ? AND c.chapterOrder = ?
`;
const results = await databaseService.query(sqlQuery, [mangaId, chapterOrder]);
if (!results[0]) {
return res.status(404).json({
success: false,
message: 'Manga or chapter not found'
});
}
const mangaName = results[0].mangaName;
const chapterName = results[0].chapterName;
const chapterPath = path.join(config.picturePath, mangaName, chapterName);
// Check if directory exists
if (!fs.existsSync(chapterPath)) {
return res.status(404).json({
success: false,
message: 'Chapter directory not found'
});
}
// Read all files in the directory
const files = fs.readdirSync(chapterPath);
// Filter for image files and sort them
const imageFiles = files
.filter(file => /\.(jpg|jpeg|png|webp)$/i.test(file))
.sort((a, b) => {
// Extract numbers from filenames for proper sorting
const numA = parseInt(a.match(/\d+/)[0]);
const numB = parseInt(b.match(/\d+/)[0]);
return numA - numB;
});
// Return the image paths
const imagePaths = imageFiles.map(file => (file));
return res.json(imagePaths);
} catch (error) {
console.error('Error getting chapter pictures:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.get('/:mangaId/:chapterOrder/:picName', async (req, res) => {
const mangaId = req.params.mangaId;
const chapterOrder = req.params.chapterOrder;
const picName = req.params.picName;
try {
// Get manga name and chapter name from database
const sqlQuery = `
SELECT m.mangaName, c.chapterName
FROM mangas m
JOIN chapters c ON m.mangaId = c.mangaId
WHERE m.mangaId = ? AND c.chapterOrder = ?
`;
const results = await databaseService.query(sqlQuery, [mangaId, chapterOrder]);
if (!results[0]) {
return res.status(404).json({
success: false,
message: 'Manga or chapter not found'
});
}
const mangaName = results[0].mangaName;
const chapterName = results[0].chapterName;
const chapterPath = path.join(config.picturePath, mangaName, chapterName);
// Check if directory exists
if (!fs.existsSync(chapterPath)) {
return res.status(404).json({
success: false,
message: 'Chapter directory not found'
});
}
const picPath = path.join(chapterPath, picName);
const pic = fs.readFileSync(picPath);
// Set appropriate content type based on file extension
const ext = path.extname(picName).toLowerCase();
const contentType = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.webp': 'image/webp'
}[ext] || 'application/octet-stream';
res.setHeader('Content-Type', contentType);
return res.send(pic);
} catch (error) {
console.error('Error getting picture:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// **** For scraper ****
module.exports = router;

View File

@ -0,0 +1,117 @@
const mysql = require('mysql2/promise');
const config = require('../config');
class DatabaseService {
constructor() {
this.pool = null;
this.config = config;
this.lastRefreshTime = null;
this.REFRESH_INTERVAL = 30 * 60 * 1000; // 30 minutes in milliseconds
}
async createConnection() {
try {
const currentTime = Date.now();
// Check if we need to refresh the connection
if (!this.pool || !this.lastRefreshTime ||
(currentTime - this.lastRefreshTime) > this.REFRESH_INTERVAL) {
// Close existing pool if it exists
if (this.pool) {
await this.closeConnection();
}
const { mysqldb } = this.config;
if (!this.isValidConfig(mysqldb)) {
throw new Error('Invalid database configuration');
}
this.pool = mysql.createPool({
host: mysqldb.host,
port: mysqldb.port,
user: mysqldb.user,
password: mysqldb.password,
database: mysqldb.database,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0
});
this.lastRefreshTime = currentTime;
console.log('Database connection pool refreshed');
}
} catch (error) {
console.error('Failed to create MySQL connection pool:', error);
throw error;
}
}
isValidConfig(config) {
return config
&& typeof config.host === 'string'
&& typeof config.port === 'number'
&& typeof config.user === 'string'
&& typeof config.password === 'string'
&& typeof config.database === 'string';
}
async closeConnection() {
if (this.pool) {
try {
await this.pool.end();
this.pool = null;
this.lastRefreshTime = null;
} catch (error) {
console.error('Error closing MySQL connection pool:', error);
throw error;
}
}
}
async query(query, params = []) {
if (!this.pool) {
await this.createConnection();
}
try {
const [results] = await this.pool.query(query, params);
return results;
} catch (error) {
console.error('Database query error:', {
query,
params,
error: error.message
});
throw error;
}
}
async transaction(callback) {
if (!this.pool) {
await this.createConnection();
}
const connection = await this.pool.getConnection();
await connection.beginTransaction();
try {
const result = await callback(connection);
await connection.commit();
return result;
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}
}
// Create a singleton instance
const databaseService = new DatabaseService();
module.exports = databaseService;

View File

@ -0,0 +1,18 @@
class GenreService {
async getGenreByName(db, genreName) {
const sqlQuery = 'SELECT genreId FROM genres WHERE genreName = ?';
const results = await db.query(sqlQuery, [genreName]);
return results[0];
}
async insertGenre(db, genre) {
const sqlQuery = 'INSERT INTO genres (genreName) VALUES (?)';
const results = await db.query(sqlQuery, [genre.genreName]);
return {
genreId: results.insertId
};
}
}
module.exports = new GenreService();

View File

@ -0,0 +1,72 @@
class MangaService {
async getMangaById(db, mangaId) {
const sqlQuery = 'SELECT * FROM mangas WHERE mangaId = ?';
const results = await db.query(sqlQuery, [mangaId]);
return results[0];
}
async getMangaIdByName(db, mangaName) {
const sqlQuery = 'SELECT mangaId FROM mangas WHERE mangaName = ?';
const results = await db.query(sqlQuery, [mangaName]);
return results[0];
}
async insertMangaInfo(db, mangaInfo) {
const sqlQuery = `
INSERT INTO mangas (
mangaId,
mangaName,
mangaAuthor,
mangaNickname,
mangaStatus
) VALUES (?, ?, ?, ?, ?)
`;
const params = [
mangaInfo.mangaId,
mangaInfo.name,
mangaInfo.author,
JSON.stringify(mangaInfo.nickNames),
mangaInfo.status
];
return await db.query(sqlQuery, params);
}
async insertMangaChapter(db, chapterInfo) {
const sqlQuery = `
INSERT INTO chapters (
mangaId,
chapterOrder,
chapterName
) VALUES (?, ?, ?)
`;
const params = [
chapterInfo.mangaId,
chapterInfo.chapterOrder,
chapterInfo.chapterName
];
return await db.query(sqlQuery, params);
}
async insertMangaGenres(db, genres) {
const sqlQuery = `
INSERT INTO manga_genres (
mangaId,
genreId
) VALUES (?, ?)
`;
const params = [
genres.mangaId,
genres.genreId
];
return await db.query(sqlQuery, params);
}
}
module.exports = new MangaService();