Upload files to "/"

This commit is contained in:
kq
2026-08-05 14:35:21 +00:00
commit 3fd9a25135
2 changed files with 387 additions and 0 deletions
+371
View File
@@ -0,0 +1,371 @@
const { Client, GatewayIntentBits } = require('discord.js');
const {
joinVoiceChannel,
createAudioPlayer,
createAudioResource,
AudioPlayerStatus,
VoiceConnectionStatus,
entersState,
StreamType,
} = require('@discordjs/voice');
const { spawn } = require('child_process');
const { PassThrough } = require('stream');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// ─── Config ──────────────────────────────────────────────────────────────────
const TOKEN = process.env.DISCORD_TOKEN || 'MTI4NzgyMDY5MjY2MTg2MjQ3MQ.GT9ssR.TXNSDgclq8JcLYpGcE8YUEqQy99Dg7jvW4hTQA';
const PREFIX = '!';
const VOLUME = 0.3; // 0.0 - 1.0
// ─── Cache setup ─────────────────────────────────────────────────────────────
const CACHE_DIR = path.join(__dirname, 'cache');
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR);
// Metadata cache: maps query/url → { title, duration, url }
// Saved to disk so it survives bot restarts
const META_FILE = path.join(CACHE_DIR, 'metadata.json');
let metaCache = {};
if (fs.existsSync(META_FILE)) {
try { metaCache = JSON.parse(fs.readFileSync(META_FILE, 'utf8')); }
catch { metaCache = {}; }
}
function saveMetaCache() {
fs.writeFileSync(META_FILE, JSON.stringify(metaCache, null, 2));
}
function getAudioCachePath(url) {
const hash = crypto.createHash('md5').update(url).digest('hex');
return path.join(CACHE_DIR, `${hash}.opus`);
}
// ─── State (per guild) ────────────────────────────────────────────────────────
const guilds = new Map();
function getState(guildId) {
if (!guilds.has(guildId)) {
guilds.set(guildId, {
connection: null,
player: null,
queue: [],
playing: false,
textChannel: null,
});
}
return guilds.get(guildId);
}
// ─── Discord client ───────────────────────────────────────────────────────────
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
client.once('ready', () => {
console.log(`✅ Logged in as ${client.user.tag}`);
});
client.on('messageCreate', async (message) => {
if (message.author.bot) return;
if (!message.content.startsWith(PREFIX)) return;
const args = message.content.slice(PREFIX.length).trim().split(/ +/);
const command = args.shift().toLowerCase();
try {
if (command === 'play') {
const query = args.join(' ');
if (!query) return message.channel.send('❌ Podaj link lub frazę do wyszukania!');
await cmdPlay(message, query);
} else if (command === 'skip') {
await cmdSkip(message);
} else if (command === 'queue') {
await cmdQueue(message);
} else if (command === 'stop') {
await cmdStop(message);
} else if (command === 'leave') {
await cmdLeave(message);
} else if (command === 'help') {
await message.channel.send(
'🎵 **Komendy:**\n' +
'`!play <url/szukaj>` dodaj utwór do kolejki\n' +
'`!skip` pomiń aktualny utwór\n' +
'`!queue` pokaż kolejkę\n' +
'`!stop` zatrzymaj i wyczyść kolejkę\n' +
'`!leave` rozłącz bota'
);
}
} catch (err) {
console.error(`[ERROR] command=${command}`, err);
message.channel.send(`⚠️ Błąd: ${err.message}`);
}
});
// ─── Commands ─────────────────────────────────────────────────────────────────
async function cmdPlay(message, query) {
const voiceChannel = message.member?.voice?.channel;
if (!voiceChannel) return message.channel.send('❌ Dołącz do kanału głosowego!');
const state = getState(message.guildId);
state.textChannel = message.channel;
// Join voice if not already connected
if (!state.connection ||
state.connection.state.status === VoiceConnectionStatus.Destroyed ||
state.connection.state.status === VoiceConnectionStatus.Disconnected) {
console.log(`[Voice] Connecting to: ${voiceChannel.name}`);
state.connection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: message.guildId,
adapterCreator: message.guild.voiceAdapterCreator,
selfDeaf: true,
});
state.connection.on('stateChange', (o, n) => console.log(`[Voice] ${o.status}${n.status}`));
state.connection.on('error', (err) => console.error('[Voice error]', err.message));
try {
await entersState(state.connection, VoiceConnectionStatus.Ready, 30_000);
console.log('[Voice] Ready!');
} catch (err) {
console.error('[Voice] Connect failed:', err.message);
state.connection.destroy();
state.connection = null;
return message.channel.send('❌ Nie udało się połączyć z kanałem głosowym.');
}
}
// Create player if needed
if (!state.player) {
state.player = createAudioPlayer();
state.connection.subscribe(state.player);
state.player.on(AudioPlayerStatus.Idle, () => {
state.playing = false;
playNext(message.guildId);
});
state.player.on('error', (err) => {
console.error('[Player error]', err.message);
state.playing = false;
state.textChannel?.send(`⚠️ Błąd odtwarzacza: ${err.message}`);
playNext(message.guildId);
});
}
// Check metadata cache first — if known, skip the yt-dlp lookup entirely
const cacheKey = query.toLowerCase().trim();
if (metaCache[cacheKey]) {
const song = metaCache[cacheKey];
const audioCached = fs.existsSync(getAudioCachePath(song.url));
console.log(`[Meta] HIT: "${query}" → ${song.url} (audio cached: ${audioCached})`);
state.queue.push(song);
await message.channel.send(
`${audioCached ? '⚡ Błyskawicznie z cache: ' : 'Dodano do kolejki: '}**${song.title}** [${song.duration}]`
);
if (!state.playing) playNext(message.guildId);
return;
}
// Not in cache — fetch metadata with yt-dlp
await message.channel.send(`🔍 Szukam: **${query}**…`);
const song = await resolveSong(query);
// Save to metadata cache under both the original query and the resolved URL
metaCache[cacheKey] = song;
metaCache[song.url.toLowerCase()] = song;
saveMetaCache();
console.log(`[Meta] Cached: "${query}" → ${song.url}`);
state.queue.push(song);
await message.channel.send(`✅ Dodano do kolejki: **${song.title}** [${song.duration}]`);
if (!state.playing) playNext(message.guildId);
}
async function cmdSkip(message) {
const state = getState(message.guildId);
if (!state.playing || !state.player) {
return message.channel.send('❌ Nic nie jest teraz odtwarzane.');
}
state.player.stop();
await message.channel.send('⏭️ Pominięto utwór.');
}
async function cmdQueue(message) {
const state = getState(message.guildId);
if (state.queue.length === 0) {
return message.channel.send('📭 Kolejka jest pusta.');
}
const list = state.queue
.map((s, i) => `${i + 1}. **${s.title}** [${s.duration}]`)
.join('\n');
await message.channel.send(`📜 **Kolejka:**\n${list}`);
}
async function cmdStop(message) {
const state = getState(message.guildId);
state.queue = [];
state.player?.stop();
state.playing = false;
await message.channel.send('⏹️ Zatrzymano i wyczyszczono kolejkę.');
}
async function cmdLeave(message) {
const state = getState(message.guildId);
state.queue = [];
state.player?.stop();
state.playing = false;
state.connection?.destroy();
state.connection = null;
state.player = null;
await message.channel.send('👋 Rozłączono.');
}
// ─── Playback ─────────────────────────────────────────────────────────────────
function playNext(guildId) {
const state = getState(guildId);
if (state.queue.length === 0) {
state.playing = false;
state.textChannel?.send('📭 Kolejka zakończona.');
return;
}
const song = state.queue.shift();
state.playing = true;
state.textChannel?.send(`▶️ Odtwarzam: **${song.title}** [${song.duration}]`);
try {
const resource = buildAudioResource(song.url);
state.player.play(resource);
} catch (err) {
console.error('[playNext error]', err);
state.textChannel?.send(`⚠️ Nie można odtworzyć: ${err.message}`);
state.playing = false;
playNext(guildId);
}
}
// ─── Audio resource with caching ─────────────────────────────────────────────
function buildAudioResource(url) {
const cachePath = getAudioCachePath(url);
// Audio cache HIT — instant play from disk
if (fs.existsSync(cachePath)) {
console.log(`[Cache] HIT: ${path.basename(cachePath)}`);
return createAudioResource(fs.createReadStream(cachePath), {
inputType: StreamType.OggOpus,
});
}
// Audio cache MISS — stream + save to disk simultaneously
console.log(`[Cache] MISS: downloading ${url}`);
const ytdlp = spawn('yt-dlp', [
'-f', 'bestaudio',
'--no-playlist',
'-o', '-',
'--', url,
], { stdio: ['ignore', 'pipe', 'pipe'] });
ytdlp.stderr.on('data', (d) => console.error('[yt-dlp]', d.toString().trim()));
const ffmpeg = spawn('ffmpeg', [
'-hide_banner', '-loglevel', 'error',
'-i', 'pipe:0',
'-af', `volume=${VOLUME}`,
'-c:a', 'libopus',
'-b:a', '128k',
'-f', 'opus',
'pipe:1',
], { stdio: ['pipe', 'pipe', 'pipe'] });
ffmpeg.stderr.on('data', (d) => console.error('[ffmpeg]', d.toString().trim()));
ytdlp.stdout.pipe(ffmpeg.stdin);
ytdlp.on('exit', (code) => {
if (code !== 0 && code !== null) {
console.error(`[yt-dlp] exited with code ${code}`);
ffmpeg.stdin.destroy();
}
});
// Tee: Discord stream AND cache file at the same time
const tee = new PassThrough();
const cacheStream = fs.createWriteStream(cachePath);
ffmpeg.stdout.pipe(tee);
tee.pipe(cacheStream);
cacheStream.on('finish', () => console.log(`[Cache] Saved: ${path.basename(cachePath)}`));
cacheStream.on('error', (err) => {
console.error('[Cache] Write error:', err.message);
if (fs.existsSync(cachePath)) fs.unlinkSync(cachePath);
});
const resource = createAudioResource(tee, {
inputType: StreamType.OggOpus,
});
resource.playStream.on('close', () => {
if (!ytdlp.killed) ytdlp.kill();
if (!ffmpeg.killed) ffmpeg.kill();
});
return resource;
}
// ─── yt-dlp metadata fetch ────────────────────────────────────────────────────
function resolveSong(query) {
const isUrl = query.startsWith('http://') || query.startsWith('https://');
const ytQuery = isUrl ? query : `ytsearch1:${query}`;
return new Promise((resolve, reject) => {
const proc = spawn('yt-dlp', [
'--no-playlist',
'--print', '%(title)s|||%(duration_string)s|||%(webpage_url)s',
'--', ytQuery,
]);
let stdout = '';
let stderr = '';
proc.stdout.on('data', (d) => stdout += d.toString());
proc.stderr.on('data', (d) => stderr += d.toString());
proc.on('close', (code) => {
const line = stdout.split('\n').find(l => l.includes('|||')) || '';
const parts = line.split('|||');
if (parts.length >= 3) {
resolve({
title: parts[0].trim(),
duration: parts[1].trim(),
url: parts[2].trim(),
});
} else if (code !== 0) {
reject(new Error(stderr.trim() || `yt-dlp exited with code ${code}`));
} else {
resolve({ title: query, duration: '?:??', url: query });
}
});
});
}
// ─── Start ────────────────────────────────────────────────────────────────────
client.login(TOKEN);