donat-widget/internal/model/widget-templates.go
harold 0dfeb43124
Some checks failed
CI/CD / Build (push) Has been cancelled
add fix for voice text moderation
2025-08-14 10:11:34 +05:00

279 lines
9.7 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 && donat.show_text) {
await playSpeech(donat.text, voiceSettings, controller.signal);
}
} else if (donat.text && donat.voice_enabled && donat.show_text) {
await playSpeech(donat.text, voiceSettings, controller.signal);
}
})();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
controller.abort();
reject(new DOMException('Timeout', 'AbortError'));
}, timeoutDuration);
});
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
}