does not work
This commit is contained in:
@@ -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);
|
||||
|
||||
107
src/server.js
107
src/server.js
@@ -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...');
|
||||
|
||||
// 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);
|
||||
// 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
101
src/soundcloud.js
Normal 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;
|
||||
173
src/youtube.js
173
src/youtube.js
@@ -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;
|
||||
Reference in New Issue
Block a user