donat-widget/internal/model/widget-templates.go

285 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package model
import "fmt"
func GetTemplate1(streamerID int, donatHost, ttsHost string) string {
style := `body {
margin: 0;
padding: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
background-color: #000;
font-family: Arial, sans-serif;
}
#content {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 1920px;
margin: 20px auto;
gap: 20px;
}
#content img {
width: 100%;
height: auto;
max-height: 90vh;
object-fit: contain;
border-radius: 15px;
}
.text-container, .donation-user {
opacity: 0;
animation: fadeIn 2s forwards;
}
.text-container {
display: flex;
align-items: center;
gap: 20px;
font-size: 40px;
color: #fff;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
.donation-user {
font-size: 35px;
color: #FFD700;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
.donation-text {
margin: 0;
}
.donation-amount {
color: #4CAF50;
font-weight: bold;
padding: 5px 15px;
background: rgba(0,0,0,0.7);
border-radius: 8px;
}
@keyframes fadeIn {
to { opacity: 1; }
}`
script := fmt.Sprintf(`
let widgetUrl = 'https://%s/api';
let ttsUrl = 'https://%s/api/tts';
function startHeartbeat(streamerID) {
setInterval(async () => {
try {
await fetch(widgetUrl + '/widget/update-streamer-online/' + streamerID, {
method: 'PUT',
headers: {'Content-Type': 'application/json'}
});
} catch (error) {
console.error('Heartbeat error:', error);
}
}, 10000);
}
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;
}
function createTextElement(text) {
const container = document.createElement('div');
container.className = 'text-container';
const textElem = document.createElement('p');
textElem.className = 'donation-text';
textElem.textContent = text;
container.appendChild(textElem);
return container;
}
async function getDonatInfo(streamerID) {
try {
let response = await fetch(widgetUrl + '/widget/get-donat-for-playing/' + streamerID);
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;
}
}
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;
const onAbort = () => {
audio.pause();
audio.src = '';
reject(new DOMException('Aborted', 'AbortError'));
};
signal?.addEventListener('abort', onAbort, { 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 => {
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;
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 {
await playAudio(objectUrl, (voiceSettings.voice_sound_percent || 100) / 100, signal);
} finally {
URL.revokeObjectURL(objectUrl);
}
}
async function playMedia(donat, voiceSettings) {
const controller = new AbortController();
const timeoutDuration = (donat.duration || 5) * 1000;
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) => {
setTimeout(() => {
controller.abort();
reject(new DOMException('Timeout', 'AbortError'));
}, timeoutDuration);
});
// Promise.race будет ждать либо завершения медиа, либо таймаута
// Если медиа завершится с ошибкой (например, автоплей заблокирован), она пробросится дальше.
return Promise.race([mediaPromise, timeoutPromise]);
}
function clearContainer(container) {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
}
async function widgetView() {
const streamerID = '%v';
startHeartbeat(streamerID);
const contentDiv = document.getElementById('content');
while (true) {
let currentDonat = null;
const iterationStart = Date.now();
try {
currentDonat = await getDonatInfo(streamerID);
if (!currentDonat) {
await new Promise(r => setTimeout(r, 5000));
continue; // Переходим к следующей итерации, если донатов нет
}
// --- Отображение ---
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) :
createTextElement(currentDonat.text);
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 };
await playMedia(currentDonat, voiceSettings);
} catch (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);
}
}
}
}
document.addEventListener('DOMContentLoaded', widgetView);`, donatHost, ttsHost, streamerID)
template := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>%s</style>
</head>
<body>
<div id='content'></div>
<script>%s</script>
</body>
</html>`, style, script)
return template
}