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"