diff --git a/public/script.ts b/public/script.ts index 243562a..bfc3a1d 100644 --- a/public/script.ts +++ b/public/script.ts @@ -912,7 +912,6 @@ class QuixoticApp { const data: ConvertResponse = await response.json(); if (data.audioUrl) { - if (this.tg) { const userId = this.tg?.initDataUnsafe?.user?.id; if (!userId) { diff --git a/public/style.css b/public/style.css index 61fd299..129f42b 100644 --- a/public/style.css +++ b/public/style.css @@ -651,19 +651,21 @@ body { /* Status message */ .tg-status-message { position: fixed; - top: 20px; - left: var(--tg-spacing-lg); - right: var(--tg-spacing-lg); + top: 12px; + left: var(--tg-spacing-md); + right: var(--tg-spacing-md); z-index: 1000; - padding: var(--tg-spacing-md) var(--tg-spacing-lg); - border-radius: var(--tg-border-radius); - font-size: var(--tg-font-size-sm); + padding: 10px 14px; + border-radius: 10px; + font-size: 13px; font-weight: var(--tg-font-weight-medium); animation: tg-slide-down 0.3s ease-out; display: flex; align-items: center; - gap: var(--tg-spacing-sm); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), 0 4px 12px rgba(0, 0, 0, 0.15); + gap: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.12); + max-width: 320px; + margin: 0 auto; } .tg-status-message--success { @@ -687,7 +689,7 @@ body { @keyframes tg-slide-down { from { opacity: 0; - transform: translateY(-20px); + transform: translateY(-12px); } to { diff --git a/src/bot.ts b/src/bot.ts index c70152b..ee7012e 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -272,6 +272,7 @@ export class QuixoticBot { // Public method for external API calls public async sendAudioFile(chatId: number, audioUrl: string, title: string, performer?: string, thumbnail?: string): Promise { + logger.debug(`sendAudioFile called with performer: ${performer}, thumbnail: ${thumbnail}`); return this.sendAudioFileInternal(chatId, audioUrl, title, performer, thumbnail); } @@ -279,7 +280,8 @@ export class QuixoticBot { try { logger.telegram('Sending audio', `${title} to chat ${chatId}`); logger.debug(`File source: ${audioUrlOrPath}`); - logger.debug(`Thumbnail: ${thumbnail}`); + logger.debug(`Performer: ${performer || 'Not provided'}`); + logger.debug(`Thumbnail: ${thumbnail || 'Not provided'}`); // Check if it's a URL or local file path const isUrl = audioUrlOrPath.startsWith('http'); @@ -318,25 +320,70 @@ export class QuixoticBot { let thumbnailPath: string | undefined; if (thumbnail && thumbnail.startsWith('http')) { try { - logger.debug('Downloading thumbnail...'); + logger.debug(`Downloading thumbnail from: ${thumbnail}`); const thumbnailFilename = `thumb_${Date.now()}.jpg`; thumbnailPath = path.join(process.cwd(), 'downloads', thumbnailFilename); await new Promise((resolve, reject) => { - const file = fs.createWriteStream(thumbnailPath); - https.get(thumbnail, (response: any) => { - response.pipe(file); - file.on('finish', () => { + const file = fs.createWriteStream(thumbnailPath!); + + // Handle both http and https + const protocol = thumbnail.startsWith('https') ? https : require('http'); + + const request = protocol.get(thumbnail, (response: any) => { + // Follow redirects + if (response.statusCode === 301 || response.statusCode === 302) { + const redirectUrl = response.headers.location; + logger.debug(`Following redirect to: ${redirectUrl}`); file.close(); - resolve(); - }); - }).on('error', (err: any) => { - fs.unlink(thumbnailPath, () => {}); + fs.unlink(thumbnailPath!, () => {}); + + const redirectProtocol = redirectUrl.startsWith('https') ? https : require('http'); + redirectProtocol.get(redirectUrl, (redirectResponse: any) => { + redirectResponse.pipe(file); + file.on('finish', () => { + file.close(); + logger.success(`Thumbnail downloaded: ${thumbnailPath} (${fs.statSync(thumbnailPath!).size} bytes)`); + resolve(); + }); + }).on('error', (err: any) => { + file.close(); + fs.unlink(thumbnailPath!, () => {}); + reject(err); + }); + } else { + response.pipe(file); + file.on('finish', () => { + file.close(); + const fileSize = fs.statSync(thumbnailPath!).size; + logger.success(`Thumbnail downloaded: ${thumbnailPath} (${fileSize} bytes)`); + + // Check if file is valid (at least 1KB) + if (fileSize < 1000) { + logger.warn('Thumbnail file too small, may be invalid'); + fs.unlink(thumbnailPath!, () => {}); + thumbnailPath = undefined; + } + resolve(); + }); + } + }); + + request.on('error', (err: any) => { + file.close(); + fs.unlink(thumbnailPath!, () => {}); reject(err); }); + + // Set timeout for thumbnail download + request.setTimeout(10000, () => { + request.destroy(); + file.close(); + fs.unlink(thumbnailPath!, () => {}); + reject(new Error('Thumbnail download timeout')); + }); }); - logger.success(`Thumbnail downloaded: ${thumbnailPath}`); } catch (thumbError: any) { logger.warn('Failed to download thumbnail:', thumbError.message); thumbnailPath = undefined; diff --git a/src/server.ts b/src/server.ts index 46ce286..6625992 100644 --- a/src/server.ts +++ b/src/server.ts @@ -158,8 +158,8 @@ app.post('/api/search', async (req: Request, res: Response) => { // Convert video to MP3 app.post('/api/convert', async (req: Request, res: Response) => { try { - const { videoId, title, userId, url }: { videoId?: string; title?: string; userId?: string; url?: string } = req.body; - logger.info(`Convert request received: ${title} (ID: ${videoId})`); + const { videoId, title, userId, url, performer }: { videoId?: string; title?: string; userId?: string; url?: string; performer?: string } = req.body; + logger.info(`Convert request received: ${title} by ${performer || 'Unknown'} (ID: ${videoId})`); if (!videoId) { return res.status(400).json({ error: 'Video ID is required' }); diff --git a/src/soundcloud.ts b/src/soundcloud.ts index 951a46c..42a13b4 100644 --- a/src/soundcloud.ts +++ b/src/soundcloud.ts @@ -49,22 +49,29 @@ export class SoundCloudService { // -large (100x100) -> -t500x500 (500x500) or -t300x300 (300x300) // Try to get the highest quality version available + let highQualityUrl = originalUrl; + if (originalUrl.includes('-large.')) { // Replace -large with -t500x500 for better quality - return originalUrl.replace('-large.', '-t500x500.'); + highQualityUrl = originalUrl.replace('-large.', '-t500x500.'); } else if (originalUrl.includes('-crop.')) { // If it's crop (400x400), try to get t500x500 or keep crop - return originalUrl.replace('-crop.', '-t500x500.'); + highQualityUrl = originalUrl.replace('-crop.', '-t500x500.'); } else if (originalUrl.includes('-t300x300.')) { // If it's already 300x300, try to upgrade to 500x500 - return originalUrl.replace('-t300x300.', '-t500x500.'); + highQualityUrl = originalUrl.replace('-t300x300.', '-t500x500.'); } else if (originalUrl.includes('default_avatar_large.png')) { // For default avatars, use a higher quality placeholder - return 'https://via.placeholder.com/500x500/007AFF/ffffff?text=🎵'; + highQualityUrl = 'https://via.placeholder.com/500x500/007AFF/ffffff?text=🎵'; + } + + // Log transformation if changed + if (highQualityUrl !== originalUrl) { + logger.debug(`Thumbnail upgraded: ${originalUrl.substring(0, 60)}... -> ${highQualityUrl.substring(0, 60)}...`); } // If no size suffix found or already high quality, return original - return originalUrl; + return highQualityUrl; } async searchTracks(query: string, maxResults: number = 10, offset: number = 0): Promise {