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 = `
${this.escapeHtml(video.channel)}
- - `).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"