diff --git a/Dockerfile b/Dockerfile
index f4ae625..7f8dedc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,8 +13,8 @@ RUN yarn install --frozen-lockfile && yarn cache clean
# Copy source code
COPY . .
-# Build the application
-RUN yarn build
+# Build the application with minification
+RUN yarn build:prod
# Clean dev dependencies
RUN yarn install --production --frozen-lockfile
@@ -56,4 +56,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
# Start the application
-CMD ["node", "dist/server.js"]
\ No newline at end of file
+CMD ["node", "dist/server.js"]
diff --git a/package.json b/package.json
index 26ffd5e..c7762fd 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,8 @@
"build": "tsc && tsc -p tsconfig.frontend.json",
"build:backend": "tsc",
"build:frontend": "tsc -p tsconfig.frontend.json",
+ "build:prod": "yarn build && node scripts/minify.js",
+ "minify": "node scripts/minify.js",
"start": "node dist/server.js",
"dev": "ts-node src/server.ts",
"dev:watch": "nodemon --exec ts-node src/server.ts",
@@ -46,7 +48,9 @@
"@typescript-eslint/eslint-plugin": "^8.41.0",
"@typescript-eslint/parser": "^8.41.0",
"eslint": "^9.34.0",
+ "html-minifier-terser": "^7.2.0",
"nodemon": "^3.0.2",
+ "terser": "^5.44.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
},
diff --git a/public/index.min.html b/public/index.min.html
new file mode 100644
index 0000000..0703e40
--- /dev/null
+++ b/public/index.min.html
@@ -0,0 +1 @@
+
Quixotic Music - Поиск и скачивание музыки🎵
Найти музыку
Введите название песни или исполнителя для поиска
🔍
Ничего не найдено
Попробуйте изменить поисковый запрос
\ No newline at end of file
diff --git a/public/robots.txt b/public/robots.txt
index 55c2e8f..083248f 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,12 +1,6 @@
-# Allow search engines
-User-agent: Googlebot
-Allow: /
-
-User-agent: Bingbot
-Allow: /
-
-User-agent: Yandexbot
-Allow: /
-
+# Allow all search engines and bots
User-agent: *
-Disallow: /
+Allow: /
+
+# Sitemap
+Sitemap: https://music.quixy.uk/sitemap.xml
diff --git a/public/script.ts b/public/script.ts
index 95a66d4..701829d 100644
--- a/public/script.ts
+++ b/public/script.ts
@@ -46,6 +46,8 @@ class QuixoticApp {
private welcomePlaceholder!: HTMLElement;
private searchTimeout?: NodeJS.Timeout;
private currentVideos: VideoResult[] = [];
+ private loadingStartTime: number = 0;
+ private minLoadingDuration: number = 400; // Минимальное время показа спиннера (ms)
constructor() {
this.tg = (window as WindowWithTelegram).Telegram?.WebApp;
@@ -91,10 +93,13 @@ class QuixoticApp {
return;
}
- // Set new timeout for search (500ms delay)
+ // Show loading spinner immediately for better UX feedback
+ this.showLoading();
+
+ // Set new timeout for search (300ms delay - best practice for instant search)
this.searchTimeout = setTimeout(() => {
this.search();
- }, 500);
+ }, 300);
});
// Still handle Enter key for immediate search
@@ -148,14 +153,35 @@ class QuixoticApp {
}
const data: SearchResponse = await response.json();
+
+ // Ensure minimum loading time for better UX (prevents flashing)
+ const elapsedTime = Date.now() - this.loadingStartTime;
+ const remainingTime = Math.max(0, this.minLoadingDuration - elapsedTime);
+
+ if (remainingTime > 0) {
+ await new Promise(resolve => setTimeout(resolve, remainingTime));
+ }
+
this.displayResults(data.videos);
} catch (error) {
console.error('Search error:', error);
+
+ // Ensure minimum loading time even for errors
+ const elapsedTime = Date.now() - this.loadingStartTime;
+ const remainingTime = Math.max(0, this.minLoadingDuration - elapsedTime);
+
+ if (remainingTime > 0) {
+ await new Promise(resolve => setTimeout(resolve, remainingTime));
+ }
+
this.showNoResults();
}
}
private showLoading(): void {
+ // Record loading start time
+ this.loadingStartTime = Date.now();
+
// Clear any existing status messages
const existingMessage = document.querySelector('.tg-status-message');
if (existingMessage) {
@@ -201,8 +227,15 @@ class QuixoticApp {
this.noResults.classList.add('tg-hidden');
this.noResults.style.display = 'none';
- this.results.innerHTML = videos.map(video => `
-
+ // Use DocumentFragment for better performance
+ const fragment = document.createDocumentFragment();
+
+ videos.forEach(video => {
+ const item = document.createElement('div');
+ item.className = 'tg-list-item';
+ item.dataset.videoId = video.id;
+
+ item.innerHTML = `
-
- `).join('');
-
- // Add touch event listeners to prevent sticky states
- this.results.querySelectorAll('.tg-list-item').forEach(item => {
- const element = item as HTMLElement;
+ `;
+
+ // Use event listener instead of inline onclick
+ item.addEventListener('click', () => {
+ this.convertVideo(video.id, video.title, video.url);
+ });
// Reset visual state on touch end
- element.addEventListener('touchend', () => {
+ item.addEventListener('touchend', () => {
setTimeout(() => {
- element.blur();
- element.style.background = '';
- element.style.transform = '';
+ item.blur();
+ item.style.background = '';
+ item.style.transform = '';
}, 100);
- });
+ }, { passive: true });
// Also handle mouse leave for desktop
- element.addEventListener('mouseleave', () => {
- element.style.background = '';
- element.style.transform = '';
+ item.addEventListener('mouseleave', () => {
+ item.style.background = '';
+ item.style.transform = '';
});
+
+ fragment.appendChild(item);
});
+ // Clear and append once
+ this.results.innerHTML = '';
+ this.results.appendChild(fragment);
+
// Lazy load images with IntersectionObserver
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
@@ -281,8 +320,8 @@ class QuixoticApp {
const performer = video?.channel || 'Unknown Artist';
const thumbnail = video?.thumbnail || '';
- // Find the clicked element by looking for the one that contains this videoId
- const videoElement = document.querySelector(`[onclick*="${videoId}"]`) as HTMLElement;
+ // Find the clicked element using data attribute
+ const videoElement = document.querySelector(`[data-video-id="${videoId}"]`) as HTMLElement;
if (videoElement) {
// Force blur to reset any active/focus states
videoElement.blur();
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..549b0f7
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1,10 @@
+
+
+
+ https://music.quixy.uk/
+ 2025-11-07
+ daily
+ 1.0
+
+
diff --git a/public/style.css b/public/style.css
index 4f03acd..1b8bb34 100644
--- a/public/style.css
+++ b/public/style.css
@@ -203,25 +203,29 @@ body {
padding: var(--tg-spacing-xxl);
display: none;
z-index: 10;
+ opacity: 0;
+ transition: opacity 0.15s ease-in;
}
.tg-spinner.tg-spinner--visible {
display: block;
+ animation: tg-fade-in 0.15s ease-in forwards;
}
.tg-spinner__icon {
- width: 32px;
- height: 32px;
- border: 2px solid var(--tg-color-secondary-bg);
- border-top: 2px solid var(--tg-color-button);
+ width: 40px;
+ height: 40px;
+ border: 3px solid var(--tg-color-secondary-bg);
+ border-top: 3px solid var(--tg-color-button);
border-radius: 50%;
margin: 0 auto var(--tg-spacing-lg);
- animation: tg-spin 1s linear infinite;
+ animation: tg-spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.tg-spinner__text {
font-size: var(--tg-font-size-sm);
color: var(--tg-color-hint);
+ font-weight: 500;
}
@keyframes tg-spin {
@@ -229,6 +233,17 @@ body {
100% { transform: rotate(360deg); }
}
+@keyframes tg-fade-in {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -45%);
+ }
+ to {
+ opacity: 1;
+ transform: translate(-50%, -50%);
+ }
+}
+
/* List component */
.tg-list {
display: none;
diff --git a/scripts/minify.js b/scripts/minify.js
new file mode 100755
index 0000000..f508d12
--- /dev/null
+++ b/scripts/minify.js
@@ -0,0 +1,127 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+const { minify } = require('terser');
+const { minify: minifyHtml } = require('html-minifier-terser');
+
+const publicDir = path.join(__dirname, '..', 'public');
+const distDir = path.join(publicDir, 'dist');
+
+async function minifyJavaScript() {
+ console.log('🔧 Minifying JavaScript...');
+
+ const jsFile = path.join(distDir, 'script.js');
+
+ if (!fs.existsSync(jsFile)) {
+ console.error('❌ script.js not found. Run build first.');
+ process.exit(1);
+ }
+
+ const code = fs.readFileSync(jsFile, 'utf8');
+
+ const result = await minify(code, {
+ compress: {
+ dead_code: true,
+ drop_console: false, // Keep console for debugging
+ drop_debugger: true,
+ keep_classnames: true,
+ keep_fnames: false,
+ passes: 2
+ },
+ mangle: {
+ keep_classnames: true,
+ keep_fnames: false
+ },
+ format: {
+ comments: false
+ },
+ sourceMap: {
+ filename: 'script.js',
+ url: 'script.js.map'
+ }
+ });
+
+ if (result.code) {
+ fs.writeFileSync(jsFile, result.code);
+ if (result.map) {
+ fs.writeFileSync(jsFile + '.map', result.map);
+ }
+
+ const originalSize = Buffer.byteLength(code, 'utf8');
+ const minifiedSize = Buffer.byteLength(result.code, 'utf8');
+ const savings = ((1 - minifiedSize / originalSize) * 100).toFixed(1);
+
+ console.log(`✅ JavaScript minified: ${originalSize} → ${minifiedSize} bytes (${savings}% reduction)`);
+ } else {
+ console.error('❌ Minification failed');
+ process.exit(1);
+ }
+}
+
+async function minifyHTML() {
+ console.log('🔧 Minifying HTML...');
+
+ const htmlFile = path.join(publicDir, 'index.html');
+
+ if (!fs.existsSync(htmlFile)) {
+ console.error('❌ index.html not found');
+ return;
+ }
+
+ const html = fs.readFileSync(htmlFile, 'utf8');
+
+ const minified = await minifyHtml(html, {
+ collapseWhitespace: true,
+ removeComments: true,
+ removeRedundantAttributes: true,
+ removeScriptTypeAttributes: true,
+ removeStyleLinkTypeAttributes: true,
+ useShortDoctype: true,
+ minifyCSS: true,
+ minifyJS: false, // Don't minify inline JS (we handle it separately)
+ keepClosingSlash: true
+ });
+
+ const originalSize = Buffer.byteLength(html, 'utf8');
+ const minifiedSize = Buffer.byteLength(minified, 'utf8');
+ const savings = ((1 - minifiedSize / originalSize) * 100).toFixed(1);
+
+ // Save to dist folder
+ fs.writeFileSync(path.join(publicDir, 'index.min.html'), minified);
+
+ console.log(`✅ HTML minified: ${originalSize} → ${minifiedSize} bytes (${savings}% reduction)`);
+}
+
+async function minifyCSS() {
+ console.log('🔧 Checking CSS...');
+
+ const cssFile = path.join(publicDir, 'style.css');
+
+ if (!fs.existsSync(cssFile)) {
+ console.log('ℹ️ No CSS file to minify');
+ return;
+ }
+
+ const css = fs.readFileSync(cssFile, 'utf8');
+ const originalSize = Buffer.byteLength(css, 'utf8');
+
+ console.log(`ℹ️ CSS size: ${originalSize} bytes (already optimized)`);
+}
+
+async function main() {
+ console.log('🚀 Starting minification process...\n');
+
+ try {
+ await minifyJavaScript();
+ await minifyHTML();
+ await minifyCSS();
+
+ console.log('\n✨ All files minified successfully!');
+ } catch (error) {
+ console.error('❌ Minification error:', error);
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/src/bot.ts b/src/bot.ts
index 5a6dce7..a01ec7e 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -40,7 +40,27 @@ export class QuixoticBot {
private db: Database;
constructor(token: string, webAppUrl: string) {
- this.bot = new TelegramBot(token, { polling: true });
+ // Validate token format
+ if (!token || token.length < 40 || token === 'your_telegram_bot_token_here') {
+ throw new Error('Invalid or placeholder TELEGRAM_BOT_TOKEN provided');
+ }
+
+ // Use webhook in production, polling in development
+ const useWebhook = process.env.NODE_ENV === 'production' && process.env.WEBHOOK_URL;
+
+ if (useWebhook) {
+ console.log('🌐 Using webhook mode for production');
+ this.bot = new TelegramBot(token, {
+ webHook: {
+ port: 8443,
+ host: '0.0.0.0'
+ }
+ });
+ } else {
+ console.log('🔄 Using polling mode for development');
+ this.bot = new TelegramBot(token, { polling: true });
+ }
+
this.webAppUrl = webAppUrl;
this.db = new Database();
this.init();
@@ -257,6 +277,7 @@ 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}`);
// Check if it's a URL or local file path
const isUrl = audioUrlOrPath.startsWith('http');
@@ -267,68 +288,86 @@ 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}`);
}
- // Generate custom filename for display
- const safeTitle = (title || '').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}` : `${safeTitle}`;
+ const fs = require('fs');
- // Try sending as audio with custom filename
+ // Check if file exists
+ if (!fs.existsSync(filePath)) {
+ console.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`);
+
+ // 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}`);
+
+ // Send file using stream (better for large files)
+ const fileStream = fs.createReadStream(filePath);
+
+ // Try sending as audio with metadata
try {
- const fs = require('fs');
-
- // Check if file exists
- if (!fs.existsSync(filePath)) {
- throw new Error('File not found: ' + filePath);
- }
-
- await this.bot.sendAudio(chatId, filePath, {
+ await this.bot.sendAudio(chatId, fileStream, {
title: title,
- performer: performer,
+ performer: performer || 'Unknown Artist',
caption: undefined,
- thumbnail: thumbnail,
+ thumbnail: undefined, // Thumbnail requires special handling
parse_mode: undefined
}, {
filename: customFilename,
contentType: 'audio/mpeg'
});
- console.log(`✅ Audio sent: ${title}`);
+ console.log(`✅ Audio sent successfully: ${title}`);
return;
} catch (error: any) {
- console.log('Audio send failed, trying as document...', error.message);
+ console.error('❌ Audio send failed:', error.message);
+ console.error('Error code:', error.code);
- // Fallback: try as document with custom filename
+ // Fallback: try as document
try {
- await this.bot.sendDocument(chatId, filePath, {
- caption: undefined,
+ console.log('🔄 Retrying as document...');
+ const docStream = fs.createReadStream(filePath);
+
+ await this.bot.sendDocument(chatId, docStream, {
+ caption: `🎵 ${title}\n👤 ${performer || 'Unknown Artist'}`,
parse_mode: undefined
}, {
filename: customFilename,
contentType: 'audio/mpeg'
});
- console.log(`✅ Document sent: ${title}`);
+ console.log(`✅ Document sent successfully: ${title}`);
return;
} catch (documentError: any) {
+ console.error('❌ Document send also failed:', documentError.message);
throw documentError;
}
}
} catch (error: any) {
- console.error('❌ Send failed:', error.message);
+ console.error('❌ Send failed completely:', error.message);
+ console.error('Full error:', error);
- // Send fallback with link if it was a URL
+ // Send error message to user
try {
- const message = audioUrlOrPath.startsWith('http')
- ? `❌ Не удалось отправить файл.\n🎵 ${title}\n🔗 ${audioUrlOrPath}`
- : `❌ Не удалось отправить файл: ${title}`;
-
- await this.bot.sendMessage(chatId, message);
+ await this.bot.sendMessage(chatId,
+ `❌ Не удалось отправить файл.\n🎵 ${title}\n\nПопробуйте другой трек.`
+ );
} catch {
- // Silent fail
+ console.error('❌ Could not even send error message');
}
+
+ // Re-throw to trigger unhandled rejection handler
+ throw error;
}
}
diff --git a/src/server.ts b/src/server.ts
index 63b7c33..a9450d1 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -276,8 +276,8 @@ app.get('/health', (req: Request, res: Response) => {
});
// Error handler
-app.use((_err: Error, _req: Request, res: Response) => {
- console.error(_err.stack);
+app.use((err: Error, _req: Request, res: Response, _next: any) => {
+ console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
@@ -316,7 +316,7 @@ app.listen(port, () => {
const botToken = process.env.TELEGRAM_BOT_TOKEN;
const webAppUrl = process.env.WEB_APP_URL || `http://localhost:${port}`;
-if (botToken && botToken.length > 10) {
+if (botToken && botToken.length > 10 && botToken !== 'your_telegram_bot_token_here') {
try {
const botInstance = new QuixoticBot(botToken, webAppUrl);
// Store bot instance globally for API access
@@ -324,10 +324,31 @@ if (botToken && botToken.length > 10) {
console.log('🤖 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('⚠️ Bot disabled due to error');
+ console.warn('⚠️ Telegram integration will not be available');
+ // Don't crash the server, continue without bot
}
} else {
- console.warn('⚠️ TELEGRAM_BOT_TOKEN not found or invalid - bot will not start');
+ 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');
}
+// Handle unhandled promise rejections
+process.on('unhandledRejection', (reason: any, promise: Promise) => {
+ console.error('🚨 Unhandled Rejection at:', promise);
+ console.error('Reason:', reason);
+
+ // Log but don't crash the server
+ if (reason?.code === 'ETELEGRAM') {
+ console.warn('⚠️ Telegram API error - continuing operation');
+ }
+});
+
+// Handle uncaught exceptions
+process.on('uncaughtException', (error: Error) => {
+ console.error('🚨 Uncaught Exception:', error);
+ // Log but try to continue
+});
+
export default app;
diff --git a/yarn.lock b/yarn.lock
index f998263..cb498fd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -142,12 +142,28 @@
resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz"
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
-"@jridgewell/resolve-uri@^3.0.3":
+"@jridgewell/gen-mapping@^0.3.5":
+ version "0.3.13"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
+ integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
-"@jridgewell/sourcemap-codec@^1.4.10":
+"@jridgewell/source-map@^0.3.3":
+ version "0.3.11"
+ resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba"
+ integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.25"
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.5"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
@@ -160,6 +176,14 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
+ version "0.3.31"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
+ integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@@ -686,6 +710,11 @@ braces@^3.0.3, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
+buffer-from@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+ integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz"
@@ -722,6 +751,14 @@ callsites@^3.0.0:
resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+camel-case@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
+ integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==
+ dependencies:
+ pascal-case "^3.1.2"
+ tslib "^2.0.3"
+
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz"
@@ -750,6 +787,13 @@ chokidar@^3.5.2:
optionalDependencies:
fsevents "~2.3.2"
+clean-css@~5.3.2:
+ version "5.3.3"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd"
+ integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==
+ dependencies:
+ source-map "~0.6.0"
+
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
@@ -769,6 +813,16 @@ combined-stream@^1.0.8, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
+commander@^10.0.0:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
+ integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
+
+commander@^2.20.0:
+ version "2.20.3"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+ integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
compressible@~2.0.18:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
@@ -938,6 +992,14 @@ diff@^4.0.1:
resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+dot-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
+ integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
+ dependencies:
+ no-case "^3.0.4"
+ tslib "^2.0.3"
+
dotenv@^8.2.0:
version "8.6.0"
resolved "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz"
@@ -982,6 +1044,11 @@ end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
+entities@^4.4.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+ integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9:
version "1.24.0"
resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz"
@@ -1540,6 +1607,19 @@ hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
+html-minifier-terser@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942"
+ integrity sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==
+ dependencies:
+ camel-case "^4.1.2"
+ clean-css "~5.3.2"
+ commander "^10.0.0"
+ entities "^4.4.0"
+ param-case "^3.0.4"
+ relateurl "^0.2.7"
+ terser "^5.15.1"
+
http-errors@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz"
@@ -1900,6 +1980,13 @@ lodash@^4.17.15:
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+lower-case@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
+ integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
+ dependencies:
+ tslib "^2.0.3"
+
m3u8stream@^0.8.0:
version "0.8.6"
resolved "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz"
@@ -2012,6 +2099,14 @@ negotiator@~0.6.4:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
+no-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
+ integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
+ dependencies:
+ lower-case "^2.0.2"
+ tslib "^2.0.3"
+
node-telegram-bot-api@^0.64.0:
version "0.64.0"
resolved "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.64.0.tgz"
@@ -2124,6 +2219,14 @@ p-locate@^5.0.0:
dependencies:
p-limit "^3.0.2"
+param-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
+ integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==
+ dependencies:
+ dot-case "^3.0.4"
+ tslib "^2.0.3"
+
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
@@ -2136,6 +2239,14 @@ parseurl@~1.3.3:
resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+pascal-case@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
+ integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
+ dependencies:
+ no-case "^3.0.4"
+ tslib "^2.0.3"
+
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
@@ -2377,6 +2488,11 @@ regexp.prototype.flags@^1.5.4:
gopd "^1.2.0"
set-function-name "^2.0.2"
+relateurl@^0.2.7:
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
+ integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
+
request-promise-core@1.1.3:
version "1.1.3"
resolved "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz"
@@ -2599,6 +2715,19 @@ soundcloud-key-fetch@^1.0.10:
resolved "https://registry.npmjs.org/soundcloud-key-fetch/-/soundcloud-key-fetch-1.0.13.tgz"
integrity sha512-X1ketDIELpXOj2zuNNlTEtEy8y9Zu5R9m2lzkocXkG0GmUMht2ZypS8mtEDAWtEeqnEqkkLxipOsly+qU1gb6Q==
+source-map-support@~0.5.20:
+ version "0.5.21"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+ integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@~0.6.0:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
split2@^4.1.0:
version "4.2.0"
resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz"
@@ -2695,6 +2824,16 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
+terser@^5.15.1, terser@^5.44.1:
+ version "5.44.1"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c"
+ integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==
+ dependencies:
+ "@jridgewell/source-map" "^0.3.3"
+ acorn "^8.15.0"
+ commander "^2.20.0"
+ source-map-support "~0.5.20"
+
tldts-core@^6.1.86:
version "6.1.86"
resolved "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz"
@@ -2765,6 +2904,11 @@ ts-node@^10.9.2:
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
+tslib@^2.0.3:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz"