This commit is contained in:
Andrey Kondratev
2025-08-27 18:57:09 +05:00
parent 98787a382e
commit 57f0519a32
13 changed files with 829 additions and 550 deletions

265
public/script.ts Normal file
View File

@@ -0,0 +1,265 @@
interface TelegramWebApp {
ready(): void;
expand(): void;
sendData(data: string): void;
MainButton: {
show(): void;
hide(): void;
};
initDataUnsafe?: {
user?: {
id: number;
};
};
}
interface Window {
Telegram?: {
WebApp: TelegramWebApp;
};
}
interface VideoResult {
id: string;
title: string;
channel: string;
thumbnail: string;
duration: number;
}
interface SearchResponse {
videos: VideoResult[];
}
interface ConvertResponse {
audioUrl?: string;
title: string;
}
class QuixoticApp {
private tg?: TelegramWebApp;
private searchInput!: HTMLInputElement;
private searchBtn!: HTMLButtonElement;
private loading!: HTMLElement;
private results!: HTMLElement;
private noResults!: HTMLElement;
constructor() {
this.tg = window.Telegram?.WebApp;
this.init();
this.bindEvents();
}
private init(): void {
if (this.tg) {
this.tg.ready();
this.tg.expand();
this.tg.MainButton.hide();
}
this.searchInput = document.getElementById('searchInput') as HTMLInputElement;
this.searchBtn = document.getElementById('searchBtn') as HTMLButtonElement;
this.loading = document.getElementById('loading') as HTMLElement;
this.results = document.getElementById('results') as HTMLElement;
this.noResults = document.getElementById('noResults') as HTMLElement;
}
private bindEvents(): void {
this.searchBtn.addEventListener('click', () => this.search());
this.searchInput.addEventListener('keypress', (e: KeyboardEvent) => {
if (e.key === 'Enter') {
this.search();
}
});
}
private async search(): Promise<void> {
const query = this.searchInput.value.trim();
if (!query) return;
this.showLoading();
try {
const response = await fetch('/api/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
userId: this.tg?.initDataUnsafe?.user?.id || 'demo'
})
});
if (!response.ok) {
throw new Error('Search failed');
}
const data: SearchResponse = await response.json();
this.displayResults(data.videos);
} catch (error) {
console.error('Search error:', error);
this.showNoResults();
}
}
private showLoading(): void {
this.loading.classList.remove('hidden');
this.results.classList.add('hidden');
this.noResults.classList.add('hidden');
this.searchBtn.disabled = true;
}
private hideLoading(): void {
this.loading.classList.add('hidden');
this.searchBtn.disabled = false;
}
private displayResults(videos: VideoResult[]): void {
this.hideLoading();
if (!videos || videos.length === 0) {
this.showNoResults();
return;
}
this.results.innerHTML = videos.map(video => `
<div class="video-item" onclick="app.convertVideo('${video.id}', '${this.escapeHtml(video.title)}')">
<img class="video-thumbnail" src="${video.thumbnail}" alt="${this.escapeHtml(video.title)}" loading="lazy">
<div class="video-info">
<div class="video-title">${this.escapeHtml(video.title)}</div>
<div class="video-channel">${this.escapeHtml(video.channel)}</div>
<div class="video-duration">${this.formatDuration(video.duration)}</div>
</div>
</div>
`).join('');
this.results.classList.remove('hidden');
this.noResults.classList.add('hidden');
}
private showNoResults(): void {
this.hideLoading();
this.results.classList.add('hidden');
this.noResults.classList.remove('hidden');
}
public async convertVideo(videoId: string, title: string): Promise<void> {
console.log('Convert video called:', { videoId, title });
const videoElement = (event as any)?.currentTarget as HTMLElement;
if (videoElement) {
videoElement.classList.add('converting');
}
try {
console.log('Sending convert request...');
const response = await fetch('/api/convert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
videoId,
title,
userId: this.tg?.initDataUnsafe?.user?.id || 'demo'
})
});
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error(`Conversion failed with status: ${response.status}`);
}
const data: ConvertResponse = await response.json();
console.log('Response data:', data);
if (data.audioUrl) {
// MP3 conversion successful!
console.log('MP3 conversion successful:', data.audioUrl);
if (this.tg) {
// Send to Telegram chat
this.tg.sendData(JSON.stringify({
action: 'send_audio',
audioUrl: data.audioUrl,
title: title
}));
this.showMessage('✓ MP3 готов! Отправляем в чат...', 'success');
} else {
// For testing in browser - download file
const link = document.createElement('a');
link.href = data.audioUrl;
link.download = `${title}.mp3`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.showMessage('✓ MP3 скачан!', 'success');
}
} else {
// Should not happen since we removed fallbacks
throw new Error('No audio URL received');
}
} catch (error: any) {
console.error('Conversion error:', error);
// Show specific error message
let errorMsg = 'Ошибка конвертации. Попробуйте другое видео.';
if (error.message.includes('No audio URL')) {
errorMsg = 'Не удалось получить аудио файл.';
} else if (error.message.includes('410')) {
errorMsg = 'Видео недоступно для скачивания.';
} else if (error.message.includes('403')) {
errorMsg = 'Видео заблокировано для скачивания.';
}
this.showMessage(`${errorMsg}`, 'warning');
} finally {
if (videoElement) {
videoElement.classList.remove('converting');
}
}
}
private formatDuration(seconds: number): string {
if (!seconds) return '';
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
private showMessage(message: string, type: string = 'info'): void {
// Remove existing message if any
const existingMessage = document.querySelector('.status-message');
if (existingMessage) {
existingMessage.remove();
}
// Create message element
const messageEl = document.createElement('div');
messageEl.className = `status-message status-${type}`;
messageEl.textContent = message;
// Add to page
const container = document.querySelector('.container');
if (container) {
container.insertBefore(messageEl, container.firstChild);
}
// Auto-remove after 5 seconds
setTimeout(() => {
if (messageEl.parentNode) {
messageEl.remove();
}
}, 5000);
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
const app = new QuixoticApp();
(window as any).app = app;