does not work

This commit is contained in:
Andrey Kondratev
2025-08-27 18:37:44 +05:00
parent 3d6836dc30
commit 98787a382e
17 changed files with 9526 additions and 245 deletions

3
.gitignore vendored
View File

@@ -9,6 +9,9 @@ yarn-error.log*
.env.local
.env.production
# YouTube authentication
youtube-cookies.json
# Database
database/*.db
database/*.sqlite

15
.mcp.json Normal file
View File

@@ -0,0 +1,15 @@
{
"mcpServers": {
"serena": {
"type": "stdio",
"command": "uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena-mcp-server",
"--enable-web-dashboard",
"false"
]
}
}
}

Binary file not shown.

69
.serena/project.yml Normal file
View File

@@ -0,0 +1,69 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: |
You are an autonomous coding agent. After every major step, tool execution, analysis, or code edit (e.g., reading files, running shell commands, or modifying code), automatically summarize the action, key findings, and next steps in a concise note. Use the 'write_memory' tool to save it as a memory file named 'progress_note_[timestamp or step number]' (e.g., 'progress_note_20250826_1231' or 'progress_note_step1'). Do this without asking for user permission or waiting for input, but briefly mention in your response that a memory was saved. If the task pauses or context is nearly full, write a summary memory with the current state and instructions for continuing. Be frugal with context and avoid reading unnecessary code bodies.
project_name: "quixotic"

View File

@@ -18,17 +18,19 @@ Telegram miniapp для поиска музыки на YouTube и конверт
```bash
git clone <repository-url>
cd quixotic
npm install
yarn install
```
### 2. Установка FFmpeg
**macOS:**
```bash
brew install ffmpeg
```
**Ubuntu/Debian:**
```bash
sudo apt update
sudo apt install ffmpeg
@@ -51,6 +53,7 @@ cp .env.example .env
```
Отредактируйте `.env`:
```env
TELEGRAM_BOT_TOKEN=your_bot_token_here
WEB_APP_URL=https://your-domain.com
@@ -60,18 +63,20 @@ PORT=3000
## Запуск
### Разработка
```bash
npm run dev
yarn dev
```
### Продакшн
```bash
npm start
yarn start
```
## Структура проекта
```
```bash
quixotic/
├── src/
│ ├── server.js # Express сервер
@@ -97,6 +102,7 @@ quixotic/
## База данных
SQLite с таблицами:
- `users` - пользователи Telegram
- `search_history` - история поиска
- `downloads` - загруженные файлы
@@ -107,23 +113,27 @@ SQLite с таблицами:
1. Установите [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli)
2. Создайте приложение:
```bash
heroku create quixotic-app
```
3. Установите buildpacks:
```bash
heroku buildpacks:add --index 1 https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git
heroku buildpacks:add --index 2 heroku/nodejs
```
4. Настройте переменные:
```bash
heroku config:set TELEGRAM_BOT_TOKEN=your_token
heroku config:set WEB_APP_URL=https://quixotic-app.herokuapp.com
```
5. Деплой:
```bash
git push heroku main
```
@@ -144,7 +154,7 @@ sudo apt install ffmpeg -y
# Клонирование проекта
git clone <repository-url>
cd quixotic
npm install
yarn install
# Настройка PM2
sudo npm install -g pm2

70
YOUTUBE_SETUP.md Normal file
View File

@@ -0,0 +1,70 @@
# YouTube Authentication Setup
Чтобы обойти блокировку YouTube "Sign in to confirm you're not a bot", нужно использовать cookies из вашего авторизованного браузера.
## Шаг 1: Получение cookies
1. Откройте Chrome/Firefox и зайдите на [youtube.com](https://youtube.com)
2. Убедитесь что вы авторизованы в своем аккаунте
3. Нажмите F12 чтобы открыть Developer Tools
4. Перейдите на вкладку **Application** (Chrome) или **Storage** (Firefox)
5. В левом меню найдите **Cookies****https://www.youtube.com**
6. Найдите и скопируйте значения следующих cookies:
### Обязательные cookies:
- `__Secure-1PSID`
- `__Secure-3PSID`
- `__Secure-1PAPISID`
- `__Secure-3PAPISID`
### Дополнительные (рекомендуемые):
- `VISITOR_INFO1_LIVE`
- `YSC`
## Шаг 2: Настройка файла
1. Откройте файл `youtube-cookies.json` в корне проекта
2. Замените `your_*_value_here` на реальные значения из браузера:
```json
{
"comment": "Replace these values with your actual YouTube cookies from browser",
"__Secure-1PSID": "СКОПИРОВАННОЕ_ЗНАЧЕНИЕ_ЗДЕСЬ",
"__Secure-3PSID": "СКОПИРОВАННОЕ_ЗНАЧЕНИЕ_ЗДЕСЬ",
"__Secure-1PAPISID": "СКОПИРОВАННОЕ_ЗНАЧЕНИЕ_ЗДЕСЬ",
"__Secure-3PAPISID": "СКОПИРОВАННОЕ_ЗНАЧЕНИЕ_ЗДЕСЬ",
"VISITOR_INFO1_LIVE": "СКОПИРОВАННОЕ_ЗНАЧЕНИЕ_ЗДЕСЬ",
"YSC": "СКОПИРОВАННОЕ_ЗНАЧЕНИЕ_ЗДЕСЬ"
}
```
## Шаг 3: Перезапуск сервера
После настройки cookies перезапустите сервер:
```bash
yarn start
```
В логах вы должны увидеть:
```
YouTube cookies loaded successfully
```
## Примечания
- Cookies периодически истекают, и их нужно обновлять
- Файл `youtube-cookies.json` добавлен в `.gitignore` для безопасности
- Если cookies не работают, попробуйте обновить их из браузера
- В случае проблем сервер автоматически переключится на анонимный доступ
## Альтернативный способ
Если cookies не помогают, можно установить системный `yt-dlp`:
```bash
# macOS
brew install yt-dlp
# Затем использовать через exec в Node.js
```

39
eslint.config.js Normal file
View File

@@ -0,0 +1,39 @@
export default [
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: 2022,
sourceType: "commonjs",
globals: {
// Node.js globals
console: "readonly",
process: "readonly",
Buffer: "readonly",
__dirname: "readonly",
__filename: "readonly",
module: "readonly",
require: "readonly",
exports: "readonly",
global: "readonly",
setTimeout: "readonly",
clearTimeout: "readonly",
setInterval: "readonly",
clearInterval: "readonly",
URLSearchParams: "readonly",
// Browser globals (for public/ files)
window: "readonly",
document: "readonly",
fetch: "readonly",
event: "readonly"
}
},
rules: {
"indent": ["error", 4],
"quotes": ["error", "single"],
"semi": ["error", "always"],
"no-unused-vars": "warn",
"no-console": "off",
"no-undef": "error"
}
}
];

5710
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,17 +5,24 @@
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
"dev": "nodemon src/server.js",
"lint": "eslint src/ public/",
"lint:fix": "eslint src/ public/ --fix",
"validate": "npm run lint && node -c src/server.js && echo '✅ All checks passed!'",
"pretest": "npm run validate"
},
"packageManager": "yarn@1.22.19",
"dependencies": {
"axios": "^1.6.2",
"express": "^4.18.2",
"sqlite3": "^5.1.6",
"node-telegram-bot-api": "^0.64.0",
"ytdl-core": "^4.11.5",
"fluent-ffmpeg": "^2.1.2",
"axios": "^1.6.2"
"node-fetch": "^3.3.2",
"node-telegram-bot-api": "^0.64.0",
"soundcloud-downloader": "^1.0.0",
"sqlite3": "^5.1.6"
},
"devDependencies": {
"eslint": "^9.34.0",
"nodemon": "^3.0.2"
},
"engines": {

View File

@@ -10,20 +10,20 @@
<body>
<div class="container">
<header>
<h1><µ Quixotic</h1>
<p>0948 8 A:0G09 <C7K:C 87 YouTube</p>
<h1>🎵 Quixotic</h1>
<p>Найди и скачай музыку из YouTube</p>
</header>
<div class="search-section">
<div class="search-container">
<input type="text" id="searchInput" placeholder="2548B5 =0720=85 ?5A=8..." autocomplete="off">
<button id="searchBtn">=
<input type="text" id="searchInput" placeholder="Введите название песни..." autocomplete="off">
<button id="searchBtn">🔍</button>
</div>
</div>
<div id="loading" class="loading hidden">
<div class="spinner"></div>
<div class="spinner"></div>
<p>Поиск...</p>
</div>
<div id="results" class="results-container">
@@ -31,7 +31,7 @@
</div>
<div id="noResults" class="no-results hidden">
<div id="noResults" class="no-results hidden">
<p>Ничего не найдено. Попробуйте другой запрос.</p>
</div>
</div>

View File

@@ -100,10 +100,12 @@ class QuixoticApp {
}
async convertVideo(videoId, title) {
console.log('Convert video called:', { videoId, title });
const videoElement = event.currentTarget;
videoElement.classList.add('converting');
try {
console.log('Sending convert request...');
const response = await fetch('/api/convert', {
method: 'POST',
headers: {
@@ -116,29 +118,55 @@ class QuixoticApp {
})
});
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error('Conversion failed');
throw new Error(`Conversion failed with status: ${response.status}`);
}
const data = await response.json();
console.log('Response data:', data);
if (this.tg) {
this.tg.sendData(JSON.stringify({
action: 'send_audio',
audioUrl: data.audioUrl,
title: title
}));
if (data.audioUrl) {
// MP3 conversion successful!
console.log('MP3 conversion successful:', data.audioUrl);
if (this.tg) {
// Send to Telegram chat
this.tg.sendData(JSON.stringify({
action: 'send_audio',
audioUrl: data.audioUrl,
title: title
}));
this.showMessage('✓ MP3 готов! Отправляем в чат...', 'success');
} else {
// For testing in browser - download file
const link = document.createElement('a');
link.href = data.audioUrl;
link.download = `${title}.mp3`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.showMessage('✓ MP3 скачан!', 'success');
}
} else {
// Fallback for testing without Telegram
window.open(data.audioUrl, '_blank');
// Should not happen since we removed fallbacks
throw new Error('No audio URL received');
}
} catch (error) {
console.error('Conversion error:', error);
if (this.tg) {
this.tg.showAlert('H81:0 ?@8 :>=25@B0F88. >?@>1C9B5 5I5 @07.');
} else {
alert('H81:0 ?@8 :>=25@B0F88. >?@>1C9B5 5I5 @07.');
// Show specific error message
let errorMsg = 'Ошибка конвертации. Попробуйте другое видео.';
if (error.message.includes('No audio URL')) {
errorMsg = 'Не удалось получить аудио файл.';
} else if (error.message.includes('410')) {
errorMsg = 'Видео недоступно для скачивания.';
} else if (error.message.includes('403')) {
errorMsg = 'Видео заблокировано для скачивания.';
}
this.showMessage(`${errorMsg}`, 'warning');
} finally {
videoElement.classList.remove('converting');
}
@@ -151,6 +179,30 @@ class QuixoticApp {
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
showMessage(message, type = 'info') {
// Remove existing message if any
const existingMessage = document.querySelector('.status-message');
if (existingMessage) {
existingMessage.remove();
}
// Create message element
const messageEl = document.createElement('div');
messageEl.className = `status-message status-${type}`;
messageEl.textContent = message;
// Add to page
const container = document.querySelector('.container');
container.insertBefore(messageEl, container.firstChild);
// Auto-remove after 5 seconds
setTimeout(() => {
if (messageEl.parentNode) {
messageEl.remove();
}
}, 5000);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;

View File

@@ -113,6 +113,7 @@ header p {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
position: relative;
}
.video-item:hover {
@@ -178,7 +179,7 @@ header p {
}
.converting::after {
content: ">=25@B0F8O...";
content: "Converting to MP3...";
position: absolute;
top: 50%;
left: 50%;
@@ -190,6 +191,44 @@ header p {
font-size: 0.8rem;
}
.status-message {
padding: 12px 16px;
margin-bottom: 16px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
animation: slideIn 0.3s ease-out;
}
.status-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.status-warning {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.status-info {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 480px) {
.video-item {
flex-direction: column;

View File

@@ -60,7 +60,7 @@ class Database {
addSearchHistory(userId, query) {
return new Promise((resolve, reject) => {
this.db.run(
`INSERT INTO search_history (user_id, query) VALUES (?, ?)`,
'INSERT INTO search_history (user_id, query) VALUES (?, ?)',
[userId, query],
function(err) {
if (err) reject(err);
@@ -73,7 +73,7 @@ class Database {
addDownload(userId, youtubeId, title, filePath) {
return new Promise((resolve, reject) => {
this.db.run(
`INSERT INTO downloads (user_id, youtube_id, title, file_path) VALUES (?, ?, ?, ?)`,
'INSERT INTO downloads (user_id, youtube_id, title, file_path) VALUES (?, ?, ?, ?)',
[userId, youtubeId, title, filePath],
function(err) {
if (err) reject(err);
@@ -86,7 +86,7 @@ class Database {
getUserByTelegramId(telegramId) {
return new Promise((resolve, reject) => {
this.db.get(
`SELECT * FROM users WHERE telegram_id = ?`,
'SELECT * FROM users WHERE telegram_id = ?',
[telegramId],
(err, row) => {
if (err) reject(err);

View File

@@ -3,14 +3,14 @@ const path = require('path');
const fs = require('fs');
const ffmpeg = require('fluent-ffmpeg');
const Database = require('./database');
const YouTubeService = require('./youtube');
const SoundCloudService = require('./soundcloud');
const app = express();
const port = process.env.PORT || 3000;
// Initialize services
const db = new Database();
const youtube = new YouTubeService();
const soundcloud = new SoundCloudService();
// Middleware
app.use(express.json());
@@ -48,7 +48,7 @@ app.post('/api/search', async (req, res) => {
}
}
const videos = await youtube.searchVideos(query.trim());
const videos = await soundcloud.searchTracks(query.trim());
res.json({ videos });
} catch (error) {
@@ -61,6 +61,7 @@ app.post('/api/search', async (req, res) => {
app.post('/api/convert', async (req, res) => {
try {
const { videoId, title, userId } = req.body;
console.log('Convert request received:', { videoId, title, userId });
if (!videoId) {
return res.status(400).json({ error: 'Video ID is required' });
@@ -73,49 +74,80 @@ app.post('/api/convert', async (req, res) => {
// Check if file already exists
if (fs.existsSync(outputPath)) {
console.log('File already exists, serving cached version');
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);
console.log(`Starting MP3 conversion for: ${title}`);
// 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();
});
try {
// Get audio stream from YouTube
console.log(`Attempting to get audio stream for: ${videoId}`);
const audioStream = await soundcloud.getAudioStream(videoId);
console.log('Audio stream obtained, starting FFmpeg conversion...');
// Save download record
if (userId && userId !== 'demo') {
try {
const user = await db.getUserByTelegramId(userId);
if (user) {
await db.addDownload(user.id, videoId, title, outputPath);
// Convert to MP3 using ffmpeg
await new Promise((resolve, reject) => {
const conversion = ffmpeg(audioStream)
.audioCodec('libmp3lame')
.audioBitrate('192k')
.audioChannels(2)
.audioFrequency(44100)
.format('mp3')
.output(outputPath)
.on('start', (command) => {
console.log('FFmpeg started:', command);
})
.on('progress', (progress) => {
if (progress.percent) {
console.log(`Conversion progress: ${Math.round(progress.percent)}%`);
}
})
.on('end', () => {
console.log('MP3 conversion completed successfully');
resolve();
})
.on('error', (err) => {
console.error('FFmpeg error:', err.message);
reject(err);
});
conversion.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);
}
} catch (dbError) {
console.error('Database error:', dbError);
}
const audioUrl = `${req.protocol}://${req.get('host')}/downloads/${filename}`;
console.log('Conversion successful, file available at:', audioUrl);
res.json({ audioUrl, title });
} catch (conversionError) {
console.error('Conversion failed for video:', videoId);
console.error('Error details:', conversionError.message);
console.error('Full error:', conversionError);
// Return error - no fallbacks for Telegram bot
return res.status(503).json({
error: 'MP3 conversion failed. This video may be restricted or unavailable for download.',
details: conversionError.message,
videoId: videoId
});
}
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' });
console.error('Server error:', error);
res.status(500).json({ error: 'Failed to process request' });
}
});
@@ -159,8 +191,9 @@ setInterval(() => {
}, 60 * 60 * 1000); // Run every hour
app.listen(port, () => {
console.log(`Quixotic server running on port ${port}`);
console.log(`Downloads directory: ${downloadsDir}`);
console.log(`Quixotic server running on port ${port}`);
console.log(`Downloads directory: ${downloadsDir}`);
console.log(`Open in browser: http://localhost:${port}`);
});
module.exports = app;

101
src/soundcloud.js Normal file
View File

@@ -0,0 +1,101 @@
const scdl = require('soundcloud-downloader').default;
class SoundCloudService {
constructor() {
console.log('SoundCloud service initialized');
}
async searchTracks(query, maxResults = 10) {
try {
console.log(`Searching SoundCloud for: ${query}`);
// Search for tracks on SoundCloud
const tracks = await scdl.search({
query: query,
limit: maxResults,
resourceType: 'tracks'
});
if (!tracks || tracks.length === 0) {
console.log('No tracks found');
return [];
}
const trackResults = tracks.map(track => ({
id: track.id,
title: track.title,
channel: track.user?.username || 'Unknown Artist',
thumbnail: track.artwork_url || track.user?.avatar_url || 'https://via.placeholder.com/300x300?text=No+Image',
duration: Math.floor(track.duration / 1000) || 0, // Convert from ms to seconds
url: track.permalink_url,
streamable: track.streamable,
downloadable: track.downloadable
}));
console.log(`Found ${trackResults.length} tracks on SoundCloud`);
return trackResults;
} catch (error) {
console.error('SoundCloud search error:', error.message);
return [];
}
}
async getTrackInfo(trackId) {
try {
const track = await scdl.getInfo(trackId);
return {
title: track.title,
author: track.user?.username || 'Unknown',
length: Math.floor(track.duration / 1000),
available: track.streamable
};
} catch (error) {
console.error('Error getting track info:', error);
throw error;
}
}
async getAudioStream(trackId) {
try {
console.log(`Getting audio stream for track: ${trackId}`);
// Get track info first
const trackInfo = await scdl.getInfo(trackId);
if (!trackInfo.streamable) {
throw new Error('Track is not streamable');
}
console.log(`Track: ${trackInfo.title}`);
console.log(`Artist: ${trackInfo.user?.username || 'Unknown'}`);
console.log(`Duration: ${Math.floor(trackInfo.duration / 1000)}s`);
// Get audio stream
const stream = await scdl.download(trackId);
console.log('Audio stream obtained successfully from SoundCloud');
return stream;
} catch (error) {
console.error('SoundCloud download failed:', error.message);
// Try alternative approach
try {
console.log('Trying alternative SoundCloud method...');
const trackUrl = `https://soundcloud.com/track/${trackId}`;
const stream = await scdl.download(trackUrl);
console.log('Audio stream obtained with alternative method');
return stream;
} catch (fallbackError) {
console.error('Alternative method also failed:', fallbackError.message);
throw new Error(`SoundCloud download failed: ${error.message}`);
}
}
}
}
module.exports = SoundCloudService;

View File

@@ -1,173 +0,0 @@
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;

3306
yarn.lock Normal file

File diff suppressed because it is too large Load Diff