ts
This commit is contained in:
223
src/bot.js
223
src/bot.js
@@ -1,223 +0,0 @@
|
||||
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;
|
||||
275
src/bot.ts
Normal file
275
src/bot.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
import { Database } from './database';
|
||||
|
||||
interface TelegramUser {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
chat: {
|
||||
id: number;
|
||||
};
|
||||
from?: TelegramUser;
|
||||
web_app?: {
|
||||
data: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface InlineQuery {
|
||||
id: string;
|
||||
query: string;
|
||||
}
|
||||
|
||||
interface WebAppData {
|
||||
action: string;
|
||||
audioUrl: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
query: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export class QuixoticBot {
|
||||
private bot: TelegramBot;
|
||||
private webAppUrl: string;
|
||||
private db: Database;
|
||||
|
||||
constructor(token: string, webAppUrl: string) {
|
||||
this.bot = new TelegramBot(token, { polling: true });
|
||||
this.webAppUrl = webAppUrl;
|
||||
this.db = new Database();
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
console.log('🤖 Telegram bot initialized');
|
||||
this.setupCommands();
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupCommands(): void {
|
||||
// Set bot commands
|
||||
this.bot.setMyCommands([
|
||||
{ command: 'start', description: 'Запустить приложение' },
|
||||
{ command: 'help', description: 'Помощь' },
|
||||
{ command: 'history', description: 'История поиска' }
|
||||
]);
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
// Start command
|
||||
this.bot.onText(/\/start/, async (msg: Message) => {
|
||||
const chatId = msg.chat.id;
|
||||
const user = msg.from;
|
||||
|
||||
try {
|
||||
// Add user to database
|
||||
if (user) {
|
||||
await this.db.addUser(user);
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [[
|
||||
{
|
||||
text: '🎵 Открыть Quixotic',
|
||||
web_app: { url: this.webAppUrl }
|
||||
}
|
||||
]]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId,
|
||||
'🎵 Добро пожаловать в Quixotic!\n\n' +
|
||||
'Найди любую песню на YouTube и получи MP3 файл прямо в чат.\n\n' +
|
||||
'Нажми кнопку ниже, чтобы начать поиск:',
|
||||
{ reply_markup: keyboard }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Start command error:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка. Попробуйте позже.');
|
||||
}
|
||||
});
|
||||
|
||||
// Help command
|
||||
this.bot.onText(/\/help/, async (msg: Message) => {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
const helpText = `🎵 *Quixotic - YouTube to MP3*
|
||||
|
||||
*Как пользоваться:*
|
||||
1️⃣ Нажми кнопку "Открыть Quixotic"
|
||||
2️⃣ Введи название песни в поисковую строку
|
||||
3️⃣ Выбери нужный трек из списка
|
||||
4️⃣ Получи MP3 файл в чат!
|
||||
|
||||
*Команды:*
|
||||
/start - Запустить приложение
|
||||
/help - Эта справка
|
||||
/history - История поиска
|
||||
|
||||
*Возможности:*
|
||||
✅ Поиск по YouTube
|
||||
✅ Высокое качество MP3 (128kbps)
|
||||
✅ Быстрая конвертация
|
||||
✅ История поиска`;
|
||||
|
||||
await this.bot.sendMessage(chatId, helpText, { parse_mode: 'Markdown' });
|
||||
});
|
||||
|
||||
// History command
|
||||
this.bot.onText(/\/history/, async (msg: Message) => {
|
||||
const chatId = msg.chat.id;
|
||||
const userId = msg.from?.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
try {
|
||||
const user = await this.db.getUserByTelegramId(userId);
|
||||
if (!user) {
|
||||
await this.bot.sendMessage(chatId, 'История поиска пуста.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get recent search history
|
||||
const history = await this.getSearchHistory(user.id);
|
||||
|
||||
if (history.length === 0) {
|
||||
await this.bot.sendMessage(chatId, 'История поиска пуста.');
|
||||
return;
|
||||
}
|
||||
|
||||
let historyText = '📋 *Последние поисковые запросы:*\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, '❌ Ошибка получения истории.');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle web app data
|
||||
this.bot.on('web_app_data', async (msg: Message) => {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
if (!msg.web_app?.data) return;
|
||||
|
||||
const data: WebAppData = 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, '❌ Ошибка обработки данных.');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle inline queries for search
|
||||
this.bot.on('inline_query', async (query: InlineQuery) => {
|
||||
const queryId = query.id;
|
||||
const searchQuery = query.query;
|
||||
|
||||
if (!searchQuery || searchQuery.length < 3) {
|
||||
await this.bot.answerInlineQuery(queryId, []);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { SoundCloudService } = require('./soundcloud');
|
||||
const soundcloud = new SoundCloudService();
|
||||
const videos = await soundcloud.searchTracks(searchQuery, 5);
|
||||
|
||||
const results = videos.map((video: any, index: number) => ({
|
||||
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: Error) => {
|
||||
console.error('Telegram bot error:', error);
|
||||
});
|
||||
|
||||
console.log('✅ Bot handlers setup complete');
|
||||
}
|
||||
|
||||
private async getSearchHistory(userId: number): Promise<SearchResult[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db['db'].all(
|
||||
`SELECT query, created_at FROM search_history
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10`,
|
||||
[userId],
|
||||
(err: Error | null, rows: SearchResult[]) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows || []);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async sendAudioFile(chatId: number, audioUrl: string, title: string): Promise<void> {
|
||||
try {
|
||||
await this.bot.sendMessage(chatId, '⏳ Подготавливаю MP3 файл...');
|
||||
|
||||
// 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,
|
||||
'❌ Не удалось отправить аудиофайл. Попробуйте еще раз.\n\n' +
|
||||
`Прямая ссылка: ${audioUrl}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private formatDuration(seconds: number): string {
|
||||
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('❌ TELEGRAM_BOT_TOKEN environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
new QuixoticBot(token, webAppUrl);
|
||||
}
|
||||
@@ -1,14 +1,33 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
import sqlite3 from 'sqlite3';
|
||||
import path from 'path';
|
||||
|
||||
interface TelegramUser {
|
||||
id: number;
|
||||
username?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
telegram_id: number;
|
||||
username?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export class Database {
|
||||
private dbPath: string;
|
||||
private db: sqlite3.Database;
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.dbPath = path.join(__dirname, '../database/quixotic.db');
|
||||
this.db = new sqlite3.Database(this.dbPath);
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
private init(): void {
|
||||
this.db.serialize(() => {
|
||||
// Users table
|
||||
this.db.run(`CREATE TABLE IF NOT EXISTS users (
|
||||
@@ -42,14 +61,14 @@ class Database {
|
||||
});
|
||||
}
|
||||
|
||||
addUser(telegramUser) {
|
||||
addUser(telegramUser: TelegramUser): Promise<number> {
|
||||
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) {
|
||||
function(err: Error | null) {
|
||||
if (err) reject(err);
|
||||
else resolve(this.lastID);
|
||||
}
|
||||
@@ -57,12 +76,12 @@ class Database {
|
||||
});
|
||||
}
|
||||
|
||||
addSearchHistory(userId, query) {
|
||||
addSearchHistory(userId: number, query: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'INSERT INTO search_history (user_id, query) VALUES (?, ?)',
|
||||
[userId, query],
|
||||
function(err) {
|
||||
function(err: Error | null) {
|
||||
if (err) reject(err);
|
||||
else resolve(this.lastID);
|
||||
}
|
||||
@@ -70,12 +89,12 @@ class Database {
|
||||
});
|
||||
}
|
||||
|
||||
addDownload(userId, youtubeId, title, filePath) {
|
||||
addDownload(userId: number, youtubeId: string, title: string, filePath: string): Promise<number> {
|
||||
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) {
|
||||
function(err: Error | null) {
|
||||
if (err) reject(err);
|
||||
else resolve(this.lastID);
|
||||
}
|
||||
@@ -83,12 +102,12 @@ class Database {
|
||||
});
|
||||
}
|
||||
|
||||
getUserByTelegramId(telegramId) {
|
||||
getUserByTelegramId(telegramId: string | number): Promise<User | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get(
|
||||
'SELECT * FROM users WHERE telegram_id = ?',
|
||||
[telegramId],
|
||||
(err, row) => {
|
||||
(err: Error | null, row: User) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
@@ -96,9 +115,7 @@ class Database {
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
close(): void {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Database;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const Database = require('./database');
|
||||
const SoundCloudService = require('./soundcloud');
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { Database } from './database';
|
||||
import { SoundCloudService } from './soundcloud';
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
@@ -23,14 +23,14 @@ if (!fs.existsSync(downloadsDir)) {
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
app.get('/', (req: Request, res: Response) => {
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
});
|
||||
|
||||
// Search videos
|
||||
app.post('/api/search', async (req, res) => {
|
||||
app.post('/api/search', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { query, userId } = req.body;
|
||||
const { query, userId }: { query?: string; userId?: string } = req.body;
|
||||
|
||||
if (!query || query.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Query is required' });
|
||||
@@ -58,9 +58,9 @@ app.post('/api/search', async (req, res) => {
|
||||
});
|
||||
|
||||
// Convert video to MP3
|
||||
app.post('/api/convert', async (req, res) => {
|
||||
app.post('/api/convert', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { videoId, title, userId } = req.body;
|
||||
const { videoId, title, userId }: { videoId?: string; title?: string; userId?: string } = req.body;
|
||||
console.log('Convert request received:', { videoId, title, userId });
|
||||
|
||||
if (!videoId) {
|
||||
@@ -68,7 +68,7 @@ app.post('/api/convert', async (req, res) => {
|
||||
}
|
||||
|
||||
// Generate safe filename
|
||||
const safeTitle = title.replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 50);
|
||||
const safeTitle = (title || '').replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 50);
|
||||
const filename = `${videoId}_${safeTitle}.mp3`;
|
||||
const outputPath = path.join(downloadsDir, filename);
|
||||
|
||||
@@ -88,7 +88,7 @@ app.post('/api/convert', async (req, res) => {
|
||||
console.log('Audio stream obtained, starting FFmpeg conversion...');
|
||||
|
||||
// Convert to MP3 using ffmpeg
|
||||
await new Promise((resolve, reject) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const conversion = ffmpeg(audioStream)
|
||||
.audioCodec('libmp3lame')
|
||||
.audioBitrate('192k')
|
||||
@@ -96,10 +96,10 @@ app.post('/api/convert', async (req, res) => {
|
||||
.audioFrequency(44100)
|
||||
.format('mp3')
|
||||
.output(outputPath)
|
||||
.on('start', (command) => {
|
||||
.on('start', (command: string) => {
|
||||
console.log('FFmpeg started:', command);
|
||||
})
|
||||
.on('progress', (progress) => {
|
||||
.on('progress', (progress: any) => {
|
||||
if (progress.percent) {
|
||||
console.log(`Conversion progress: ${Math.round(progress.percent)}%`);
|
||||
}
|
||||
@@ -108,7 +108,7 @@ app.post('/api/convert', async (req, res) => {
|
||||
console.log('MP3 conversion completed successfully');
|
||||
resolve();
|
||||
})
|
||||
.on('error', (err) => {
|
||||
.on('error', (err: Error) => {
|
||||
console.error('FFmpeg error:', err.message);
|
||||
reject(err);
|
||||
});
|
||||
@@ -121,7 +121,7 @@ app.post('/api/convert', async (req, res) => {
|
||||
try {
|
||||
const user = await db.getUserByTelegramId(userId);
|
||||
if (user) {
|
||||
await db.addDownload(user.id, videoId, title, outputPath);
|
||||
await db.addDownload(user.id, videoId, title || '', outputPath);
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.error('Database error:', dbError);
|
||||
@@ -132,7 +132,7 @@ app.post('/api/convert', async (req, res) => {
|
||||
console.log('Conversion successful, file available at:', audioUrl);
|
||||
res.json({ audioUrl, title });
|
||||
|
||||
} catch (conversionError) {
|
||||
} catch (conversionError: any) {
|
||||
console.error('Conversion failed for video:', videoId);
|
||||
console.error('Error details:', conversionError.message);
|
||||
console.error('Full error:', conversionError);
|
||||
@@ -155,12 +155,12 @@ app.post('/api/convert', async (req, res) => {
|
||||
app.use('/downloads', express.static(downloadsDir));
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
app.get('/health', (req: Request, res: Response) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ error: 'Something went wrong!' });
|
||||
});
|
||||
@@ -196,4 +196,4 @@ app.listen(port, () => {
|
||||
console.log(`Open in browser: http://localhost:${port}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
export default app;
|
||||
@@ -1,12 +1,44 @@
|
||||
const scdl = require('soundcloud-downloader').default;
|
||||
import scdl from 'soundcloud-downloader';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
interface SearchTrack {
|
||||
id: number;
|
||||
title: string;
|
||||
user?: {
|
||||
username: string;
|
||||
avatar_url?: string;
|
||||
};
|
||||
artwork_url?: string;
|
||||
duration: number;
|
||||
permalink_url: string;
|
||||
streamable: boolean;
|
||||
downloadable: boolean;
|
||||
}
|
||||
|
||||
class SoundCloudService {
|
||||
interface TrackResult {
|
||||
id: number;
|
||||
title: string;
|
||||
channel: string;
|
||||
thumbnail: string;
|
||||
duration: number;
|
||||
url: string;
|
||||
streamable: boolean;
|
||||
downloadable: boolean;
|
||||
}
|
||||
|
||||
interface TrackInfo {
|
||||
title: string;
|
||||
author: string;
|
||||
length: number;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export class SoundCloudService {
|
||||
constructor() {
|
||||
console.log('SoundCloud service initialized');
|
||||
}
|
||||
|
||||
async searchTracks(query, maxResults = 10) {
|
||||
async searchTracks(query: string, maxResults: number = 10): Promise<TrackResult[]> {
|
||||
try {
|
||||
console.log(`Searching SoundCloud for: ${query}`);
|
||||
|
||||
@@ -15,14 +47,14 @@ class SoundCloudService {
|
||||
query: query,
|
||||
limit: maxResults,
|
||||
resourceType: 'tracks'
|
||||
});
|
||||
}) as any;
|
||||
|
||||
if (!tracks || tracks.length === 0) {
|
||||
console.log('No tracks found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const trackResults = tracks.map(track => ({
|
||||
const trackResults: TrackResult[] = tracks.map(track => ({
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
channel: track.user?.username || 'Unknown Artist',
|
||||
@@ -36,15 +68,15 @@ class SoundCloudService {
|
||||
console.log(`Found ${trackResults.length} tracks on SoundCloud`);
|
||||
return trackResults;
|
||||
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('SoundCloud search error:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getTrackInfo(trackId) {
|
||||
async getTrackInfo(trackId: string | number): Promise<TrackInfo> {
|
||||
try {
|
||||
const track = await scdl.getInfo(trackId);
|
||||
const track = await scdl.getInfo(String(trackId)) as SearchTrack;
|
||||
return {
|
||||
title: track.title,
|
||||
author: track.user?.username || 'Unknown',
|
||||
@@ -57,12 +89,12 @@ class SoundCloudService {
|
||||
}
|
||||
}
|
||||
|
||||
async getAudioStream(trackId) {
|
||||
async getAudioStream(trackId: string | number): Promise<Readable> {
|
||||
try {
|
||||
console.log(`Getting audio stream for track: ${trackId}`);
|
||||
|
||||
// Get track info first
|
||||
const trackInfo = await scdl.getInfo(trackId);
|
||||
const trackInfo = await scdl.getInfo(String(trackId)) as SearchTrack;
|
||||
|
||||
if (!trackInfo.streamable) {
|
||||
throw new Error('Track is not streamable');
|
||||
@@ -73,12 +105,12 @@ class SoundCloudService {
|
||||
console.log(`Duration: ${Math.floor(trackInfo.duration / 1000)}s`);
|
||||
|
||||
// Get audio stream
|
||||
const stream = await scdl.download(trackId);
|
||||
const stream = await scdl.download(String(trackId));
|
||||
|
||||
console.log('Audio stream obtained successfully from SoundCloud');
|
||||
return stream;
|
||||
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('SoundCloud download failed:', error.message);
|
||||
|
||||
// Try alternative approach
|
||||
@@ -90,12 +122,10 @@ class SoundCloudService {
|
||||
console.log('Audio stream obtained with alternative method');
|
||||
return stream;
|
||||
|
||||
} catch (fallbackError) {
|
||||
} catch (fallbackError: any) {
|
||||
console.error('Alternative method also failed:', fallbackError.message);
|
||||
throw new Error(`SoundCloud download failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SoundCloudService;
|
||||
}
|
||||
Reference in New Issue
Block a user