From 82a9596370ce33fe4cdc14497aa701440599a357 Mon Sep 17 00:00:00 2001 From: Andrey Kondratev <81143241+cockroach-eater@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:56:19 +0500 Subject: [PATCH] new ui?! --- .dockerignore | 4 - Dockerfile | 12 +- docker-compose.local.yml | 6 + package.json | 5 +- public/index.html | 85 ++++++++- public/script.ts | 365 +++++++++++++++++++++++++++++++++++++-- public/style.css | 197 +++++++++++++++++++++ src/bot.ts | 158 +++++++++++------ src/database.ts | 9 +- src/logger.ts | 81 +++++++++ src/server.ts | 92 +++++----- src/soundcloud.ts | 39 +++-- yarn.lock | 185 +++++++++++++++++++- 13 files changed, 1086 insertions(+), 152 deletions(-) create mode 100644 src/logger.ts diff --git a/.dockerignore b/.dockerignore index 899adc4..37f382e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -58,7 +58,3 @@ docs Dockerfile* docker-compose* .dockerignore - -# Misc -.cache -.parcel-cache \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7f8dedc..ee38b52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,15 +3,20 @@ FROM node:18-alpine AS builder WORKDIR /app -# Copy package files +# Copy package files first (better caching) COPY package*.json ./ COPY yarn.lock* ./ # Install all dependencies (including dev for build) +# This layer will be cached unless package.json changes RUN yarn install --frozen-lockfile && yarn cache clean -# Copy source code -COPY . . +# Copy source code (separate from dependencies) +COPY tsconfig*.json ./ +COPY eslint.config.mjs ./ +COPY scripts ./scripts +COPY src ./src +COPY public ./public # Build the application with minification RUN yarn build:prod @@ -28,6 +33,7 @@ RUN apk update && apk add --no-cache ffmpeg # Set ffmpeg paths ENV FFMPEG_PATH=/usr/bin/ffmpeg ENV FFPROBE_PATH=/usr/bin/ffprobe +ENV NODE_ENV=production WORKDIR /app diff --git a/docker-compose.local.yml b/docker-compose.local.yml index b494a2f..20eb563 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -22,6 +22,9 @@ services: build: context: . dockerfile: Dockerfile + cache_from: + - quixotic-app:latest + image: quixotic-app:latest container_name: quixotic-app restart: unless-stopped environment: @@ -33,6 +36,9 @@ services: WEB_APP_URL: http://localhost:3000 volumes: - downloads:/app/downloads + # Mount source code for hot reload (uncomment for development) + # - ./src:/app/src + # - ./public:/app/public ports: - "3000:3000" depends_on: diff --git a/package.json b/package.json index c7762fd..b0488fd 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,14 @@ "fluent-ffmpeg": "^2.1.2", "node-telegram-bot-api": "^0.64.0", "pg": "^8.11.3", - "soundcloud-downloader": "^1.0.0" + "soundcloud-downloader": "^1.0.0", + "winston": "^3.18.3" }, "devDependencies": { "@types/compression": "^1.8.1", "@types/express": "^5.0.3", "@types/fluent-ffmpeg": "^2.1.27", - "@types/node": "^24.3.0", + "@types/node": "^24.10.0", "@types/node-telegram-bot-api": "^0.64.10", "@types/pg": "^8.15.5", "@typescript-eslint/eslint-plugin": "^8.41.0", diff --git a/public/index.html b/public/index.html index 06af595..dac105a 100644 --- a/public/index.html +++ b/public/index.html @@ -54,17 +54,13 @@ - - - - - + @@ -84,6 +80,79 @@
Поиск музыки...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -101,6 +170,12 @@ id="searchInput" placeholder="Название песни или исполнитель..." autocomplete="off"> + diff --git a/public/script.ts b/public/script.ts index 701829d..34fcbad 100644 --- a/public/script.ts +++ b/public/script.ts @@ -6,6 +6,11 @@ interface TelegramWebApp { show(): void; hide(): void; }; + HapticFeedback?: { + impactOccurred(style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft'): void; + notificationOccurred(type: 'error' | 'success' | 'warning'): void; + selectionChanged(): void; + }; initDataUnsafe?: { user?: { id: number; @@ -48,13 +53,66 @@ class QuixoticApp { private currentVideos: VideoResult[] = []; private loadingStartTime: number = 0; private minLoadingDuration: number = 400; // Минимальное время показа спиннера (ms) + private recentSearches: string[] = []; + private maxRecentSearches: number = 5; + private currentAudio: HTMLAudioElement | null = null; + private currentPlayingItem: HTMLElement | null = null; constructor() { this.tg = (window as WindowWithTelegram).Telegram?.WebApp; + this.loadRecentSearches(); this.init(); this.bindEvents(); } + private triggerHaptic(type: 'light' | 'medium' | 'heavy' | 'success' | 'error' = 'light'): void { + if (this.tg?.HapticFeedback) { + switch(type) { + case 'light': + this.tg.HapticFeedback.impactOccurred('light'); + break; + case 'medium': + this.tg.HapticFeedback.impactOccurred('medium'); + break; + case 'heavy': + this.tg.HapticFeedback.impactOccurred('heavy'); + break; + case 'success': + this.tg.HapticFeedback.notificationOccurred('success'); + break; + case 'error': + this.tg.HapticFeedback.notificationOccurred('error'); + break; + } + } + } + + private loadRecentSearches(): void { + try { + const saved = localStorage.getItem('recentSearches'); + if (saved) { + this.recentSearches = JSON.parse(saved); + } + } catch (e) { + console.error('Failed to load recent searches:', e); + } + } + + private saveSearch(query: string): void { + // Remove if already exists + this.recentSearches = this.recentSearches.filter(s => s !== query); + // Add to beginning + this.recentSearches.unshift(query); + // Limit count + this.recentSearches = this.recentSearches.slice(0, this.maxRecentSearches); + // Save + try { + localStorage.setItem('recentSearches', JSON.stringify(this.recentSearches)); + } catch (e) { + console.error('Failed to save recent searches:', e); + } + } + private init(): void { if (this.tg) { this.tg.ready(); @@ -87,19 +145,27 @@ class QuixoticApp { const query = this.searchInput.value.trim(); + // Show/hide clear button + const clearButton = document.getElementById('clearButton'); + if (query) { + clearButton?.classList.remove('tg-hidden'); + } else { + clearButton?.classList.add('tg-hidden'); + } + // If input is empty, reset to welcome state immediately if (query === '') { this.resetToWelcomeState(); return; } - // Show loading spinner immediately for better UX feedback - this.showLoading(); + // Don't show loading immediately - wait for debounce to finish + // This prevents flickering when user is typing fast - // Set new timeout for search (300ms delay - best practice for instant search) + // Set new timeout for search (600ms delay - prevents missing characters) this.searchTimeout = setTimeout(() => { this.search(); - }, 300); + }, 600); }); // Still handle Enter key for immediate search @@ -112,6 +178,67 @@ class QuixoticApp { this.search(); } }); + + // Clear button handler + const clearButton = document.getElementById('clearButton'); + clearButton?.addEventListener('click', () => { + this.triggerHaptic('light'); + this.searchInput.value = ''; + this.searchInput.focus(); + this.resetToWelcomeState(); + clearButton.classList.add('tg-hidden'); + }); + + // Setup pull-to-refresh + this.setupPullToRefresh(); + } + + private setupPullToRefresh(): void { + let touchStartY = 0; + let pullDistance = 0; + const threshold = 80; + + const container = document.querySelector('.tg-content') as HTMLElement; + if (!container) return; + + container.addEventListener('touchstart', (e) => { + if (window.scrollY === 0) { + touchStartY = e.touches[0].clientY; + } + }, { passive: true }); + + container.addEventListener('touchmove', (e) => { + if (touchStartY > 0) { + pullDistance = e.touches[0].clientY - touchStartY; + if (pullDistance > 0 && pullDistance < threshold * 2) { + // Visual indication of stretching + container.style.transform = `translateY(${pullDistance * 0.5}px)`; + + // Add pulling class for indicator + if (pullDistance > threshold * 0.5) { + container.classList.add('tg-pulling'); + } + } + } + }, { passive: true }); + + container.addEventListener('touchend', () => { + if (pullDistance > threshold) { + this.triggerHaptic('medium'); + this.refreshResults(); + } + container.style.transform = ''; + container.classList.remove('tg-pulling'); + touchStartY = 0; + pullDistance = 0; + }); + } + + private refreshResults(): void { + const query = this.searchInput.value.trim(); + if (query) { + this.search(); + } } private resetToWelcomeState(): void { @@ -122,18 +249,25 @@ class QuixoticApp { // Hide all other states this.loading.classList.add('tg-hidden'); this.loading.classList.remove('tg-spinner--visible'); + + const skeletonList = document.getElementById('skeletonList'); + if (skeletonList) { + skeletonList.classList.add('tg-hidden'); + } + this.results.classList.add('tg-hidden'); this.results.classList.remove('tg-list--visible'); this.noResults.classList.add('tg-hidden'); this.noResults.style.display = 'none'; - - } private async search(): Promise { const query = this.searchInput.value.trim(); if (!query) return; + // Save to recent searches + this.saveSearch(query); + this.showLoading(); try { @@ -162,6 +296,7 @@ class QuixoticApp { await new Promise(resolve => setTimeout(resolve, remainingTime)); } + this.triggerHaptic('light'); this.displayResults(data.videos); } catch (error) { console.error('Search error:', error); @@ -174,6 +309,7 @@ class QuixoticApp { await new Promise(resolve => setTimeout(resolve, remainingTime)); } + this.triggerHaptic('error'); this.showNoResults(); } } @@ -192,22 +328,31 @@ class QuixoticApp { this.welcomePlaceholder.classList.add('tg-hidden'); this.welcomePlaceholder.style.display = 'none'; - // Show loading spinner - this.loading.classList.remove('tg-hidden'); - this.loading.classList.add('tg-spinner--visible'); + // Show skeleton screens instead of spinner for better UX + const skeletonList = document.getElementById('skeletonList'); + if (skeletonList) { + skeletonList.classList.remove('tg-hidden'); + } + + // Hide spinner (we're using skeletons now) + this.loading.classList.add('tg-hidden'); + this.loading.classList.remove('tg-spinner--visible'); // Hide other elements this.results.classList.add('tg-hidden'); this.results.classList.remove('tg-list--visible'); this.noResults.classList.add('tg-hidden'); this.noResults.style.display = 'none'; - - } private hideLoading(): void { this.loading.classList.add('tg-hidden'); this.loading.classList.remove('tg-spinner--visible'); + + const skeletonList = document.getElementById('skeletonList'); + if (skeletonList) { + skeletonList.classList.add('tg-hidden'); + } } private displayResults(videos: VideoResult[]): void { @@ -230,7 +375,7 @@ class QuixoticApp { // Use DocumentFragment for better performance const fragment = document.createDocumentFragment(); - videos.forEach(video => { + videos.forEach((video, index) => { const item = document.createElement('div'); item.className = 'tg-list-item'; item.dataset.videoId = video.id; @@ -244,16 +389,46 @@ class QuixoticApp { alt='${this.escapeHtml(video.title)}' loading='lazy'>
${this.formatDuration(video.duration)}
+
${this.escapeHtml(video.title)}
${this.escapeHtml(video.channel)}
+
+
+
+
+
+
+ 0:00 + ${this.formatDuration(video.duration)} +
+
+
`; - // Use event listener instead of inline onclick - item.addEventListener('click', () => { + // Handle play button click + const playBtn = item.querySelector('.tg-list-item__play-btn') as HTMLElement; + playBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + this.triggerHaptic('light'); + this.toggleAudioPreview(video, item); + }); + + // Use event listener for download + item.addEventListener('click', (e) => { + // Don't trigger download if clicking play button + if ((e.target as HTMLElement).closest('.tg-list-item__play-btn')) { + return; + } + this.triggerHaptic('medium'); this.convertVideo(video.id, video.title, video.url); }); @@ -330,6 +505,9 @@ class QuixoticApp { videoElement.classList.remove('tg-list-item--active'); videoElement.classList.add('tg-list-item--converting'); + + // Add progress bar + this.showConversionProgress(videoElement); } try { @@ -358,6 +536,7 @@ class QuixoticApp { if (this.tg) { const userId = this.tg?.initDataUnsafe?.user?.id; if (!userId) { + this.triggerHaptic('error'); this.showMessage('❌ Ошибка: не удается определить пользователя', 'error'); return; } @@ -377,11 +556,14 @@ class QuixoticApp { }); if (directResponse.ok) { + this.triggerHaptic('success'); this.showMessage('✅ MP3 отправлен в чат!', 'success'); } else { + this.triggerHaptic('error'); this.showMessage('❌ Ошибка отправки в Telegram', 'error'); } } catch { + this.triggerHaptic('error'); this.showMessage('❌ Ошибка соединения с ботом', 'error'); } } else { @@ -411,14 +593,49 @@ class QuixoticApp { errorMsg = 'Видео заблокировано для скачивания.'; } + this.triggerHaptic('error'); this.showMessage(`❌ ${errorMsg}`, 'error'); } finally { if (videoElement) { videoElement.classList.remove('tg-list-item--converting'); + // Remove progress bar + const progressBar = videoElement.querySelector('.tg-conversion-progress'); + if (progressBar) { + progressBar.remove(); + } } } } + private showConversionProgress(element: HTMLElement): void { + const progressBar = document.createElement('div'); + progressBar.className = 'tg-conversion-progress'; + progressBar.innerHTML = ` +
+
+
+
Конвертация...
+ `; + + element.appendChild(progressBar); + + // Simulate progress (smooth animation) + const fill = progressBar.querySelector('.tg-conversion-progress__fill') as HTMLElement; + let progress = 0; + const interval = setInterval(() => { + progress += Math.random() * 15; + if (progress > 90) progress = 90; // Stop at 90% + fill.style.width = `${progress}%`; + }, 200); + + // Store cleanup function + (element as any).__progressCleanup = () => { + clearInterval(interval); + fill.style.width = '100%'; + setTimeout(() => progressBar.remove(), 500); + }; + } + private formatDuration(seconds: number): string { if (!seconds) return ''; const mins = Math.floor(seconds / 60); @@ -454,6 +671,126 @@ class QuixoticApp { div.textContent = text; return div.innerHTML; } + + private async toggleAudioPreview(video: VideoResult, item: HTMLElement): Promise { + const playerContainer = item.querySelector('.tg-audio-player') as HTMLElement; + const playBtn = item.querySelector('.tg-list-item__play-btn') as HTMLElement; + + // If this is already playing, pause it + if (this.currentPlayingItem === item && this.currentAudio) { + this.stopAudioPreview(); + return; + } + + // Stop any other playing audio + if (this.currentAudio) { + this.stopAudioPreview(); + } + + try { + // Show converting state + item.classList.add('tg-list-item--loading-preview'); + playBtn.innerHTML = ` + + + + + + + `; + + // Get audio URL (we need to convert first) + const response = await fetch('/api/convert', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + videoId: video.id, + title: video.title, + url: video.url, + performer: video.channel, + userId: this.tg?.initDataUnsafe?.user?.id || 'demo' + }) + }); + + if (!response.ok) { + throw new Error('Failed to get audio'); + } + + const data = await response.json(); + if (!data.audioUrl) { + throw new Error('No audio URL'); + } + + // Create audio element + this.currentAudio = new Audio(data.audioUrl); + this.currentPlayingItem = item; + + // Show player + playerContainer.classList.remove('tg-hidden'); + item.classList.remove('tg-list-item--loading-preview'); + item.classList.add('tg-list-item--playing'); + + // Update play button to pause + playBtn.innerHTML = ` + + + + + + `; + + // Setup progress tracking + const progressFill = playerContainer.querySelector('.tg-audio-player__progress-fill') as HTMLElement; + const currentTimeEl = playerContainer.querySelector('.tg-audio-player__current') as HTMLElement; + + this.currentAudio.addEventListener('timeupdate', () => { + if (!this.currentAudio) return; + const progress = (this.currentAudio.currentTime / this.currentAudio.duration) * 100; + progressFill.style.width = `${progress}%`; + currentTimeEl.textContent = this.formatDuration(Math.floor(this.currentAudio.currentTime)); + }); + + this.currentAudio.addEventListener('ended', () => { + this.stopAudioPreview(); + }); + + // Play audio + await this.currentAudio.play(); + this.triggerHaptic('light'); + + } catch (error) { + console.error('Preview error:', error); + item.classList.remove('tg-list-item--loading-preview'); + this.showMessage('❌ Не удалось воспроизвести превью', 'error'); + } + } + + private stopAudioPreview(): void { + if (this.currentAudio) { + this.currentAudio.pause(); + this.currentAudio = null; + } + + if (this.currentPlayingItem) { + const playerContainer = this.currentPlayingItem.querySelector('.tg-audio-player') as HTMLElement; + const playBtn = this.currentPlayingItem.querySelector('.tg-list-item__play-btn') as HTMLElement; + + playerContainer?.classList.add('tg-hidden'); + this.currentPlayingItem.classList.remove('tg-list-item--playing'); + + // Reset play button + if (playBtn) { + playBtn.innerHTML = ` + + + + + `; + } + + this.currentPlayingItem = null; + } + } } const app = new QuixoticApp(); diff --git a/public/style.css b/public/style.css index 1b8bb34..6c28f39 100644 --- a/public/style.css +++ b/public/style.css @@ -71,6 +71,11 @@ body { display: flex; flex-direction: column; gap: var(--tg-spacing-xl); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.tg-content.tg-pulling { + transition: none; } /* Form components */ @@ -93,6 +98,7 @@ body { width: 100%; height: 48px; padding: 0 var(--tg-spacing-lg); + padding-right: 48px; /* Make room for clear button */ background: var(--tg-color-section-bg); border: 2px solid var(--tg-color-secondary-bg); border-radius: var(--tg-border-radius); @@ -111,6 +117,33 @@ body { background: var(--tg-color-bg); } +.tg-input-clear { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--tg-color-hint); + padding: 8px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s ease; + -webkit-tap-highlight-color: transparent; +} + +.tg-input-clear:hover { + background: var(--tg-color-secondary-bg); + color: var(--tg-color-text); +} + +.tg-input-clear:active { + transform: translateY(-50%) scale(0.9); +} + /* Button components */ .tg-button { position: relative; @@ -244,6 +277,66 @@ body { } } +/* Skeleton loading screens */ +.tg-skeleton-list { + display: flex; + flex-direction: column; + gap: var(--tg-spacing-xs); +} + +.tg-skeleton-list.tg-hidden { + display: none; +} + +.tg-skeleton-item { + display: flex; + gap: var(--tg-spacing-md); + padding: var(--tg-spacing-md); + background: var(--tg-color-section-bg); + border-radius: var(--tg-border-radius); +} + +.tg-skeleton-thumbnail { + width: 80px; + height: 60px; + background: linear-gradient(90deg, + var(--tg-color-secondary-bg) 25%, + var(--tg-color-hint) 50%, + var(--tg-color-secondary-bg) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--tg-border-radius-small); + flex-shrink: 0; +} + +.tg-skeleton-text { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + gap: var(--tg-spacing-sm); +} + +.tg-skeleton-line { + height: 16px; + background: linear-gradient(90deg, + var(--tg-color-secondary-bg) 25%, + var(--tg-color-hint) 50%, + var(--tg-color-secondary-bg) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 4px; +} + +.tg-skeleton-line--short { + width: 60%; +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + /* List component */ .tg-list { display: none; @@ -264,6 +357,7 @@ body { user-select: none; -webkit-tap-highlight-color: transparent; position: relative; + opacity: 1; } /* Hover effects for desktop */ @@ -313,6 +407,34 @@ body { image-rendering: optimizeQuality; } +.tg-list-item__play-btn { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: none; + border: none; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s ease; + z-index: 2; +} + +.tg-list-item__media:hover .tg-list-item__play-btn, +.tg-list-item--playing .tg-list-item__play-btn { + opacity: 1; +} + +@media (hover: none) { + .tg-list-item__play-btn { + opacity: 1; + } +} + .tg-list-item__duration { position: absolute; bottom: 2px; @@ -371,6 +493,81 @@ body { animation: tg-spin 1s linear infinite; } +/* Conversion progress bar */ +.tg-conversion-progress { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: var(--tg-spacing-xs); + background: rgba(0, 0, 0, 0.05); + z-index: 10; +} + +.tg-conversion-progress__bar { + height: 4px; + background: var(--tg-color-secondary-bg); + border-radius: 2px; + overflow: hidden; +} + +.tg-conversion-progress__fill { + height: 100%; + background: var(--tg-color-button); + transition: width 0.3s ease; + border-radius: 2px; + width: 0; +} + +.tg-conversion-progress__text { + font-size: var(--tg-font-size-xs); + color: var(--tg-color-hint); + text-align: center; + margin-top: 4px; +} + +/* Audio player */ +.tg-audio-player { + padding: var(--tg-spacing-sm) var(--tg-spacing-md); + background: var(--tg-color-secondary-bg); + border-top: 1px solid rgba(0, 0, 0, 0.05); +} + +.tg-audio-player__progress { + display: flex; + flex-direction: column; + gap: var(--tg-spacing-xs); +} + +.tg-audio-player__progress-bar { + height: 4px; + background: rgba(0, 0, 0, 0.1); + border-radius: 2px; + overflow: hidden; + cursor: pointer; +} + +.tg-audio-player__progress-fill { + height: 100%; + background: var(--tg-color-button); + width: 0%; + transition: width 0.1s linear; + border-radius: 2px; +} + +.tg-audio-player__time { + display: flex; + justify-content: space-between; + font-size: var(--tg-font-size-xs); + color: var(--tg-color-hint); + font-variant-numeric: tabular-nums; +} + +.tg-list-item--loading-preview { + opacity: 0.7; + pointer-events: none; +} + /* Status message */ .tg-status-message { position: fixed; diff --git a/src/bot.ts b/src/bot.ts index a01ec7e..c70152b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,5 +1,6 @@ import TelegramBot from 'node-telegram-bot-api'; import { Database } from './database'; +import { logger } from './logger'; interface TelegramUser { id: number; @@ -49,7 +50,7 @@ export class QuixoticBot { const useWebhook = process.env.NODE_ENV === 'production' && process.env.WEBHOOK_URL; if (useWebhook) { - console.log('🌐 Using webhook mode for production'); + logger.telegram('Using webhook mode for production'); this.bot = new TelegramBot(token, { webHook: { port: 8443, @@ -57,7 +58,7 @@ export class QuixoticBot { } }); } else { - console.log('🔄 Using polling mode for development'); + logger.telegram('Using polling mode for development'); this.bot = new TelegramBot(token, { polling: true }); } @@ -67,7 +68,7 @@ export class QuixoticBot { } private init(): void { - console.log('🤖 Telegram bot initialized'); + logger.telegram('Bot initialized'); this.setupCommands(); this.setupHandlers(); } @@ -82,7 +83,7 @@ export class QuixoticBot { } private setupHandlers(): void { - console.log('🔧 Setting up bot handlers...'); + logger.telegram('Setting up bot handlers...'); // Handle messages this.bot.on('message', (msg: any) => { @@ -108,21 +109,21 @@ export class QuixoticBot { const keyboard = { inline_keyboard: [[ { - text: '🎵 Открыть Quixotic', + text: 'Открыть Quixotic', web_app: { url: this.webAppUrl } } ]] }; await this.bot.sendMessage(chatId, - '🎵 Добро пожаловать в Quixotic!\n\n' + + 'Добро пожаловать в Quixotic!\n\n' + 'Найди любую песню на SoundCloud и получи MP3 файл прямо в чат.\n\n' + 'Нажми кнопку ниже, чтобы начать поиск:', { reply_markup: keyboard } ); } catch (error) { - console.error('Start command error:', error); - await this.bot.sendMessage(chatId, '❌ Произошла ошибка. Попробуйте позже.'); + logger.error('Start command error:', error); + await this.bot.sendMessage(chatId, 'Произошла ошибка. Попробуйте позже.'); } }); @@ -130,13 +131,13 @@ export class QuixoticBot { this.bot.onText(/\/help/, async (msg: Message) => { const chatId = msg.chat.id; - const helpText = `🎵 *Quixotic - SoundCloud to MP3* + const helpText = `*Quixotic - SoundCloud to MP3* *Как пользоваться:* -1️⃣ Нажми кнопку "Открыть Quixotic" -2️⃣ Введи название песни в поисковую строку -3️⃣ Выбери нужный трек из списка -4️⃣ Получи MP3 файл в чат! +1. Нажми кнопку "Открыть Quixotic" +2. Введи название песни в поисковую строку +3. Выбери нужный трек из списка +4. Получи MP3 файл в чат! *Команды:* /start - Запустить приложение @@ -144,10 +145,10 @@ export class QuixoticBot { /history - История поиска *Возможности:* -✅ Поиск по SoundCloud -✅ Высокое качество MP3 (192kbps) -✅ Быстрая конвертация -✅ История поиска`; +- Поиск по SoundCloud +- Высокое качество MP3 (192kbps) +- Быстрая конвертация +- История поиска`; await this.bot.sendMessage(chatId, helpText, { parse_mode: 'Markdown' }); }); @@ -174,7 +175,7 @@ export class QuixoticBot { return; } - let historyText = '📋 *Последние поисковые запросы:*\n\n'; + 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`; @@ -182,8 +183,8 @@ export class QuixoticBot { await this.bot.sendMessage(chatId, historyText, { parse_mode: 'Markdown' }); } catch (error) { - console.error('History command error:', error); - await this.bot.sendMessage(chatId, '❌ Ошибка получения истории.'); + logger.error('History command error:', error); + await this.bot.sendMessage(chatId, 'Ошибка получения истории.'); } }); @@ -226,10 +227,10 @@ export class QuixoticBot { type: 'article', id: `${index}`, title: video.title, - description: `${video.channel} • ${this.formatDuration(video.duration)}`, + description: `${video.channel} - ${this.formatDuration(video.duration)}`, thumb_url: video.thumbnail, input_message_content: { - message_text: `🎵 ${video.title}\n🔗 ${video.url}` + message_text: `${video.title}\n${video.url}` } })); @@ -238,31 +239,31 @@ export class QuixoticBot { is_personal: true }); } catch (error) { - console.error('Inline query error:', error); + logger.error('Inline query error:', error); await this.bot.answerInlineQuery(queryId, []); } }); // Error handler with detailed logging this.bot.on('error', (error: any) => { - console.error('🚨 Telegram bot error:', error.message || error); - console.error('Error code:', error.code); - console.error('Full error:', error); + logger.error('Telegram bot error:', error.message || error); + logger.error('Error code:', error.code); + logger.error('Full error:', error); }); // Handle polling errors specifically this.bot.on('polling_error', (error: any) => { - console.error('🚨 Telegram polling error:', error.message || error); - console.error('Error code:', error.code); + logger.error('Telegram polling error:', error.message || error); + logger.error('Error code:', error.code); // Don't crash on polling errors, just log them if (error.code === 'ETELEGRAM') { - console.warn('⚠️ Telegram API error - continuing operation'); + logger.warn('Telegram API error - continuing operation'); } }); - console.log('✅ Bot handlers setup complete'); + logger.telegram('Bot handlers setup complete'); } private async getSearchHistory(userId: number): Promise { @@ -276,8 +277,9 @@ export class QuixoticBot { private async sendAudioFileInternal(chatId: number, audioUrlOrPath: string, title: string, performer?: string, thumbnail?: string): Promise { try { - console.log(`📤 Sending: ${title} to chat ${chatId}`); - console.log(`📂 File source: ${audioUrlOrPath}`); + logger.telegram('Sending audio', `${title} to chat ${chatId}`); + logger.debug(`File source: ${audioUrlOrPath}`); + logger.debug(`Thumbnail: ${thumbnail}`); // Check if it's a URL or local file path const isUrl = audioUrlOrPath.startsWith('http'); @@ -288,82 +290,132 @@ export class QuixoticBot { const urlParts = audioUrlOrPath.split('/'); const filename = urlParts[urlParts.length - 1]; filePath = require('path').join(process.cwd(), 'downloads', filename); - console.log(`📂 Converted URL to local path: ${filePath}`); + logger.debug(`Converted URL to local path: ${filePath}`); } const fs = require('fs'); + const path = require('path'); + const https = require('https'); // Check if file exists if (!fs.existsSync(filePath)) { - console.error(`❌ File not found: ${filePath}`); + logger.error(`File not found: ${filePath}`); throw new Error('File not found: ' + filePath); } // Get file stats for debugging const stats = fs.statSync(filePath); - console.log(`📊 File size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`); + logger.debug(`File size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`); // Generate custom filename for display const safeTitle = (title || 'audio').replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 30); const safePerformer = (performer || '').replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 20); const customFilename = safePerformer ? `${safePerformer} - ${safeTitle}.mp3` : `${safeTitle}.mp3`; - console.log(`📝 Sending as: ${customFilename}`); + logger.debug(`Sending as: ${customFilename}`); + + // Download thumbnail if provided + let thumbnailPath: string | undefined; + if (thumbnail && thumbnail.startsWith('http')) { + try { + logger.debug('Downloading 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', () => { + file.close(); + resolve(); + }); + }).on('error', (err: any) => { + fs.unlink(thumbnailPath, () => {}); + reject(err); + }); + }); + + logger.success(`Thumbnail downloaded: ${thumbnailPath}`); + } catch (thumbError: any) { + logger.warn('Failed to download thumbnail:', thumbError.message); + thumbnailPath = undefined; + } + } // Send file using stream (better for large files) const fileStream = fs.createReadStream(filePath); // Try sending as audio with metadata try { - await this.bot.sendAudio(chatId, fileStream, { + const options: any = { title: title, performer: performer || 'Unknown Artist', caption: undefined, - thumbnail: undefined, // Thumbnail requires special handling parse_mode: undefined - }, { + }; + + // Add thumbnail if downloaded + if (thumbnailPath) { + options.thumbnail = fs.createReadStream(thumbnailPath); + } + + await this.bot.sendAudio(chatId, fileStream, options, { filename: customFilename, contentType: 'audio/mpeg' }); - console.log(`✅ Audio sent successfully: ${title}`); + + logger.success(`Audio sent successfully: ${title}`); + + // Clean up thumbnail file + if (thumbnailPath) { + fs.unlink(thumbnailPath, (err: any) => { + if (err) logger.error('Failed to delete thumbnail:', err); + }); + } return; } catch (error: any) { - console.error('❌ Audio send failed:', error.message); - console.error('Error code:', error.code); + logger.error('Audio send failed:', error.message); + logger.error('Error code:', error.code); + + // Clean up thumbnail file on error + if (thumbnailPath) { + fs.unlink(thumbnailPath, () => {}); + } // Fallback: try as document try { - console.log('🔄 Retrying as document...'); + logger.info('Retrying as document...'); const docStream = fs.createReadStream(filePath); await this.bot.sendDocument(chatId, docStream, { - caption: `🎵 ${title}\n👤 ${performer || 'Unknown Artist'}`, + caption: `${title}\n${performer || 'Unknown Artist'}`, parse_mode: undefined }, { filename: customFilename, contentType: 'audio/mpeg' }); - console.log(`✅ Document sent successfully: ${title}`); + logger.success(`Document sent successfully: ${title}`); return; } catch (documentError: any) { - console.error('❌ Document send also failed:', documentError.message); + logger.error('Document send also failed:', documentError.message); throw documentError; } } } catch (error: any) { - console.error('❌ Send failed completely:', error.message); - console.error('Full error:', error); + logger.error('Send failed completely:', error.message); + logger.error('Full error:', error); // Send error message to user try { await this.bot.sendMessage(chatId, - `❌ Не удалось отправить файл.\n🎵 ${title}\n\nПопробуйте другой трек.` + `Не удалось отправить файл.\n${title}\n\nПопробуйте другой трек.` ); } catch { - console.error('❌ Could not even send error message'); + logger.error('Could not even send error message'); } // Re-throw to trigger unhandled rejection handler @@ -382,11 +434,11 @@ export class QuixoticBot { const data: WebAppData = JSON.parse(msg.web_app.data); if (data.action === 'send_audio') { - console.log(`🎵 WebApp request: ${data.title}`); + logger.telegram('WebApp request', data.title); await this.sendAudioFileInternal(chatId, data.audioUrl, data.title); } } catch (parseError: any) { - console.error('❌ WebApp data parse error:', parseError.message); + logger.error('WebApp data parse error:', parseError.message); } } @@ -405,7 +457,7 @@ if (require.main === module) { const webAppUrl = process.env.WEB_APP_URL || 'https://your-domain.com'; if (!token) { - console.error('❌ TELEGRAM_BOT_TOKEN environment variable is required'); + logger.error('TELEGRAM_BOT_TOKEN environment variable is required'); process.exit(1); } diff --git a/src/database.ts b/src/database.ts index 7e28d43..0a6dd0a 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,4 +1,5 @@ import { Pool } from 'pg'; +import { logger } from './logger'; interface TelegramUser { id: number; @@ -39,7 +40,7 @@ export class Database { `); if (!tablesExist.rows[0].exists) { - console.log('Creating database tables...'); + logger.info('Creating database tables...'); // Users table await this.pool.query(`CREATE TABLE users ( @@ -69,12 +70,12 @@ export class Database { created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )`); - console.log('Database tables created successfully'); + logger.success('Database tables created successfully'); } else { - console.log('Database tables already exist'); + logger.info('Database tables already exist'); } } catch (error) { - console.error('Database initialization error:', error); + logger.error('Database initialization error:', error); } } diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..272b918 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,81 @@ +import winston from 'winston'; + +/** + * Professional logging utility using Winston + * Provides colored console output with timestamps + */ + +const { combine, timestamp, printf, colorize, align } = winston.format; + +// Custom format for clean, readable logs +const logFormat = printf(({ level, message, timestamp }) => { + return `${timestamp} ${level}: ${message}`; +}); + +// Create Winston logger instance +const winstonLogger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: combine( + colorize({ all: true }), + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + align(), + logFormat + ), + transports: [ + new winston.transports.Console({ + stderrLevels: ['error'] + }) + ] +}); + +// Wrapper class for convenience methods +class Logger { + debug(message: string, ...meta: any[]): void { + winstonLogger.debug(message, ...meta); + } + + info(message: string, ...meta: any[]): void { + winstonLogger.info(message, ...meta); + } + + warn(message: string, ...meta: any[]): void { + winstonLogger.warn(message, ...meta); + } + + error(message: string, ...meta: any[]): void { + winstonLogger.error(message, ...meta); + } + + // Success is just info with green color + success(message: string, ...meta: any[]): void { + winstonLogger.info(message, ...meta); + } + + // Specialized logging methods + http(method: string, path: string, status: number): void { + const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info'; + winstonLogger.log(level, `${method} ${path} ${status}`); + } + + database(operation: string, details: string): void { + this.debug(`[DB] ${operation}: ${details}`); + } + + telegram(action: string, details?: string): void { + const msg = details ? `[Telegram] ${action}: ${details}` : `[Telegram] ${action}`; + this.info(msg); + } + + soundcloud(action: string, details?: string): void { + const msg = details ? `[SoundCloud] ${action}: ${details}` : `[SoundCloud] ${action}`; + this.info(msg); + } + + ffmpeg(action: string, details?: string): void { + const msg = details ? `[FFmpeg] ${action}: ${details}` : `[FFmpeg] ${action}`; + this.debug(msg); + } +} + +// Export singleton instance +export const logger = new Logger(); diff --git a/src/server.ts b/src/server.ts index a9450d1..b6087ec 100644 --- a/src/server.ts +++ b/src/server.ts @@ -10,6 +10,7 @@ ffmpeg.setFfprobePath('/usr/bin/ffprobe'); import { Database } from './database'; import { SoundCloudService } from './soundcloud'; import { QuixoticBot } from './bot'; +import { logger } from './logger'; const app = express(); const port = process.env.PORT || 3000; @@ -70,7 +71,10 @@ if (!fs.existsSync(downloadsDir)) { // Routes app.get('/', (req: Request, res: Response) => { - const indexPath = path.join(__dirname, '../public/index.html'); + // Use minified HTML in production + const isProduction = process.env.NODE_ENV === 'production'; + const htmlFile = isProduction ? 'index.min.html' : 'index.html'; + const indexPath = path.join(__dirname, '../public', htmlFile); // Set cache headers for HTML (short cache) res.set({ @@ -97,7 +101,7 @@ app.post('/api/search', async (req: Request, res: Response) => { await db.addSearchHistory(user.id, query); } } catch (dbError) { - console.error('Database error:', dbError); + logger.error('Database error:', dbError); } } @@ -105,7 +109,7 @@ app.post('/api/search', async (req: Request, res: Response) => { res.json({ videos }); } catch (error) { - console.error('Search error:', error); + logger.error('Search error:', error); res.status(500).json({ error: 'Failed to search videos' }); } }); @@ -114,7 +118,7 @@ app.post('/api/search', async (req: Request, res: Response) => { 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; - console.log('Convert request received:', { videoId, title, userId }); + logger.info(`Convert request received: ${title} (ID: ${videoId})`); if (!videoId) { return res.status(400).json({ error: 'Video ID is required' }); @@ -127,18 +131,18 @@ app.post('/api/convert', async (req: Request, res: Response) => { // Check if file already exists if (fs.existsSync(outputPath)) { - console.log('File already exists, serving cached version'); + logger.info('File already exists, serving cached version'); const audioUrl = `${req.protocol}://${req.get('host')}/downloads/${filename}`; return res.json({ audioUrl, title }); } - console.log(`Starting MP3 conversion for: ${title}`); + logger.info(`Starting MP3 conversion: ${title}`); try { // Get audio stream from YouTube - console.log(`Attempting to get audio stream for: ${videoId}`); + logger.debug(`Attempting to get audio stream for: ${videoId}`); const audioStream = await soundcloud.getAudioStream(videoId, url); - console.log('Audio stream obtained, starting FFmpeg conversion...'); + logger.info('Audio stream obtained, starting FFmpeg conversion...'); // Download to temporary file first, then convert const tempInputPath = path.join(downloadsDir, `temp_${videoId}.tmp`); @@ -152,19 +156,19 @@ app.post('/api/convert', async (req: Request, res: Response) => { writeStream.on('error', reject); }); - console.log('Temporary file saved, starting FFmpeg conversion...'); + logger.info('Temporary file saved, starting FFmpeg conversion...'); // Debug: check temp file const stats = fs.statSync(tempInputPath); - console.log(`Temp file size: ${stats.size} bytes`); + logger.debug(`Temp file size: ${stats.size} bytes`); // Test ffmpeg with simple command first try { const { execSync } = require('child_process'); execSync(`ffmpeg -i "${tempInputPath}" -t 1 -f null -`, { encoding: 'utf8', stdio: 'pipe' }); - console.log('FFmpeg file test passed'); + logger.debug('FFmpeg file test passed'); } catch (e: any) { - console.error('FFmpeg file test failed:', e.stderr || e.message); + logger.error('FFmpeg file test failed:', e.stderr || e.message); } // Convert temporary file to MP3 using ffmpeg @@ -177,23 +181,23 @@ app.post('/api/convert', async (req: Request, res: Response) => { .format('mp3') .output(outputPath) .on('start', (command: string) => { - console.log('FFmpeg started:', command); + logger.ffmpeg('Started', command); }) .on('progress', (progress: any) => { if (progress.percent) { - console.log(`Conversion progress: ${Math.round(progress.percent)}%`); + logger.ffmpeg('Progress', `${Math.round(progress.percent)}%`); } }) .on('end', () => { - console.log('MP3 conversion completed successfully'); + logger.success('MP3 conversion completed successfully'); // Clean up temporary file fs.unlink(tempInputPath, (err) => { - if (err) console.error('Failed to delete temp file:', err); + if (err) logger.error('Failed to delete temp file:', err); }); resolve(); }) .on('error', (err: Error) => { - console.error('FFmpeg error:', err.message); + logger.error('FFmpeg error:', err.message); // Clean up temporary file on error fs.unlink(tempInputPath, () => {}); reject(err); @@ -210,18 +214,18 @@ app.post('/api/convert', async (req: Request, res: Response) => { await db.addDownload(user.id, videoId, title || '', outputPath); } } catch (dbError) { - console.error('Database error:', dbError); + logger.error('Database error:', dbError); } } const audioUrl = `${req.protocol}://${req.get('host')}/downloads/${filename}`; - console.log('Conversion successful, file available at:', audioUrl); + logger.success(`Conversion successful: ${audioUrl}`); res.json({ audioUrl, title }); } catch (conversionError: any) { - console.error('Conversion failed for video:', videoId); - console.error('Error details:', conversionError.message); - console.error('Full error:', conversionError); + logger.error(`Conversion failed for video: ${videoId}`); + logger.error('Error details:', conversionError.message); + logger.error('Full error:', conversionError); // Return error - no fallbacks for Telegram bot return res.status(503).json({ @@ -232,18 +236,18 @@ app.post('/api/convert', async (req: Request, res: Response) => { } } catch (error) { - console.error('Server error:', error); + logger.error('Server error:', error); res.status(500).json({ error: 'Failed to process request' }); } }); // Direct Telegram API for sending audio app.post('/api/telegram-send', async (req: Request, res: Response) => { - console.log('🚀 Telegram send request received'); + logger.telegram('Send request received'); try { const { userId, audioUrl, title, performer, thumbnail }: { userId?: string; audioUrl?: string; title?: string; performer?: string; thumbnail?: string } = req.body; - console.log(`📤 Sending to user ${userId}: ${title}`); + logger.telegram('Sending to user', `${userId}: ${title}`); if (!userId || !audioUrl || !title) { return res.status(400).json({ error: 'Missing required fields' }); @@ -251,18 +255,18 @@ app.post('/api/telegram-send', async (req: Request, res: Response) => { const botInstance = (global as any).quixoticBot; if (!botInstance) { - console.log('❌ Bot not available'); + logger.error('Bot not available'); return res.status(500).json({ error: 'Bot not available' }); } const chatId = parseInt(userId); await botInstance.sendAudioFile(chatId, audioUrl, title, performer, thumbnail); - console.log('✅ Audio sent successfully'); + logger.success('Audio sent successfully'); res.json({ success: true, message: 'Audio sent successfully' }); } catch (error: any) { - console.error('❌ Send failed:', error.message); + logger.error('Send failed:', error.message); res.status(500).json({ error: error.message }); } }); @@ -277,7 +281,7 @@ app.get('/health', (req: Request, res: Response) => { // Error handler app.use((err: Error, _req: Request, res: Response, _next: any) => { - console.error(err.stack); + logger.error(err.stack || err.message); res.status(500).json({ error: 'Something went wrong!' }); }); @@ -297,7 +301,7 @@ setInterval(() => { if (now - stats.mtime.getTime() > maxAge) { fs.unlink(filePath, (err) => { if (!err) { - console.log('Deleted old file:', file); + logger.info('Deleted old file:', file); } }); } @@ -307,9 +311,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(`Open in browser: http://localhost:${port}`); + logger.success(`Quixotic server running on port ${port}`); + logger.info(`Downloads directory: ${downloadsDir}`); + logger.info(`Open in browser: http://localhost:${port}`); }); // Initialize Telegram bot @@ -321,33 +325,33 @@ if (botToken && botToken.length > 10 && botToken !== 'your_telegram_bot_token_he const botInstance = new QuixoticBot(botToken, webAppUrl); // Store bot instance globally for API access (global as any).quixoticBot = botInstance; - console.log('🤖 Telegram bot started and stored globally'); + logger.telegram('Bot started and stored globally'); } catch (error: any) { - console.error('❌ Bot initialization failed:', error.message); - console.warn('⚠️ Bot disabled due to error'); - console.warn('⚠️ Telegram integration will not be available'); + logger.error('Bot initialization failed:', error.message); + logger.warn('Bot disabled due to error'); + logger.warn('Telegram integration will not be available'); // Don't crash the server, continue without bot } } else { - console.warn('⚠️ TELEGRAM_BOT_TOKEN not configured properly'); - console.warn('⚠️ Bot will not start - only web interface will be available'); - console.warn('ℹ️ To enable Telegram bot, set a valid TELEGRAM_BOT_TOKEN'); + logger.warn('TELEGRAM_BOT_TOKEN not configured properly'); + logger.warn('Bot will not start - only web interface will be available'); + logger.info('To enable Telegram bot, set a valid TELEGRAM_BOT_TOKEN'); } // Handle unhandled promise rejections process.on('unhandledRejection', (reason: any, promise: Promise) => { - console.error('🚨 Unhandled Rejection at:', promise); - console.error('Reason:', reason); + logger.error('Unhandled Rejection at:', promise); + logger.error('Reason:', reason); // Log but don't crash the server if (reason?.code === 'ETELEGRAM') { - console.warn('⚠️ Telegram API error - continuing operation'); + logger.warn('Telegram API error - continuing operation'); } }); // Handle uncaught exceptions process.on('uncaughtException', (error: Error) => { - console.error('🚨 Uncaught Exception:', error); + logger.error('Uncaught Exception:', error); // Log but try to continue }); diff --git a/src/soundcloud.ts b/src/soundcloud.ts index ab66d5c..c3e4698 100644 --- a/src/soundcloud.ts +++ b/src/soundcloud.ts @@ -1,5 +1,6 @@ import scdl from 'soundcloud-downloader'; import { Readable } from 'stream'; +import { logger } from './logger'; interface SearchTrack { id: number; @@ -36,7 +37,7 @@ interface TrackInfo { export class SoundCloudService { constructor() { - console.log('SoundCloud service initialized'); + logger.soundcloud('Service initialized'); } private getHighQualityThumbnail(originalUrl: string): string { @@ -68,7 +69,7 @@ export class SoundCloudService { async searchTracks(query: string, maxResults: number = 10): Promise { try { - console.log(`Searching SoundCloud for: ${query}`); + logger.soundcloud('Searching', query); // Search for tracks on SoundCloud const searchResult = await scdl.search({ @@ -101,7 +102,7 @@ export class SoundCloudService { } if (!tracks || tracks.length === 0) { - console.log('No tracks found'); + logger.warn('No tracks found'); return []; } @@ -116,11 +117,11 @@ export class SoundCloudService { downloadable: track.downloadable })); - console.log(`Found ${trackResults.length} tracks on SoundCloud`); + logger.success(`Found ${trackResults.length} tracks on SoundCloud`); return trackResults; } catch (error: any) { - console.error('SoundCloud search error:', error.message); + logger.error('SoundCloud search error:', error.message); return []; } } @@ -136,20 +137,20 @@ export class SoundCloudService { thumbnail: this.getHighQualityThumbnail(track.artwork_url || track.user?.avatar_url || '') }; } catch (error) { - console.error('Error getting track info:', error); + logger.error('Error getting track info:', error); throw error; } } async getAudioStream(trackId: string | number, trackUrl?: string): Promise { try { - console.log(`Getting audio stream for track: ${trackId}`); + logger.soundcloud('Getting audio stream', `track: ${trackId}`); // If trackUrl is provided, use it directly if (trackUrl) { - console.log(`Using provided track URL: ${trackUrl}`); + logger.debug(`Using provided track URL: ${trackUrl}`); const stream = await scdl.download(trackUrl); - console.log('Audio stream obtained successfully from SoundCloud using URL'); + logger.success('Audio stream obtained successfully from SoundCloud using URL'); return stream; } @@ -160,39 +161,39 @@ export class SoundCloudService { 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`); + logger.debug(`Track: ${trackInfo.title}`); + logger.debug(`Artist: ${trackInfo.user?.username || 'Unknown'}`); + logger.debug(`Duration: ${Math.floor(trackInfo.duration / 1000)}s`); // Use the permalink_url from track info const stream = await scdl.download(trackInfo.permalink_url); - console.log('Audio stream obtained successfully from SoundCloud'); + logger.success('Audio stream obtained successfully from SoundCloud'); return stream; } catch (error: any) { - console.error('SoundCloud download failed:', error.message); + logger.error('SoundCloud download failed:', error.message); // Try alternative approaches try { - console.log('Trying alternative SoundCloud methods...'); + logger.info('Trying alternative SoundCloud methods...'); // Try with track ID directly const stream = await scdl.download(String(trackId)); - console.log('Audio stream obtained with track ID method'); + logger.success('Audio stream obtained with track ID method'); return stream; } catch { - console.error('Track ID method failed, trying URL construction...'); + logger.error('Track ID method failed, trying URL construction...'); // Final fallback - try constructing different URL formats try { const trackUrl = `https://soundcloud.com/${trackId}`; const stream = await scdl.download(trackUrl); - console.log('Audio stream obtained with constructed URL method'); + logger.success('Audio stream obtained with constructed URL method'); return stream; } catch (finalError: any) { - console.error('All methods failed:', finalError.message); + logger.error('All methods failed:', finalError.message); throw new Error(`SoundCloud download failed: ${error.message}`); } } diff --git a/yarn.lock b/yarn.lock index cb498fd..9532989 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz" integrity sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA== +"@colors/colors@1.6.0", "@colors/colors@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" + integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" @@ -48,6 +53,15 @@ tunnel-agent "^0.6.0" uuid "^8.3.2" +"@dabh/diagnostics@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.8.tgz#ead97e72ca312cf0e6dd7af0d300b58993a31a5e" + integrity sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q== + dependencies: + "@so-ric/colorspace" "^1.1.6" + enabled "2.0.x" + kuler "^2.0.0" + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": version "4.7.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz" @@ -205,6 +219,14 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@so-ric/colorspace@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@so-ric/colorspace/-/colorspace-1.1.6.tgz#62515d8b9f27746b76950a83bde1af812d91923b" + integrity sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw== + dependencies: + color "^5.0.2" + text-hex "1.0.x" + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz" @@ -316,13 +338,20 @@ "@types/node" "*" "@types/request" "*" -"@types/node@*", "@types/node@^24.3.0": +"@types/node@*": version "24.3.0" resolved "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz" integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== dependencies: undici-types "~7.10.0" +"@types/node@^24.10.0": + version "24.10.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.0.tgz#6b79086b0dfc54e775a34ba8114dcc4e0221f31f" + integrity sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A== + dependencies: + undici-types "~7.16.0" + "@types/pg@^8.15.5": version "8.15.5" resolved "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz" @@ -391,6 +420,11 @@ resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz" integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== +"@types/triple-beam@^1.3.2": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" + integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== + "@typescript-eslint/eslint-plugin@^8.41.0": version "8.41.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz" @@ -609,6 +643,11 @@ async@^0.2.9: resolved "https://registry.npmjs.org/async/-/async-0.2.10.tgz" integrity sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ== +async@^3.2.3: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -801,11 +840,38 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-convert@^3.0.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-3.1.2.tgz#cef9e0fd4cb90b07c14697b3fa70af9d7f4870f1" + integrity sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg== + dependencies: + color-name "^2.0.0" + +color-name@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.0.2.tgz#85054825a23e6d6f81d3503f660c4c4a2a15f04f" + integrity sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A== + color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-2.1.2.tgz#db1dd52414cc9037ada8fa7d936b8e9f6c3366c9" + integrity sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA== + dependencies: + color-name "^2.0.0" + +color@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/color/-/color-5.0.2.tgz#712ec894007ab27b37207732d182784e001b4a3d" + integrity sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA== + dependencies: + color-convert "^3.0.1" + color-string "^2.0.0" + combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1027,6 +1093,11 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" @@ -1344,6 +1415,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fecha@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" + integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== + file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" @@ -1405,6 +1481,11 @@ fluent-ffmpeg@^2.1.2: async "^0.2.9" which "^1.1.1" +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + follow-redirects@^1.15.6: version "1.15.11" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" @@ -1675,7 +1756,7 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -inherits@2.0.4, inherits@~2.0.3: +inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1832,6 +1913,11 @@ is-shared-array-buffer@^1.0.4: dependencies: call-bound "^1.0.3" +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + is-string@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz" @@ -1955,6 +2041,11 @@ keyv@^4.5.4: dependencies: json-buffer "3.0.1" +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + levn@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" @@ -1980,6 +2071,18 @@ lodash@^4.17.15: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +logform@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.7.0.tgz#cfca97528ef290f2e125a08396805002b2d060d1" + integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== + dependencies: + "@colors/colors" "1.6.0" + "@types/triple-beam" "^1.3.2" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + lower-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" @@ -2184,6 +2287,13 @@ once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + optionator@^0.9.3: version "0.9.4" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" @@ -2455,6 +2565,15 @@ readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.4.0, readable-stream@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -2533,7 +2652,7 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -2560,6 +2679,11 @@ safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" +safe-stable-stringify@^2.3.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" @@ -2748,6 +2872,11 @@ sshpk@^1.18.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" @@ -2798,6 +2927,13 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" @@ -2834,6 +2970,11 @@ terser@^5.15.1, terser@^5.44.1: commander "^2.20.0" source-map-support "~0.5.20" +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + tldts-core@^6.1.86: version "6.1.86" resolved "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz" @@ -2880,6 +3021,11 @@ tough-cookie@^5.0.0: dependencies: tldts "^6.1.32" +triple-beam@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" + integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== + ts-api-utils@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" @@ -3006,6 +3152,11 @@ undici-types@~7.10.0: resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz" integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz" @@ -3031,7 +3182,7 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -3132,6 +3283,32 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +winston-transport@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9" + integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== + dependencies: + logform "^2.7.0" + readable-stream "^3.6.2" + triple-beam "^1.3.0" + +winston@^3.18.3: + version "3.18.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.18.3.tgz#93ac10808c8e1081d723bc8811cd2f445ddfdcd1" + integrity sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww== + dependencies: + "@colors/colors" "^1.6.0" + "@dabh/diagnostics" "^2.0.8" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.7.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.9.0" + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"