From efd2cb04ee0611801cacbcb8334978918c204baa Mon Sep 17 00:00:00 2001 From: harold Date: Tue, 24 Jun 2025 00:21:06 +0500 Subject: [PATCH] add fix for widgets js code --- internal/model/widget-templates.go | 214 ++++++++++++----------------- 1 file changed, 85 insertions(+), 129 deletions(-) diff --git a/internal/model/widget-templates.go b/internal/model/widget-templates.go index 11c1f2e..a12cc25 100644 --- a/internal/model/widget-templates.go +++ b/internal/model/widget-templates.go @@ -80,15 +80,12 @@ function startHeartbeat(streamerID) { function createTextWithAmount(text, amount) { const container = document.createElement('div'); container.className = 'text-container'; - const textElem = document.createElement('p'); textElem.className = 'donation-text'; textElem.textContent = text; - const amountElem = document.createElement('div'); amountElem.className = 'donation-amount'; amountElem.textContent = amount + '₽'; - container.appendChild(textElem); container.appendChild(amountElem); return container; @@ -107,7 +104,12 @@ function createTextElement(text) { async function getDonatInfo(streamerID) { try { let response = await fetch(widgetUrl + '/widget/get-donat-for-playing/' + streamerID); - return response.ok ? await response.json() : null; + if (!response.ok) { + console.error('Failed to get donation info:', response.status); + return null; + } + const data = await response.json(); + return (data && Object.keys(data).length > 0) ? data : null; } catch (error) { console.error('Fetch error (getDonatInfo):', error); return null; @@ -117,120 +119,69 @@ async function getDonatInfo(streamerID) { function playAudio(url, volume, signal) { return new Promise((resolve, reject) => { if (signal?.aborted) return reject(new DOMException('Aborted', 'AbortError')); - const audio = new Audio(url); audio.volume = volume; - let resolved = false; - - const cleanup = () => { - resolved = true; - audio.pause(); - audio.src = ""; - audio.removeEventListener('ended', onEnded); - audio.removeEventListener('error', onError); - }; - const onAbort = () => { - if (!resolved) cleanup(); + audio.pause(); + audio.src = ''; reject(new DOMException('Aborted', 'AbortError')); }; - - const onEnded = () => { - cleanup(); - resolve(audio); - }; - - const onError = (event) => { - cleanup(); - reject(event.error || new Error('Audio playback failed')); - }; - signal?.addEventListener('abort', onAbort, { once: true }); - audio.addEventListener('ended', onEnded, { once: true }); - audio.addEventListener('error', onError, { once: true }); - + audio.addEventListener('ended', () => { + signal?.removeEventListener('abort', onAbort); + resolve(audio); + }, { once: true }); + audio.addEventListener('error', (e) => { + signal?.removeEventListener('abort', onAbort); + reject(e.error || new Error('Audio playback failed')); + }, { once: true }); audio.play().catch(err => { - cleanup(); - if (err.name === 'AbortError' && signal?.aborted) { - reject(new DOMException('Aborted', 'AbortError')); - } else { - reject(err); - } + signal?.removeEventListener('abort', onAbort); + reject(err); }); }); } async function playSpeech(text, voiceSettings, signal) { if (signal?.aborted) throw new DOMException('Aborted', 'AbortError'); - if (!voiceSettings.voice_enabled) return null; - - const requestBody = { - text: text, - speed: (voiceSettings.voice_speed || 'medium').toLowerCase(), - scenery: voiceSettings.scenery || 'default', - sound_percent: voiceSettings.voice_sound_percent || 100, - min_price: voiceSettings.min_price || 0, - languages: voiceSettings.languages || ['ru'] - }; + if (!voiceSettings.voice_enabled) return; + const requestBody = { text, speed: (voiceSettings.voice_speed || 'medium').toLowerCase(), scenery: voiceSettings.scenery || 'default', sound_percent: voiceSettings.voice_sound_percent || 100, min_price: voiceSettings.min_price || 0, languages: voiceSettings.languages || ['ru'] }; + const response = await fetch(ttsUrl + '/generate', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(requestBody), signal }); + if (!response.ok) throw new Error('TTS API error: ' + response.status); + + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); try { - const response = await fetch(ttsUrl + '/generate', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(requestBody), - signal: signal - }); - - if (!response.ok) throw new Error('TTS API error: ' + response.status); - const blob = await response.blob(); - const objectUrl = URL.createObjectURL(blob); - const audio = new Audio(objectUrl); - audio.volume = (voiceSettings.voice_sound_percent || 100) / 100; - - return new Promise((resolve, reject) => { - audio.addEventListener('ended', () => { - URL.revokeObjectURL(objectUrl); - resolve(audio); - }, { once: true }); - - audio.addEventListener('error', (err) => { - URL.revokeObjectURL(objectUrl); - reject(err); - }, { once: true }); - - audio.play().catch(reject); - }); - } catch (error) { - throw error; + await playAudio(objectUrl, (voiceSettings.voice_sound_percent || 100) / 100, signal); + } finally { + URL.revokeObjectURL(objectUrl); } } async function playMedia(donat, voiceSettings) { const controller = new AbortController(); - let timeoutId; + const timeoutDuration = (donat.duration || 5) * 1000; - try { - const mediaOperation = (async () => { - if (donat.play_content && donat.audio_link) { - await playAudio(donat.audio_link, (voiceSettings.voice_sound_percent || 100) / 100, controller.signal); - if (donat.text) await playSpeech(donat.text, voiceSettings, controller.signal); - } else if (donat.text && donat.voice_enabled) { - await playSpeech(donat.text, voiceSettings, controller.signal); - } - })(); + const mediaPromise = (async () => { + if (donat.play_content && donat.audio_link) { + await playAudio(donat.audio_link, (voiceSettings.voice_sound_percent || 100) / 100, controller.signal); + if (donat.text) await playSpeech(donat.text, voiceSettings, controller.signal); + } else if (donat.text && donat.voice_enabled) { + await playSpeech(donat.text, voiceSettings, controller.signal); + } + })(); - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - controller.abort(); - reject(new DOMException('Timeout', 'AbortError')); - }, (donat.duration || 5) * 1000); - }); + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + controller.abort(); + reject(new DOMException('Timeout', 'AbortError')); + }, timeoutDuration); + }); - await Promise.race([mediaOperation, timeoutPromise]); - } finally { - clearTimeout(timeoutId); - controller.abort(); - } + // Promise.race будет ждать либо завершения медиа, либо таймаута + // Если медиа завершится с ошибкой (например, автоплей заблокирован), она пробросится дальше. + return Promise.race([mediaPromise, timeoutPromise]); } function clearContainer(container) { @@ -245,31 +196,29 @@ async function widgetView() { const contentDiv = document.getElementById('content'); while (true) { - const iterationStart = Date.now(); let currentDonat = null; + const iterationStart = Date.now(); try { currentDonat = await getDonatInfo(streamerID); - if (!currentDonat || Object.keys(currentDonat).length === 0) { + if (!currentDonat) { await new Promise(r => setTimeout(r, 5000)); - continue; + continue; // Переходим к следующей итерации, если донатов нет } - - clearContainer(contentDiv); - + + // --- Отображение --- + clearContainer(contentDiv); // Очищаем перед показом нового if (currentDonat.image_link) { const img = document.createElement('img'); img.src = currentDonat.image_link; contentDiv.appendChild(img); } - if (currentDonat.show_name && currentDonat.donat_user) { const userElem = document.createElement('div'); userElem.className = 'donation-user'; userElem.textContent = currentDonat.donat_user; contentDiv.appendChild(userElem); } - if (currentDonat.show_text && currentDonat.text) { const textElem = currentDonat.amount ? createTextWithAmount(currentDonat.text, currentDonat.amount) : @@ -277,36 +226,43 @@ async function widgetView() { contentDiv.appendChild(textElem); } - const voiceSettings = { - voice_speed: currentDonat.voice_speed, - scenery: currentDonat.scenery, - voice_sound_percent: currentDonat.voice_sound_percent, - min_price: currentDonat.min_price, - languages: currentDonat.languages, - voice_enabled: currentDonat.voice_enabled - }; - + // --- Воспроизведение --- + const voiceSettings = { voice_speed: currentDonat.voice_speed, scenery: currentDonat.scenery, voice_sound_percent: currentDonat.voice_sound_percent, min_price: currentDonat.min_price, languages: currentDonat.languages, voice_enabled: currentDonat.voice_enabled }; await playMedia(currentDonat, voiceSettings); - if (currentDonat.order_id) { - await fetch(widgetUrl + '/widget/donat/viewed', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({order_id: currentDonat.order_id}), - }); - } } catch (error) { - console.error('Ошибка обработки доната:', error); + // Логируем ошибку, но не прерываем логику ожидания и очистки + console.error('Ошибка обработки доната:', error.name === 'AbortError' ? 'Таймаут или блокировка звука' : error); + } finally { + // --- Очистка и завершение --- + // Этот блок выполнится ВСЕГДА: и после успеха, и после ошибки. + if (currentDonat) { + // Отмечаем донат как просмотренный + if (currentDonat.order_id) { + try { + await fetch(widgetUrl + '/widget/donat/viewed', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({order_id: currentDonat.order_id}), + }); + } catch (e) { + console.error('Не удалось отметить донат как просмотренный:', e); + } + } + + // Ждем оставшееся время до конца показа доната + const elapsedMs = Date.now() - iterationStart; + const durationMs = (currentDonat.duration || 5) * 1000; + const remainingTimeMs = Math.max(0, durationMs - elapsedMs); + + if (remainingTimeMs > 0) { + await new Promise(r => setTimeout(r, remainingTimeMs)); + } + + // Гарантированно очищаем контейнер + clearContainer(contentDiv); + } } - - // Очистка контента после отображения - const elapsedMs = Date.now() - iterationStart; - const remainingTimeMs = Math.max(0, (currentDonat?.duration || 5) * 1000 - elapsedMs); - - if (remainingTimeMs > 0) { - await new Promise(r => setTimeout(r, remainingTimeMs)); - } - clearContainer(contentDiv); // Ключевое изменение здесь } }