Initial commit: Complete Quixotic Telegram MiniApp implementation
- Set up Express.js server with YouTube search and MP3 conversion API - Created Telegram Web App frontend with responsive design - Implemented SQLite database for user management and history - Added Telegram Bot integration with commands and Web App support - Configured FFmpeg-based audio conversion pipeline - Added comprehensive documentation and deployment guides 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
223
src/bot.js
Normal file
223
src/bot.js
Normal file
@@ -0,0 +1,223 @@
|
||||
const TelegramBot = require('node-telegram-bot-api');
|
||||
const Database = require('./database');
|
||||
|
||||
class QuixoticBot {
|
||||
constructor(token, webAppUrl) {
|
||||
this.bot = new TelegramBot(token, { polling: true });
|
||||
this.webAppUrl = webAppUrl;
|
||||
this.db = new Database();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('> Telegram bot initialized');
|
||||
this.setupCommands();
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
setupCommands() {
|
||||
// Set bot commands
|
||||
this.bot.setMyCommands([
|
||||
{ command: 'start', description: '0?CAB8BL ?@8;>65=85' },
|
||||
{ command: 'help', description: '><>IL' },
|
||||
{ command: 'history', description: 'AB>@8O ?>8A:0' }
|
||||
]);
|
||||
}
|
||||
|
||||
setupHandlers() {
|
||||
// Start command
|
||||
this.bot.onText(/\/start/, async (msg) => {
|
||||
const chatId = msg.chat.id;
|
||||
const user = msg.from;
|
||||
|
||||
try {
|
||||
// Add user to database
|
||||
await this.db.addUser(user);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [[
|
||||
{
|
||||
text: '<µ B:@KBL Quixotic',
|
||||
web_app: { url: this.webAppUrl }
|
||||
}
|
||||
]]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId,
|
||||
'<µ >1@> ?>60;>20BL 2 Quixotic!\n\n' +
|
||||
'0948 ;N1CN ?5A=N =0 YouTube 8 ?>;CG8 MP3 D09; ?@O<> 2 G0B.\n\n' +
|
||||
'06<8 :=>?:C =865, GB>1K =0G0BL ?>8A::',
|
||||
{ reply_markup: keyboard }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Start command error:', error);
|
||||
await this.bot.sendMessage(chatId, '@>87>H;0 >H81:0. >?@>1C9B5 ?>765.');
|
||||
}
|
||||
});
|
||||
|
||||
// Help command
|
||||
this.bot.onText(/\/help/, async (msg) => {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
const helpText = `<µ *Quixotic - YouTube to MP3*
|
||||
|
||||
*0: ?>;L7>20BLAO:*
|
||||
1ã 06<8 :=>?:C "B:@KBL Quixotic"
|
||||
2ã 2548 =0720=85 ?5A=8 2 ?>8A:>2CN AB@>:C
|
||||
3ã K15@8 =C6=K9 B@5: 87 A?8A:0
|
||||
4ã >;CG8 MP3 D09; 2 G0B!
|
||||
|
||||
*><0=4K:*
|
||||
/start - 0?CAB8BL ?@8;>65=85
|
||||
/help - -B0 A?@02:0
|
||||
/history - AB>@8O ?>8A:0
|
||||
|
||||
*>7<>6=>AB8:*
|
||||
" >8A: ?> YouTube
|
||||
" KA>:>5 :0G5AB2> MP3 (128kbps)
|
||||
" KAB@0O :>=25@B0F8O
|
||||
" AB>@8O ?>8A:0`;
|
||||
|
||||
await this.bot.sendMessage(chatId, helpText, { parse_mode: 'Markdown' });
|
||||
});
|
||||
|
||||
// History command
|
||||
this.bot.onText(/\/history/, async (msg) => {
|
||||
const chatId = msg.chat.id;
|
||||
const userId = msg.from.id;
|
||||
|
||||
try {
|
||||
const user = await this.db.getUserByTelegramId(userId);
|
||||
if (!user) {
|
||||
await this.bot.sendMessage(chatId, 'AB>@8O ?>8A:0 ?CAB0.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get recent search history
|
||||
const history = await this.db.db.all(
|
||||
`SELECT query, created_at FROM search_history
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10`,
|
||||
[user.id]
|
||||
);
|
||||
|
||||
if (history.length === 0) {
|
||||
await this.bot.sendMessage(chatId, 'AB>@8O ?>8A:0 ?CAB0.');
|
||||
return;
|
||||
}
|
||||
|
||||
let historyText = '=È *>A;54=85 ?>8A:>2K5 70?@>AK:*\n\n';
|
||||
history.forEach((item, index) => {
|
||||
const date = new Date(item.created_at).toLocaleDateString('ru-RU');
|
||||
historyText += `${index + 1}. ${item.query} _(${date})_\n`;
|
||||
});
|
||||
|
||||
await this.bot.sendMessage(chatId, historyText, { parse_mode: 'Markdown' });
|
||||
} catch (error) {
|
||||
console.error('History command error:', error);
|
||||
await this.bot.sendMessage(chatId, 'H81:0 ?>;CG5=8O 8AB>@88.');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle web app data
|
||||
this.bot.on('web_app_data', async (msg) => {
|
||||
const chatId = msg.chat.id;
|
||||
const data = JSON.parse(msg.web_app.data);
|
||||
|
||||
try {
|
||||
if (data.action === 'send_audio') {
|
||||
await this.sendAudioFile(chatId, data.audioUrl, data.title);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Web app data error:', error);
|
||||
await this.bot.sendMessage(chatId, 'H81:0 >1@01>B:8 40==KE.');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle inline queries for search
|
||||
this.bot.on('inline_query', async (query) => {
|
||||
const queryId = query.id;
|
||||
const searchQuery = query.query;
|
||||
|
||||
if (!searchQuery || searchQuery.length < 3) {
|
||||
await this.bot.answerInlineQuery(queryId, []);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const YouTubeService = require('./youtube');
|
||||
const youtube = new YouTubeService();
|
||||
const videos = await youtube.searchVideos(searchQuery, 5);
|
||||
|
||||
const results = videos.map((video, index) => ({
|
||||
type: 'article',
|
||||
id: `${index}`,
|
||||
title: video.title,
|
||||
description: `${video.channel} " ${this.formatDuration(video.duration)}`,
|
||||
thumb_url: video.thumbnail,
|
||||
input_message_content: {
|
||||
message_text: `<µ ${video.title}\n=ú ${video.url}`
|
||||
}
|
||||
}));
|
||||
|
||||
await this.bot.answerInlineQuery(queryId, results, {
|
||||
cache_time: 300,
|
||||
is_personal: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Inline query error:', error);
|
||||
await this.bot.answerInlineQuery(queryId, []);
|
||||
}
|
||||
});
|
||||
|
||||
// Error handler
|
||||
this.bot.on('error', (error) => {
|
||||
console.error('Telegram bot error:', error);
|
||||
});
|
||||
|
||||
console.log(' Bot handlers setup complete');
|
||||
}
|
||||
|
||||
async sendAudioFile(chatId, audioUrl, title) {
|
||||
try {
|
||||
await this.bot.sendMessage(chatId, 'ó >43>B02;820N MP3 D09;...');
|
||||
|
||||
// Send audio file
|
||||
await this.bot.sendAudio(chatId, audioUrl, {
|
||||
title: title,
|
||||
performer: 'Quixotic',
|
||||
caption: `<µ ${title}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Send audio error:', error);
|
||||
await this.bot.sendMessage(chatId,
|
||||
'L 5 C40;>AL >B?@028BL 0C48>D09;. >?@>1C9B5 5I5 @07.\n\n' +
|
||||
`@O<0O AAK;:0: ${audioUrl}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
formatDuration(seconds) {
|
||||
if (!seconds) return '';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize bot if this file is run directly
|
||||
if (require.main === module) {
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
const webAppUrl = process.env.WEB_APP_URL || 'https://your-domain.com';
|
||||
|
||||
if (!token) {
|
||||
console.error('L TELEGRAM_BOT_TOKEN environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
new QuixoticBot(token, webAppUrl);
|
||||
}
|
||||
|
||||
module.exports = QuixoticBot;
|
||||
104
src/database.js
Normal file
104
src/database.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.dbPath = path.join(__dirname, '../database/quixotic.db');
|
||||
this.db = new sqlite3.Database(this.dbPath);
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.db.serialize(() => {
|
||||
// Users table
|
||||
this.db.run(`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
telegram_id INTEGER UNIQUE NOT NULL,
|
||||
username TEXT,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
|
||||
// Search history table
|
||||
this.db.run(`CREATE TABLE IF NOT EXISTS search_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
query TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)`);
|
||||
|
||||
// Downloaded files table
|
||||
this.db.run(`CREATE TABLE IF NOT EXISTS downloads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
youtube_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
file_path TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)`);
|
||||
});
|
||||
}
|
||||
|
||||
addUser(telegramUser) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { id, username, first_name, last_name } = telegramUser;
|
||||
this.db.run(
|
||||
`INSERT OR REPLACE INTO users (telegram_id, username, first_name, last_name)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[id, username, first_name, last_name],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve(this.lastID);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
addSearchHistory(userId, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
`INSERT INTO search_history (user_id, query) VALUES (?, ?)`,
|
||||
[userId, query],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve(this.lastID);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
addDownload(userId, youtubeId, title, filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
`INSERT INTO downloads (user_id, youtube_id, title, file_path) VALUES (?, ?, ?, ?)`,
|
||||
[userId, youtubeId, title, filePath],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve(this.lastID);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getUserByTelegramId(telegramId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get(
|
||||
`SELECT * FROM users WHERE telegram_id = ?`,
|
||||
[telegramId],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Database;
|
||||
166
src/server.js
Normal file
166
src/server.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const Database = require('./database');
|
||||
const YouTubeService = require('./youtube');
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// Initialize services
|
||||
const db = new Database();
|
||||
const youtube = new YouTubeService();
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Ensure downloads directory exists
|
||||
const downloadsDir = path.join(__dirname, '../downloads');
|
||||
if (!fs.existsSync(downloadsDir)) {
|
||||
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
});
|
||||
|
||||
// Search videos
|
||||
app.post('/api/search', async (req, res) => {
|
||||
try {
|
||||
const { query, userId } = req.body;
|
||||
|
||||
if (!query || query.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Query is required' });
|
||||
}
|
||||
|
||||
// Save search history
|
||||
if (userId && userId !== 'demo') {
|
||||
try {
|
||||
const user = await db.getUserByTelegramId(userId);
|
||||
if (user) {
|
||||
await db.addSearchHistory(user.id, query);
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.error('Database error:', dbError);
|
||||
}
|
||||
}
|
||||
|
||||
const videos = await youtube.searchVideos(query.trim());
|
||||
res.json({ videos });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
res.status(500).json({ error: 'Failed to search videos' });
|
||||
}
|
||||
});
|
||||
|
||||
// Convert video to MP3
|
||||
app.post('/api/convert', async (req, res) => {
|
||||
try {
|
||||
const { videoId, title, userId } = req.body;
|
||||
|
||||
if (!videoId) {
|
||||
return res.status(400).json({ error: 'Video ID is required' });
|
||||
}
|
||||
|
||||
// Generate safe filename
|
||||
const safeTitle = title.replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 50);
|
||||
const filename = `${videoId}_${safeTitle}.mp3`;
|
||||
const outputPath = path.join(downloadsDir, filename);
|
||||
|
||||
// Check if file already exists
|
||||
if (fs.existsSync(outputPath)) {
|
||||
const audioUrl = `${req.protocol}://${req.get('host')}/downloads/${filename}`;
|
||||
return res.json({ audioUrl, title });
|
||||
}
|
||||
|
||||
// Get audio stream from YouTube
|
||||
const audioStream = await youtube.getAudioStream(videoId);
|
||||
|
||||
// Convert to MP3 using ffmpeg
|
||||
await new Promise((resolve, reject) => {
|
||||
ffmpeg(audioStream)
|
||||
.audioCodec('libmp3lame')
|
||||
.audioBitrate(128)
|
||||
.format('mp3')
|
||||
.output(outputPath)
|
||||
.on('error', (err) => {
|
||||
console.error('FFmpeg error:', err);
|
||||
reject(err);
|
||||
})
|
||||
.on('end', () => {
|
||||
console.log('Conversion finished:', filename);
|
||||
resolve();
|
||||
})
|
||||
.run();
|
||||
});
|
||||
|
||||
// Save download record
|
||||
if (userId && userId !== 'demo') {
|
||||
try {
|
||||
const user = await db.getUserByTelegramId(userId);
|
||||
if (user) {
|
||||
await db.addDownload(user.id, videoId, title, outputPath);
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.error('Database error:', dbError);
|
||||
}
|
||||
}
|
||||
|
||||
const audioUrl = `${req.protocol}://${req.get('host')}/downloads/${filename}`;
|
||||
res.json({ audioUrl, title });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Conversion error:', error);
|
||||
res.status(500).json({ error: 'Failed to convert video' });
|
||||
}
|
||||
});
|
||||
|
||||
// Serve download files
|
||||
app.use('/downloads', express.static(downloadsDir));
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ error: 'Something went wrong!' });
|
||||
});
|
||||
|
||||
// Cleanup old files periodically (every hour)
|
||||
setInterval(() => {
|
||||
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const now = Date.now();
|
||||
|
||||
fs.readdir(downloadsDir, (err, files) => {
|
||||
if (err) return;
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(downloadsDir, file);
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err) return;
|
||||
|
||||
if (now - stats.mtime.getTime() > maxAge) {
|
||||
fs.unlink(filePath, (err) => {
|
||||
if (!err) {
|
||||
console.log('Deleted old file:', file);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 60 * 60 * 1000); // Run every hour
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`<µ Quixotic server running on port ${port}`);
|
||||
console.log(`=Á Downloads directory: ${downloadsDir}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
173
src/youtube.js
Normal file
173
src/youtube.js
Normal file
@@ -0,0 +1,173 @@
|
||||
const ytdl = require('ytdl-core');
|
||||
const axios = require('axios');
|
||||
|
||||
class YouTubeService {
|
||||
constructor() {
|
||||
// Using YouTube's internal API endpoint for search (no API key needed)
|
||||
this.searchEndpoint = 'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
||||
}
|
||||
|
||||
async searchVideos(query, maxResults = 10) {
|
||||
try {
|
||||
const response = await axios.post(this.searchEndpoint, {
|
||||
context: {
|
||||
client: {
|
||||
clientName: 'WEB',
|
||||
clientVersion: '2.20230728.00.00'
|
||||
}
|
||||
},
|
||||
query: query
|
||||
});
|
||||
|
||||
const contents = response.data?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents;
|
||||
if (!contents) return [];
|
||||
|
||||
const videoResults = [];
|
||||
|
||||
for (const section of contents) {
|
||||
const itemSection = section?.itemSectionRenderer?.contents;
|
||||
if (!itemSection) continue;
|
||||
|
||||
for (const item of itemSection) {
|
||||
const videoRenderer = item?.videoRenderer;
|
||||
if (!videoRenderer) continue;
|
||||
|
||||
const video = this.parseVideoRenderer(videoRenderer);
|
||||
if (video && videoResults.length < maxResults) {
|
||||
videoResults.push(video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return videoResults;
|
||||
} catch (error) {
|
||||
console.error('YouTube search error:', error.message);
|
||||
// Fallback to alternative method
|
||||
return this.searchWithYtdl(query, maxResults);
|
||||
}
|
||||
}
|
||||
|
||||
parseVideoRenderer(videoRenderer) {
|
||||
try {
|
||||
const videoId = videoRenderer.videoId;
|
||||
if (!videoId) return null;
|
||||
|
||||
const title = videoRenderer.title?.runs?.[0]?.text || 'Unknown Title';
|
||||
const channel = videoRenderer.ownerText?.runs?.[0]?.text || 'Unknown Channel';
|
||||
|
||||
// Get thumbnail
|
||||
const thumbnails = videoRenderer.thumbnail?.thumbnails || [];
|
||||
const thumbnail = thumbnails[thumbnails.length - 1]?.url ||
|
||||
`https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
|
||||
|
||||
// Parse duration
|
||||
let duration = 0;
|
||||
const durationText = videoRenderer.lengthText?.simpleText;
|
||||
if (durationText) {
|
||||
duration = this.parseDuration(durationText);
|
||||
}
|
||||
|
||||
return {
|
||||
id: videoId,
|
||||
title,
|
||||
channel,
|
||||
thumbnail: thumbnail.startsWith('//') ? 'https:' + thumbnail : thumbnail,
|
||||
duration,
|
||||
url: `https://www.youtube.com/watch?v=${videoId}`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing video:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async searchWithYtdl(query, maxResults = 10) {
|
||||
// Fallback method using ytdl-core with search
|
||||
try {
|
||||
const searchUrl = `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`;
|
||||
const response = await axios.get(searchUrl);
|
||||
|
||||
const videoIds = this.extractVideoIds(response.data);
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < Math.min(videoIds.length, maxResults); i++) {
|
||||
try {
|
||||
const info = await ytdl.getBasicInfo(videoIds[i]);
|
||||
const videoDetails = info.videoDetails;
|
||||
|
||||
results.push({
|
||||
id: videoDetails.videoId,
|
||||
title: videoDetails.title,
|
||||
channel: videoDetails.ownerChannelName || 'Unknown Channel',
|
||||
thumbnail: videoDetails.thumbnails?.[0]?.url ||
|
||||
`https://img.youtube.com/vi/${videoDetails.videoId}/maxresdefault.jpg`,
|
||||
duration: parseInt(videoDetails.lengthSeconds) || 0,
|
||||
url: videoDetails.video_url
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error getting info for video ${videoIds[i]}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Fallback search error:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
extractVideoIds(html) {
|
||||
const videoIdRegex = /"videoId":"([^"]+)"/g;
|
||||
const videoIds = [];
|
||||
let match;
|
||||
|
||||
while ((match = videoIdRegex.exec(html)) !== null) {
|
||||
if (!videoIds.includes(match[1])) {
|
||||
videoIds.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return videoIds;
|
||||
}
|
||||
|
||||
parseDuration(durationText) {
|
||||
const parts = durationText.split(':').reverse();
|
||||
let seconds = 0;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
seconds += parseInt(parts[i]) * Math.pow(60, i);
|
||||
}
|
||||
|
||||
return seconds;
|
||||
}
|
||||
|
||||
async getVideoInfo(videoId) {
|
||||
try {
|
||||
const info = await ytdl.getBasicInfo(videoId);
|
||||
return {
|
||||
title: info.videoDetails.title,
|
||||
author: info.videoDetails.author.name,
|
||||
length: info.videoDetails.lengthSeconds,
|
||||
formats: info.formats
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting video info:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getAudioStream(videoId) {
|
||||
try {
|
||||
const stream = ytdl(videoId, {
|
||||
quality: 'highestaudio',
|
||||
filter: 'audioonly'
|
||||
});
|
||||
return stream;
|
||||
} catch (error) {
|
||||
console.error('Error getting audio stream:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = YouTubeService;
|
||||
Reference in New Issue
Block a user