discord-media-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +37 -0
- package/bin/reset.js +11 -0
- package/bin/scan.js +13 -0
- package/bin/setup.js +11 -0
- package/bin/start.js +12 -0
- package/lib/bot.js +352 -0
- package/lib/database.js +107 -0
- package/lib/loadConfig.js +22 -0
- package/lib/reset.js +42 -0
- package/lib/scanner.js +375 -0
- package/lib/server.js +91 -0
- package/lib/setup.js +362 -0
- package/package.json +43 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const command = args[0];
|
|
7
|
+
|
|
8
|
+
switch (command) {
|
|
9
|
+
case 'init':
|
|
10
|
+
console.log('Starting setup...');
|
|
11
|
+
execSync('node ./bin/setup.js', { stdio: 'inherit' });
|
|
12
|
+
|
|
13
|
+
console.log('Starting scan...');
|
|
14
|
+
execSync('node ./bin/scan.js', { stdio: 'inherit' });
|
|
15
|
+
|
|
16
|
+
console.log('Starting Discord bot...');
|
|
17
|
+
execSync('node ./bin/start.js', { stdio: 'inherit' });
|
|
18
|
+
break;
|
|
19
|
+
case 'setup':
|
|
20
|
+
console.log('Starting setup...');
|
|
21
|
+
execSync('node ./bin/setup.js', { stdio: 'inherit' });
|
|
22
|
+
break;
|
|
23
|
+
case 'scan':
|
|
24
|
+
console.log('Starting scan...');
|
|
25
|
+
execSync('node ./bin/scan.js', { stdio: 'inherit' });
|
|
26
|
+
break;
|
|
27
|
+
case 'start':
|
|
28
|
+
console.log('Starting Discord bot...');
|
|
29
|
+
execSync('node ./bin/start.js', { stdio: 'inherit' });
|
|
30
|
+
break;
|
|
31
|
+
case 'reset':
|
|
32
|
+
console.log('Resetting & clearing database...');
|
|
33
|
+
execSync('node ./bin/reset.js', { stdio: 'inherit' });
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
console.log('Usage: discord-media-server [init|setup|scan|start|reset]');
|
|
37
|
+
}
|
package/bin/reset.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// bin/reset.js
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import '../lib/reset.js'; // reset database and clear cache
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
dotenv.config({ path: path.join(__dirname, '../.env') });
|
|
10
|
+
|
|
11
|
+
console.log('Reset database and cleared cache...');
|
package/bin/scan.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// bin/scan.js
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import runScanner from '../lib/scanner.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
dotenv.config({ path: path.join(__dirname, '../.env') });
|
|
10
|
+
|
|
11
|
+
console.log('Running media scan...');
|
|
12
|
+
await runScanner();
|
|
13
|
+
console.log('Scan complete.');
|
package/bin/setup.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// bin/setup.js
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import runSetup from '../lib/setup.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
dotenv.config({ path: path.join(__dirname, '../.env') });
|
|
10
|
+
|
|
11
|
+
await runSetup();
|
package/bin/start.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// bin/reset.js
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import '../lib/server.js'; // starts the Express server
|
|
6
|
+
import '../lib/bot.js'; // starts the Discord bot
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
dotenv.config({ path: path.join(__dirname, '../.env') });
|
|
11
|
+
|
|
12
|
+
console.log('Starting Discord Media Server...');
|
package/lib/bot.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
|
|
2
|
+
// bot.js (ES module version using database.js directly)
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
import { Client, GatewayIntentBits, EmbedBuilder, ButtonBuilder, ActionRowBuilder } from 'discord.js';
|
|
7
|
+
import { query, isSQLite } from './database.js';
|
|
8
|
+
const { default: runScanner } = await import('./scanner.js');
|
|
9
|
+
|
|
10
|
+
const client = new Client({
|
|
11
|
+
intents: [
|
|
12
|
+
GatewayIntentBits.Guilds,
|
|
13
|
+
GatewayIntentBits.GuildMessages,
|
|
14
|
+
GatewayIntentBits.MessageContent
|
|
15
|
+
]
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
client.on('ready', () => {
|
|
19
|
+
console.log(`Logged in as ${client.user.tag}`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
client.on('messageCreate', async (message) => {
|
|
23
|
+
if (message.author.bot || !message.content.startsWith('.')) return;
|
|
24
|
+
|
|
25
|
+
console.log(`${new Date().toLocaleString()} - ${message.author.tag}: ${message.content}`);
|
|
26
|
+
|
|
27
|
+
const args = message.content.slice(1).trim().split(/ +/);
|
|
28
|
+
const command = args.shift().toLowerCase();
|
|
29
|
+
|
|
30
|
+
if (command === 'm' || command === 'movie') {
|
|
31
|
+
const { queryText, year, limit } = parseMovieArgs(args);
|
|
32
|
+
let searchText = queryText;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
let movie;
|
|
36
|
+
let sql = `SELECT * FROM Movie_Info WHERE 1=1`;
|
|
37
|
+
let sqlArgs = [];
|
|
38
|
+
|
|
39
|
+
if (queryText) {
|
|
40
|
+
sql += ` AND title LIKE ?`;
|
|
41
|
+
sqlArgs.push(`%${queryText}%`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (year) {
|
|
45
|
+
sql += ` AND year = ?`;
|
|
46
|
+
sqlArgs.push(year);
|
|
47
|
+
searchText = queryText+' ('+year+')'.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
sql += ` ORDER BY title ASC LIMIT 1`;
|
|
51
|
+
|
|
52
|
+
let rows = await query(sql, sqlArgs);
|
|
53
|
+
movie = rows[0];
|
|
54
|
+
|
|
55
|
+
if (!movie || (!queryText && !year)) {
|
|
56
|
+
// fallback to random if no filters
|
|
57
|
+
sql = `SELECT * FROM Movie_Info ORDER BY ${isSQLite ? 'RANDOM()' : 'RAND()'} LIMIT 1`;
|
|
58
|
+
sqlArgs = [];
|
|
59
|
+
rows = await query(sql, sqlArgs);
|
|
60
|
+
movie = rows[0];
|
|
61
|
+
searchText = `${searchText ? searchText+'\n' : ''}No Result, Random movie chosen`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const embed = createMovieEmbed(movie, message.author, searchText);
|
|
65
|
+
const reply = await message.channel.send({ embeds: [embed] });
|
|
66
|
+
await message.delete().catch(() => {});
|
|
67
|
+
setTimeout(() => reply.delete().catch(() => {}), 5 * 60 * 1000);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('Error retrieving movie:', err);
|
|
70
|
+
message.reply('❌ Error retrieving movie.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (command === 'ml' || command === 'list') {
|
|
75
|
+
const { queryText, year, limit } = parseMovieArgs(args);
|
|
76
|
+
let finalLimit = 10;
|
|
77
|
+
if (limit && (limit <= 25 && limit > 0)) {
|
|
78
|
+
finalLimit = limit;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let sql;
|
|
82
|
+
let sqlArgs = [];
|
|
83
|
+
|
|
84
|
+
if (!queryText && !year) {
|
|
85
|
+
// No args == random movies
|
|
86
|
+
sql = `SELECT * FROM Movie_Info ORDER BY ${isSQLite ? 'RANDOM()' : 'RAND()'} LIMIT ?`;
|
|
87
|
+
sqlArgs.push(finalLimit);
|
|
88
|
+
} else {
|
|
89
|
+
// Build search query
|
|
90
|
+
sql = `SELECT * FROM Movie_Info WHERE 1=1`;
|
|
91
|
+
if (queryText) {
|
|
92
|
+
sql += ` AND title LIKE ?`;
|
|
93
|
+
sqlArgs.push(`%${queryText}%`);
|
|
94
|
+
}
|
|
95
|
+
if (year) {
|
|
96
|
+
sql += ` AND year = ?`;
|
|
97
|
+
sqlArgs.push(year);
|
|
98
|
+
}
|
|
99
|
+
if (!queryText && year) {
|
|
100
|
+
sql += ` ORDER BY ${isSQLite ? 'RANDOM()' : 'RAND()'} LIMIT ?`;
|
|
101
|
+
} else {
|
|
102
|
+
sql += ` ORDER BY title ASC LIMIT ?`;
|
|
103
|
+
}
|
|
104
|
+
sqlArgs.push(finalLimit);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const rows = await query(sql, sqlArgs);
|
|
109
|
+
if (!rows.length) return message.reply('❌ No movies found.');
|
|
110
|
+
|
|
111
|
+
let searchText = queryText;
|
|
112
|
+
if (year) {
|
|
113
|
+
searchText = queryText+' ('+year+')'.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const embed = new EmbedBuilder()
|
|
117
|
+
.setTitle(
|
|
118
|
+
!queryText && !year
|
|
119
|
+
? '🎬 Random Movies'
|
|
120
|
+
: `🎬 Movies matching: "${searchText}"`.trim()
|
|
121
|
+
)
|
|
122
|
+
.setDescription(
|
|
123
|
+
rows
|
|
124
|
+
.map((m, i) => `**${i + 1}.** ${m.title} (${m.year === '0000' ? 'n/a' : m.year})\n - ${m.format} - ${formatBytes(m.filesize)}${m.year === '0000' ? '\n' : ' - [IMDB](https://www.imdb.com/title/${m.imdb})\n'}`)
|
|
125
|
+
.join('\n')
|
|
126
|
+
)
|
|
127
|
+
.setColor(0x2ecc71)
|
|
128
|
+
.setFooter({ text: `Requested by ${message.author.username}` })
|
|
129
|
+
.setFooter({
|
|
130
|
+
text: `${message.author.username}\n${searchText}`,
|
|
131
|
+
iconURL: message.author.displayAvatarURL({ dynamic: true })
|
|
132
|
+
})
|
|
133
|
+
.setTimestamp();
|
|
134
|
+
|
|
135
|
+
const reply = await message.channel.send({ embeds: [embed] });
|
|
136
|
+
await message.delete().catch(() => {});
|
|
137
|
+
setTimeout(() => reply.delete().catch(() => {}), 5 * 60 * 1000);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error('Error retrieving movie list:', err);
|
|
140
|
+
message.reply('❌ Error retrieving movie list.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (command === 'random') {
|
|
145
|
+
try {
|
|
146
|
+
const rows = await query(`SELECT * FROM Movie_Info ORDER BY ${isSQLite ? 'RANDOM()' : 'RAND()'} LIMIT 1`);
|
|
147
|
+
const movie = rows[0];
|
|
148
|
+
if (!movie) return message.reply('❌ No random movie found.');
|
|
149
|
+
|
|
150
|
+
const embed = createMovieEmbed(movie, message.author, '');
|
|
151
|
+
const reply = await message.channel.send({ embeds: [embed] });
|
|
152
|
+
await message.delete().catch(() => {});
|
|
153
|
+
setTimeout(() => reply.delete().catch(() => {}), 5 * 60 * 1000);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error('Error fetching random movie:', err);
|
|
156
|
+
message.reply('❌ Error fetching random movie.');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// scan movie command
|
|
161
|
+
if (command === 'scan' || command === 'rescan') {
|
|
162
|
+
if (!message.member.permissions.has('ManageGuild')) {
|
|
163
|
+
return message.reply('You do not have permission to trigger a scan.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const initialEmbed = new EmbedBuilder()
|
|
168
|
+
.setTitle('🔍 Starting scan for new movies...')
|
|
169
|
+
.setColor(0x2ecc71)
|
|
170
|
+
.setFooter({ text: `Scanning movies...` })
|
|
171
|
+
.setTimestamp();
|
|
172
|
+
|
|
173
|
+
const reply = await message.channel.send({ embeds: [initialEmbed] });
|
|
174
|
+
|
|
175
|
+
if (message.channel.permissionsFor(message.client.user).has('ManageMessages')) {
|
|
176
|
+
message.delete().catch(() => {});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Start periodic typing indicator
|
|
180
|
+
const typingInterval = setInterval(() => {
|
|
181
|
+
message.channel.sendTyping().catch(() => {});
|
|
182
|
+
}, 8000); // Re-send every 8s to keep it alive
|
|
183
|
+
|
|
184
|
+
const result = await runScanner();
|
|
185
|
+
clearInterval(typingInterval); // Stop when done
|
|
186
|
+
|
|
187
|
+
const finalEmbed = new EmbedBuilder()
|
|
188
|
+
.setTitle('✅ Scan Complete!')
|
|
189
|
+
.setColor(0x2ecc71)
|
|
190
|
+
.setDescription(
|
|
191
|
+
`**Total movies:** ${result.totalFiles}\n` +
|
|
192
|
+
`**Movies added:** ${result.newMovies}\n` +
|
|
193
|
+
`**Movies updated:** ${result.updatedMovies}`
|
|
194
|
+
)
|
|
195
|
+
.setFooter({ text: `Movie scan completed` })
|
|
196
|
+
.setTimestamp();
|
|
197
|
+
|
|
198
|
+
await reply.edit({ embeds: [finalEmbed] });
|
|
199
|
+
|
|
200
|
+
// Send new movies to a channel
|
|
201
|
+
if (result.addedList.length > 0) {
|
|
202
|
+
const channel = message.guild.channels.cache.find(c =>
|
|
203
|
+
c.name === 'general' && c.isTextBased?.()
|
|
204
|
+
);
|
|
205
|
+
if (channel) {
|
|
206
|
+
|
|
207
|
+
const movieList = result.addedList
|
|
208
|
+
.map((m, i) => `**[${i + 1}. ${m.title} (${m.year})](https://www.imdb.com/title/${m.imdb})**`);
|
|
209
|
+
|
|
210
|
+
const chunks = chunkLines(movieList, 25); // 25 movies per page
|
|
211
|
+
let currentPage = 0;
|
|
212
|
+
|
|
213
|
+
// Build the initial embed
|
|
214
|
+
const buildEmbed = (page) =>
|
|
215
|
+
new EmbedBuilder()
|
|
216
|
+
.setTitle('🎬 '+result.newMovies+' New Movies Added')
|
|
217
|
+
.setColor(0x2ecc71)
|
|
218
|
+
.setDescription(chunks[page].join('\n'))
|
|
219
|
+
.setFooter({ text: `Page ${page + 1} of ${chunks.length}` })
|
|
220
|
+
.setTimestamp();
|
|
221
|
+
|
|
222
|
+
// Build navigation buttons
|
|
223
|
+
const getRow = () =>
|
|
224
|
+
new ActionRowBuilder().addComponents(
|
|
225
|
+
new ButtonBuilder()
|
|
226
|
+
.setCustomId('prev')
|
|
227
|
+
.setLabel('⬅ Prev')
|
|
228
|
+
.setStyle(1)
|
|
229
|
+
.setDisabled(currentPage === 0),
|
|
230
|
+
new ButtonBuilder()
|
|
231
|
+
.setCustomId('next')
|
|
232
|
+
.setLabel('Next ➡')
|
|
233
|
+
.setStyle(1)
|
|
234
|
+
.setDisabled(currentPage === chunks.length - 1)
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Send the first embed with buttons
|
|
238
|
+
const sent = await message.channel.send({
|
|
239
|
+
embeds: [buildEmbed(currentPage)],
|
|
240
|
+
components: [getRow()]
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Set up button collector
|
|
244
|
+
const collector = sent.createMessageComponentCollector({
|
|
245
|
+
//time: 60_000 // 1 minute
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
collector.on('collect', async (interaction) => {
|
|
249
|
+
if (interaction.customId === 'prev') currentPage--;
|
|
250
|
+
if (interaction.customId === 'next') currentPage++;
|
|
251
|
+
|
|
252
|
+
await interaction.update({
|
|
253
|
+
embeds: [buildEmbed(currentPage)],
|
|
254
|
+
components: [getRow()]
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
collector.on('end', async () => {
|
|
259
|
+
// Disable buttons after timeout
|
|
260
|
+
//await sent.edit({ components: [] }).catch(() => {});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
setTimeout(() => reply.delete().catch(() => {}), 5 * 60 * 1000);
|
|
266
|
+
|
|
267
|
+
} catch (err) {
|
|
268
|
+
clearInterval(typingInterval);
|
|
269
|
+
console.error('Scan failed:', err);
|
|
270
|
+
message.reply('Scan failed. Check server logs for details.');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
function formatBytes(a, b = 2, k = 1024) {
|
|
278
|
+
if (a === 0) return '0 Bytes';
|
|
279
|
+
const i = Math.floor(Math.log(a) / Math.log(k));
|
|
280
|
+
return `${(a / Math.pow(k, i)).toFixed(b)} ${['Bytes', 'KB', 'MB', 'GB', 'TB'][i]}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseMovieArgs(args) {
|
|
284
|
+
let queryText = null;
|
|
285
|
+
let year = null;
|
|
286
|
+
let limit = null;
|
|
287
|
+
let joined = args.join(' ');
|
|
288
|
+
|
|
289
|
+
const yearMatch = joined.match(/-y (\d{4})/i);
|
|
290
|
+
if (yearMatch) {
|
|
291
|
+
year = yearMatch[1];
|
|
292
|
+
joined = joined.replace(yearMatch[0], '').trim();
|
|
293
|
+
}
|
|
294
|
+
let limitMatch = joined.match(/-l (\d+)/i);
|
|
295
|
+
if (limitMatch) {
|
|
296
|
+
limit = parseInt(limitMatch[1], 10);
|
|
297
|
+
joined = joined.replace(limitMatch[0], '').trim();
|
|
298
|
+
}
|
|
299
|
+
let limitMatch2 = joined.match(/-(\d+)/i);
|
|
300
|
+
if (limitMatch2) {
|
|
301
|
+
limit = parseInt(limitMatch2[1], 10);
|
|
302
|
+
joined = joined.replace(limitMatch2[0], '').trim();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { queryText:joined, year, limit };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function createMovieEmbed(movie, user, search) {
|
|
309
|
+
const embed = new EmbedBuilder()
|
|
310
|
+
.setTitle(`${movie.title} (${movie.year || '0000'})`)
|
|
311
|
+
.setDescription(movie.overview || 'No overview available.')
|
|
312
|
+
.setColor(0x2ecc71)
|
|
313
|
+
.addFields(
|
|
314
|
+
{ name: 'Format', value: movie.format || '', inline: true },
|
|
315
|
+
{ name: 'Filesize', value: formatBytes(movie.filesize) || 'N/A', inline: true },
|
|
316
|
+
{ name: 'Rating', value: movie.rating || 'N/A', inline: true }
|
|
317
|
+
)
|
|
318
|
+
.setFooter({
|
|
319
|
+
text: `${user.tag}\n${search}`,
|
|
320
|
+
iconURL: user.displayAvatarURL({ dynamic: true })
|
|
321
|
+
})
|
|
322
|
+
.setTimestamp();
|
|
323
|
+
|
|
324
|
+
/*
|
|
325
|
+
if (movie.poster || movie.poster_fallback) {
|
|
326
|
+
embed.setThumbnail(
|
|
327
|
+
`http://localhost:3000/movies/${movie.poster}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
*/
|
|
331
|
+
|
|
332
|
+
let posterURL = movie.poster_fallback || movie.posterURL;
|
|
333
|
+
if (posterURL?.startsWith('http')) {
|
|
334
|
+
embed.setThumbnail(posterURL);
|
|
335
|
+
} else {
|
|
336
|
+
posterURL = `http://localhost:3000/movies/${encodeURI(movie.posterURL)}`;
|
|
337
|
+
embed.setThumbnail(posterURL);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return embed;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
function chunkLines(lines, size = 10) {
|
|
345
|
+
const result = [];
|
|
346
|
+
for (let i = 0; i < lines.length; i += size) {
|
|
347
|
+
result.push(lines.slice(i, i + size));
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
client.login(process.env.DISCORD_TOKEN);
|
package/lib/database.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// database.js (universal MySQL + SQLite handler)
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname } from 'path';
|
|
9
|
+
|
|
10
|
+
let db;
|
|
11
|
+
let isSQLite = false;
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Lazy-loaded config & connection
|
|
17
|
+
let initialized = false;
|
|
18
|
+
|
|
19
|
+
const { loadConfig } = await import('./loadConfig.js');
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
isSQLite = config.dbType === 'sqlite';
|
|
22
|
+
|
|
23
|
+
/** Initializes DB connection based on config.json */
|
|
24
|
+
export async function initDatabase() {
|
|
25
|
+
if (initialized) return;
|
|
26
|
+
|
|
27
|
+
if (isSQLite) {
|
|
28
|
+
const sqlite3 = (await import('better-sqlite3')).default;
|
|
29
|
+
db = new sqlite3(path.join(__dirname, config.dbConfig.filename || config.dbName));
|
|
30
|
+
} else {
|
|
31
|
+
const mysql = await import('mysql2/promise');
|
|
32
|
+
db = await mysql.createConnection({
|
|
33
|
+
host: config.dbConfig.host || config.dbHost,
|
|
34
|
+
user: config.dbConfig.user || config.dbUser,
|
|
35
|
+
password: config.dbConfig.password || process.env.DB_PASS,
|
|
36
|
+
database: config.dbConfig.database || config.dbName
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
initialized = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Connects to SQLite for setup validation */
|
|
44
|
+
export async function connectSQLite(config) {
|
|
45
|
+
const sqlite3 = (await import('better-sqlite3')).default;
|
|
46
|
+
|
|
47
|
+
const dbPath = path.resolve(__dirname, '../data', config.filename || 'database.sqlite');
|
|
48
|
+
|
|
49
|
+
// Ensure the folder exists
|
|
50
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const db = new sqlite3(dbPath);
|
|
54
|
+
db.exec('SELECT 1');
|
|
55
|
+
db.close();
|
|
56
|
+
console.log(`SQLite database ready at ${dbPath}`);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error('SQLite connection failed:', err.message);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Connects to MySQL for setup validation */
|
|
64
|
+
export async function connectMySQL(config) {
|
|
65
|
+
try {
|
|
66
|
+
const mysql = await import('mysql2/promise');
|
|
67
|
+
const db = await mysql.createConnection({
|
|
68
|
+
host: config.host,
|
|
69
|
+
user: config.user,
|
|
70
|
+
password: config.password,
|
|
71
|
+
database: config.database
|
|
72
|
+
});
|
|
73
|
+
await db.query('SELECT 1');
|
|
74
|
+
await db.end();
|
|
75
|
+
console.log('MySQL connection successful');
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('MySQL connection failed:', err.message);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function query(sql, params = []) {
|
|
83
|
+
if (!initialized) await initDatabase();
|
|
84
|
+
|
|
85
|
+
if (isSQLite) {
|
|
86
|
+
const stmt = db.prepare(sql);
|
|
87
|
+
if (sql.trim().toLowerCase().startsWith('select')) {
|
|
88
|
+
return stmt.all(...params);
|
|
89
|
+
} else {
|
|
90
|
+
return stmt.run(...params);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
const [rows] = await db.execute(sql, params);
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function close() {
|
|
99
|
+
if (!initialized) return;
|
|
100
|
+
if (isSQLite) {
|
|
101
|
+
db.close();
|
|
102
|
+
} else {
|
|
103
|
+
await db.end();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { isSQLite };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// lib/loadConfig.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const configPath = path.resolve(__dirname, '../config.json');
|
|
9
|
+
|
|
10
|
+
export function loadConfig() {
|
|
11
|
+
if (!fs.existsSync(configPath)) {
|
|
12
|
+
console.error('config.json not found. Please run setup.js first.');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('Failed to parse config.json:', err.message);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/lib/reset.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// reset-db.js (clears Movie_Info table without touching config)
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { query, close, isSQLite } from './database.js';
|
|
7
|
+
import config from '../config.json' assert { type: 'json' };
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
async function resetMovieTable() {
|
|
12
|
+
try {
|
|
13
|
+
// delete cache file
|
|
14
|
+
const CACHE_FILE = `../data/${config.dbConfig.database}.json`;
|
|
15
|
+
const cachePath = path.resolve(__dirname, CACHE_FILE);
|
|
16
|
+
fs.unlink(cachePath, (err) => {
|
|
17
|
+
if (err) {
|
|
18
|
+
console.log('Error deleting file:', err);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log('File deleted successfully');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// delete database
|
|
25
|
+
if (isSQLite) {
|
|
26
|
+
await query('DELETE FROM Movie_Info');
|
|
27
|
+
await query('VACUUM'); // Clean up disk space
|
|
28
|
+
} else {
|
|
29
|
+
await query('TRUNCATE TABLE Movie_Info');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('Movie_Info table has been cleared.');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('Failed to reset Movie_Info:', err.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
} finally {
|
|
38
|
+
await close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resetMovieTable();
|