faster smaller

This commit is contained in:
Andrey Kondratev
2025-11-09 18:48:57 +05:00
parent ca27a2b3f0
commit 6db48b16a7
11 changed files with 470 additions and 76 deletions

View File

@@ -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

View File

@@ -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"
},

1
public/index.min.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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 => `
<div class='tg-list-item' onclick='app.convertVideo("${video.id}", "${this.escapeHtml(video.title)}", "${this.escapeHtml(video.url)}")'>
// 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 = `
<div class='tg-list-item__content'>
<div class='tg-list-item__media'>
<img class='tg-list-item__thumbnail'
@@ -217,29 +250,35 @@ class QuixoticApp {
<div class='tg-list-item__subtitle'>${this.escapeHtml(video.channel)}</div>
</div>
</div>
</div>
`).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();

10
public/sitemap.xml Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://music.quixy.uk/</loc>
<lastmod>2025-11-07</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
</urlset>

View File

@@ -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;

127
scripts/minify.js Executable file
View File

@@ -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();

View File

@@ -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<void> {
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}`);
}
const fs = require('fs');
// 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 || '').replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 30);
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}` : `${safeTitle}`;
const customFilename = safePerformer ? `${safePerformer} - ${safeTitle}.mp3` : `${safeTitle}.mp3`;
// Try sending as audio with custom filename
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\опробуйте другой трек.`
);
} catch {
// Silent fail
console.error('❌ Could not even send error message');
}
// Re-throw to trigger unhandled rejection handler
throw error;
}
}

View File

@@ -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<any>) => {
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;

148
yarn.lock
View File

@@ -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"