diff --git a/internal/docs/docs.go b/internal/docs/docs.go index 4ae7526..d176c70 100644 --- a/internal/docs/docs.go +++ b/internal/docs/docs.go @@ -2048,6 +2048,9 @@ const docTemplate = `{ "audio_link": { "type": "string" }, + "donat_user": { + "type": "string" + }, "duration": { "type": "integer", "example": 30 @@ -2067,9 +2070,18 @@ const docTemplate = `{ "order_id": { "type": "string" }, + "play_content": { + "type": "boolean" + }, "scenery": { "type": "string" }, + "show_name": { + "type": "boolean" + }, + "show_text": { + "type": "boolean" + }, "text": { "type": "string" }, diff --git a/internal/docs/swagger.json b/internal/docs/swagger.json index 4bf8347..eacfd41 100644 --- a/internal/docs/swagger.json +++ b/internal/docs/swagger.json @@ -2041,6 +2041,9 @@ "audio_link": { "type": "string" }, + "donat_user": { + "type": "string" + }, "duration": { "type": "integer", "example": 30 @@ -2060,9 +2063,18 @@ "order_id": { "type": "string" }, + "play_content": { + "type": "boolean" + }, "scenery": { "type": "string" }, + "show_name": { + "type": "boolean" + }, + "show_text": { + "type": "boolean" + }, "text": { "type": "string" }, diff --git a/internal/docs/swagger.yaml b/internal/docs/swagger.yaml index df4edff..aafabda 100644 --- a/internal/docs/swagger.yaml +++ b/internal/docs/swagger.yaml @@ -401,6 +401,8 @@ definitions: type: integer audio_link: type: string + donat_user: + type: string duration: example: 30 type: integer @@ -414,8 +416,14 @@ definitions: type: integer order_id: type: string + play_content: + type: boolean scenery: type: string + show_name: + type: boolean + show_text: + type: boolean text: type: string voice_enabled: diff --git a/internal/model/models.go b/internal/model/models.go index 557cf30..46ca6c7 100644 --- a/internal/model/models.go +++ b/internal/model/models.go @@ -327,21 +327,29 @@ type MarkDonatViewed struct { } type PlayingDonat struct { - Duration int `json:"duration" example:"30"` - Image *string `json:"image_link"` - Audio *string `json:"audio_link"` - Text string `json:"text"` - Amount int `json:"amount"` - OrderID string `json:"order_id"` + Duration int `json:"duration" example:"30"` + Image *string `json:"image_link"` + Audio *string `json:"audio_link"` + Text string `json:"text"` + Amount int `json:"amount"` + OrderID string `json:"order_id"` + DonatUser *string `json:"donat_user"` + PlayContent bool `json:"play_content"` + ShowName bool `json:"show_name"` + ShowText bool `json:"show_text"` } type PlayingDonatResponse struct { - Duration int `json:"duration" example:"30"` - Image *string `json:"image_link"` - Audio *string `json:"audio_link"` - Text string `json:"text"` - Amount int `json:"amount"` - OrderID string `json:"order_id"` + Duration int `json:"duration" example:"30"` + Image *string `json:"image_link"` + Audio *string `json:"audio_link"` + Text string `json:"text"` + Amount int `json:"amount"` + OrderID string `json:"order_id"` + DonatUser *string `json:"donat_user"` + PlayContent bool `json:"play_content"` + ShowName bool `json:"show_name"` + ShowText bool `json:"show_text"` // Добавляем новые поля для настроек голоса VoiceSpeed string `json:"voice_speed,omitempty"` Scenery string `json:"scenery,omitempty"` diff --git a/internal/model/sql/query.go b/internal/model/sql/query.go index 864fa65..40f4b70 100644 --- a/internal/model/sql/query.go +++ b/internal/model/sql/query.go @@ -555,7 +555,7 @@ VALUES (@streamer_id, @voice_speed, @voice_sound_percent, @min_price)` var GetPlayingDonats = ` -SELECT w.duration, w.image, w.audio, d.text, d.amount, d.order_id +SELECT w.duration, w.image, w.audio, d.text, d.amount, d.order_id, d.donat_user, d. FROM widgets AS w INNER JOIN donats AS d ON d.widget_id = w.id diff --git a/internal/model/widget-templates.go b/internal/model/widget-templates.go index 2525b53..9ff0be4 100644 --- a/internal/model/widget-templates.go +++ b/internal/model/widget-templates.go @@ -29,6 +29,10 @@ func GetTemplate1(streamerID int, donatHost, ttsHost string) string { object-fit: contain; border-radius: 15px; } + .text-container, .donation-user { + opacity: 0; + animation: fadeIn 2s forwards; + } .text-container { display: flex; align-items: center; @@ -36,8 +40,11 @@ func GetTemplate1(streamerID int, donatHost, ttsHost string) string { font-size: 40px; color: #fff; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); - opacity: 0; - animation: fadeIn 2s forwards; + } + .donation-user { + font-size: 35px; + color: #FFD700; + text-shadow: 2px 2px 4px rgba(0,0,0,0.5); } .donation-text { margin: 0; @@ -50,9 +57,7 @@ func GetTemplate1(streamerID int, donatHost, ttsHost string) string { border-radius: 8px; } @keyframes fadeIn { - to { - opacity: 1; - } + to { opacity: 1; } }` script := fmt.Sprintf(` @@ -76,79 +81,6 @@ function createTextWithAmount(text, amount) { return container; } -async function getDonatInfo(streamerID) { - try { - let response = await fetch(widgetUrl + '/widget/get-donat-for-playing/' + streamerID); - return await response.json(); - } catch (error) { - console.error('Fetch error:', error); - return null; - } -} - -function playAudio(url, callback, volume) { - const audio = new Audio(url); - audio.volume = volume; - audio.play().then(() => { - audio.addEventListener('ended', callback); - }).catch(error => { - console.error('Error playing audio:', error); - callback(); - }); -} - -function playSpeech(text, voiceSettings) { - if (!voiceSettings.voice_enabled) return; - - 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'] - }; - - fetch(ttsUrl + '/generate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requestBody) - }) - .then(response => { - if (!response.ok) throw new Error('TTS error'); - return response.blob(); - }) - .then(blob => { - const url = URL.createObjectURL(blob); - const audio = new Audio(url); - audio.volume = (voiceSettings.voice_sound_percent || 100) / 100; - audio.play().catch(console.error); - audio.addEventListener('ended', () => { - URL.revokeObjectURL(url); - }); - }) - .catch(console.error); -} - -function playSpeechAfterAudio(audioUrl, text, voiceSettings) { - const volume = (voiceSettings.voice_sound_percent || 100) / 100; - playAudio(audioUrl, () => playSpeech(text, voiceSettings), volume); -} - -function clearContainer(container) { - while (container.firstChild) { - container.removeChild(container.firstChild); - } -} - -function addImage(container, imageUrl) { - const img = document.createElement('img'); - img.src = imageUrl; - container.appendChild(img); -} - function createTextElement(text) { const container = document.createElement('div'); container.className = 'text-container'; @@ -159,10 +91,93 @@ function createTextElement(text) { return container; } +async function getDonatInfo(streamerID) { + try { + let response = await fetch(widgetUrl + '/widget/get-donat-for-playing/' + streamerID); + return await response.json(); + } catch (error) { + console.error('Fetch error:', error); + return null; + } +} + +function playAudio(url, volume) { + return new Promise((resolve, reject) => { + const audio = new Audio(url); + audio.volume = volume; + audio.play().then(() => { + audio.addEventListener('ended', resolve); + }).catch(error => { + console.error('Error playing audio:', error); + reject(error); + }); + }); +} + +function playSpeech(text, voiceSettings) { + return new Promise((resolve, reject) => { + if (!voiceSettings.voice_enabled) return resolve(); + + 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'] + }; + + fetch(ttsUrl + '/generate', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(requestBody) + }) + .then(response => { + if (!response.ok) throw new Error('TTS error'); + return response.blob(); + }) + .then(blob => { + const url = URL.createObjectURL(blob); + const audio = new Audio(url); + audio.volume = (voiceSettings.voice_sound_percent || 100) / 100; + audio.play().catch(reject); + audio.addEventListener('ended', () => { + URL.revokeObjectURL(url); + resolve(); + }); + }) + .catch(reject); + }); +} + +async function playMedia(donat, voiceSettings) { + try { + let mediaPromise = Promise.resolve(); + const volume = (voiceSettings.voice_sound_percent || 100) / 100; + + if (donat.play_content && donat.audio_link) { + mediaPromise = playAudio(donat.audio_link, volume) + .then(() => playSpeech(donat.text, voiceSettings)); + } else if (donat.text && donat.voice_enabled) { + mediaPromise = playSpeech(donat.text, voiceSettings); + } + + const timeoutPromise = new Promise(r => setTimeout(r, donat.duration * 1000)); + await Promise.race([mediaPromise, timeoutPromise]); + } catch (error) { + console.error('Media play error:', error); + } +} + +function clearContainer(container) { + while (container.firstChild) { + container.removeChild(container.firstChild); + } +} + async function widgetView() { const streamerID = '%v'; const contentDiv = document.getElementById('content'); - const REQUEST_INTERVAL = 5000; if (!contentDiv) { console.error('Content container not found!'); @@ -171,10 +186,9 @@ async function widgetView() { while (true) { const iterationStart = Date.now(); - + try { const donat = await getDonatInfo(streamerID); - if (!donat || Object.keys(donat).length === 0) { await new Promise(r => setTimeout(r, 5000)); continue; @@ -182,20 +196,30 @@ async function widgetView() { clearContainer(contentDiv); - // Добавление элементов + // Добавление изображения if (donat.image_link) { - addImage(contentDiv, donat.image_link); + const img = document.createElement('img'); + img.src = donat.image_link; + contentDiv.appendChild(img); } - // Текст с суммой - if (donat.text) { - const textElement = donat.amount - ? createTextWithAmount(donat.text, donat.amount) - : createTextElement(donat.text); - contentDiv.appendChild(textElement); + // Отображение имени пользователя + if (donat.show_name && donat.donat_user) { + const userElem = document.createElement('div'); + userElem.className = 'donation-user'; + userElem.textContent = donat.donat_user; + contentDiv.appendChild(userElem); } - // Настройки голоса и громкости + // Отображение текста + if (donat.show_text && donat.text) { + const textElem = donat.amount ? + createTextWithAmount(donat.text, donat.amount) : + createTextElement(donat.text); + contentDiv.appendChild(textElem); + } + + // Воспроизведение медиа const voiceSettings = { voice_speed: donat.voice_speed, scenery: donat.scenery, @@ -205,39 +229,30 @@ async function widgetView() { voice_enabled: donat.voice_enabled }; - // Воспроизведение аудио и TTS - if (donat.audio_link) { - playSpeechAfterAudio(donat.audio_link, donat.text, voiceSettings); - } else if (donat.text && donat.voice_enabled) { - playSpeech(donat.text, voiceSettings); - } + await playMedia(donat, voiceSettings); - // Таймаут на основе duration - await new Promise(r => setTimeout(r, donat.duration * 1000)); - - // Отправка подтверждения просмотра + // Отправка подтверждения if (donat.order_id) { try { - const response = await fetch(widgetUrl + '/widget/donat/viewed', { + await fetch(widgetUrl + '/widget/donat/viewed', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_id: donat.order_id}), }); - if (!response.ok) console.error('Ошибка подтверждения просмотра'); } catch (error) { - console.error('Ошибка:', error); + console.error('Ошибка подтверждения:', error); } } } catch (error) { - console.error('Ошибка в цикле:', error); - } finally { - // Гарантируем задержку между итерациями - const elapsed = Date.now() - iterationStart; - const remaining = REQUEST_INTERVAL - elapsed; - if (remaining > 0) { - await new Promise(r => setTimeout(r, remaining)); - } + console.error('Ошибка обработки доната:', error); + } + + // Пауза между итерациями + const elapsed = Date.now() - iterationStart; + const remaining = 5000 - elapsed; + if (remaining > 0) { + await new Promise(r => setTimeout(r, remaining)); } } } diff --git a/internal/repository/donat/donat.go b/internal/repository/donat/donat.go index 0ec8c6b..fdb7763 100644 --- a/internal/repository/donat/donat.go +++ b/internal/repository/donat/donat.go @@ -844,6 +844,10 @@ func (repoDonat *RepoDonat) GetPlayingDonat( &donatForPlaying.Text, &donatForPlaying.Amount, &donatForPlaying.OrderID, + &donatForPlaying.DonatUser, + &donatForPlaying.PlayContent, + &donatForPlaying.ShowName, + &donatForPlaying.ShowText, ) if err != nil { diff --git a/internal/service/donat/donat.go b/internal/service/donat/donat.go index f3ee933..ab4fdea 100644 --- a/internal/service/donat/donat.go +++ b/internal/service/donat/donat.go @@ -718,12 +718,16 @@ func (donatService *ServiceDonat) GetPlayingDonat( } response := model.PlayingDonatResponse{ - Duration: playingDonat.Duration, - Image: playingDonat.Image, - Audio: playingDonat.Audio, - Text: playingDonat.Text, - Amount: playingDonat.Amount, - OrderID: playingDonat.OrderID, + Duration: playingDonat.Duration, + Image: playingDonat.Image, + Audio: playingDonat.Audio, + Text: playingDonat.Text, + Amount: playingDonat.Amount, + OrderID: playingDonat.OrderID, + DonatUser: playingDonat.DonatUser, + PlayContent: playingDonat.PlayContent, + ShowName: playingDonat.ShowName, + ShowText: playingDonat.ShowText, } filteredSettings, err := donatService.GetFiltersSettings(ctx, streamerID)