From a1b07fbe61cce4d640d1d39e98cb451d3e8047a6 Mon Sep 17 00:00:00 2001
From: Tobias Koops
Date: Sun, 5 Apr 2026 16:30:46 +0200
Subject: [PATCH 1/3] =?UTF-8?q?fix:=20MA+YTMusic=20playback=20regression?=
=?UTF-8?q?=20=E2=80=94=20restore=20timeout=20tolerance=20and=20add=20titl?=
=?UTF-8?q?e=20verification=20(#345)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Restore return True on MA playback timeout (reverts #418 regression)
- Replace "any title change" with expected title substring match to prevent race condition
- Remove hardcoded 2s setTimeout on Next Round button, wait for server phase change
- Add WebSocket guard to prevent double connections (InvalidStateError fix)
- Update and add unit tests for title matching and timeout behavior
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../beatify/services/media_player.py | 29 +++---
.../beatify/www/js/player-core.js | 13 ++-
.../beatify/www/js/player-game.js | 16 ++--
.../beatify/www/js/player.bundle.min.js | 6 +-
tests/unit/test_media_player.py | 88 ++++++++++++++++++-
5 files changed, 127 insertions(+), 25 deletions(-)
diff --git a/custom_components/beatify/services/media_player.py b/custom_components/beatify/services/media_player.py
index 4effc942..c274742e 100644
--- a/custom_components/beatify/services/media_player.py
+++ b/custom_components/beatify/services/media_player.py
@@ -237,11 +237,10 @@ async def _play_via_music_assistant(self, song: dict[str, Any]) -> bool:
_LOGGER.debug("MA URI converted: %s → %s", raw_uri, uri)
_LOGGER.debug("MA playback: %s on %s", uri, self._entity_id)
+ expected_title = song.get("title", "")
+
# Capture state before to detect actual song change on speaker
state_before = self._hass.states.get(self._entity_id)
- title_before = (
- state_before.attributes.get("media_title") if state_before else None
- )
position_updated_before = (
state_before.attributes.get("media_position_updated_at")
if state_before
@@ -257,10 +256,12 @@ async def _play_via_music_assistant(self, song: dict[str, Any]) -> bool:
blocking=False,
)
- # Wait for the song to actually play on the speaker:
- # - media_title changed (new song queued)
- # - media_position is low (< 5s = song just started, not leftover from old song)
+ # Wait for the EXPECTED song to actually play on the speaker:
+ # - media_title contains expected title (not just "any change" — prevents
+ # race condition where a previous slow song arrives during retry)
+ # - media_position >= 1 (pos=0 means only queued in MA, not yet playing)
# - media_position_updated_at changed (speaker is actively reporting)
+ expected_lower = expected_title.lower()
elapsed = 0.0
while elapsed < PLAYBACK_TIMEOUT:
try:
@@ -275,13 +276,15 @@ async def _play_via_music_assistant(self, song: dict[str, Any]) -> bool:
position = state.attributes.get("media_position", 0)
position_updated = state.attributes.get("media_position_updated_at")
- title_changed = current_title and current_title != title_before
position_fresh = position_updated != position_updated_before
- # position >= 1 means speaker is actually outputting audio
- # position == 0 only means "queued" in MA, not yet playing
actually_playing = isinstance(position, (int, float)) and position >= 1
+ # Match expected title (substring, case-insensitive) — MA may
+ # append suffixes like "(Official HD Video)" or "(7″ mix)"
+ title_matches = (
+ expected_lower in current_title.lower() if current_title else False
+ )
- if title_changed and position_fresh and actually_playing:
+ if title_matches and position_fresh and actually_playing:
_LOGGER.debug(
"MA playback confirmed after %.1fs: %s (pos=%.1f)",
elapsed,
@@ -295,12 +298,14 @@ async def _play_via_music_assistant(self, song: dict[str, Any]) -> bool:
current_state = self._hass.states.get(self._entity_id)
_LOGGER.warning(
"MA playback not confirmed after %.1fs for %s (state: %s). "
- "Returning failure so the round can retry or skip. (#418)",
+ "Continuing anyway — MA may still be buffering. (#345)",
PLAYBACK_TIMEOUT,
uri,
current_state.state if current_state else "unknown",
)
- return False
+ # Return True: don't skip the song — MA+YTMusic can take >8s to buffer.
+ # Returning False would trigger retries that cause race conditions (#345).
+ return True
async def _play_via_sonos(self, song: dict[str, Any]) -> bool:
"""Play via Sonos (URI-based)."""
diff --git a/custom_components/beatify/www/js/player-core.js b/custom_components/beatify/www/js/player-core.js
index 808b934a..b06372b2 100644
--- a/custom_components/beatify/www/js/player-core.js
+++ b/custom_components/beatify/www/js/player-core.js
@@ -31,7 +31,7 @@ import {
showReactionBar, hideReactionBar, setupReactionBar,
showFloatingReaction,
updateControlBarState, handleSongStopped, handleVolumeChanged,
- handleNextRound, setupAdminControlBar, setupRevealControls,
+ handleNextRound, resetNextRoundPending, setupAdminControlBar, setupRevealControls,
setupRevealLeaderboardToggle, setupRoundAnalyticsToggle,
resetSongStoppedState,
showIntroSplashModal, hideIntroSplashModal
@@ -294,6 +294,11 @@ function connectWithSession() {
var sessionCookie = getSessionCookie();
if (!sessionCookie) return;
+ // Guard: don't open a second WebSocket if one is already connecting/open
+ if (state.ws && (state.ws.readyState === WebSocket.CONNECTING || state.ws.readyState === WebSocket.OPEN)) {
+ return;
+ }
+
var wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var wsUrl = wsProtocol + '//' + window.location.host + '/beatify/ws';
@@ -354,6 +359,11 @@ function connectWebSocket(name) {
state.playerName = name;
storePlayerName(name);
+ // Guard: don't open a second WebSocket if one is already connecting/open
+ if (state.ws && (state.ws.readyState === WebSocket.CONNECTING || state.ws.readyState === WebSocket.OPEN)) {
+ return;
+ }
+
var wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var wsUrl = wsProtocol + '//' + window.location.host + '/beatify/ws';
@@ -478,6 +488,7 @@ function handleServerMessage(data) {
state.currentRoundNumber = newRound;
resetSubmissionState();
}
+ resetNextRoundPending();
setEnergyLevel('party');
showView('game-view');
closeInviteModal();
diff --git a/custom_components/beatify/www/js/player-game.js b/custom_components/beatify/www/js/player-game.js
index b103eea8..5920de8d 100644
--- a/custom_components/beatify/www/js/player-game.js
+++ b/custom_components/beatify/www/js/player-game.js
@@ -1699,14 +1699,20 @@ export function handleNextRound() {
action: 'next_round'
}));
- setTimeout(function() {
- nextRoundPending = false;
- if (revealBtn) revealBtn.disabled = false;
- if (barBtn) barBtn.disabled = false;
- }, NEXT_ROUND_DEBOUNCE_MS);
+ // No setTimeout here — button stays disabled until the server
+ // sends a state update (phase change to PLAYING). This prevents
+ // the button from re-enabling before the new song is ready.
}
}
+/**
+ * Reset next-round pending state. Called when a new game state arrives
+ * (phase change), so the button can be used again in the next reveal.
+ */
+export function resetNextRoundPending() {
+ nextRoundPending = false;
+}
+
/**
* Handle Next Round from control bar (reuse reveal logic)
*/
diff --git a/custom_components/beatify/www/js/player.bundle.min.js b/custom_components/beatify/www/js/player.bundle.min.js
index a3d1e84f..85634b67 100644
--- a/custom_components/beatify/www/js/player.bundle.min.js
+++ b/custom_components/beatify/www/js/player.bundle.min.js
@@ -1,3 +1,3 @@
-var Ae=window.BeatifyUtils||{},s={ws:null,playerName:null,isAdmin:!1,reconnectAttempts:0,isReconnecting:!1,intentionalLeave:!1,hasReactedThisPhase:!1,currentRoundNumber:0,gameId:new URLSearchParams(window.location.search).get("game"),connectWithSession:null,connectWebSocket:null},En=document.getElementById("loading-view"),Ln=document.getElementById("not-found-view"),wn=document.getElementById("ended-view"),Sn=document.getElementById("in-progress-view"),In=document.getElementById("join-view"),Bn=document.getElementById("lobby-view"),xn=document.getElementById("game-view"),Cn=document.getElementById("reveal-view"),_n=document.getElementById("paused-view"),kn=document.getElementById("end-view"),An=document.getElementById("connection-lost-view"),Tn=[En,Ln,wn,Sn,In,Bn,xn,Cn,_n,kn,An];function I(e){Ae.showView(Tn,e),(e==="join-view"||e==="loading-view"||e==="not-found-view"||e==="ended-view"||e==="in-progress-view"||e==="connection-lost-view")&&q("calm"),e==="join-view"&&setTimeout(function(){var t=document.getElementById("name-input");t&&t.focus()},100)}function Z(e,t,n,a){return new Promise(function(i){var r=document.getElementById("confirm-modal"),o=document.getElementById("confirm-modal-title"),l=document.getElementById("confirm-modal-message"),c=document.getElementById("confirm-modal-yes"),u=document.getElementById("confirm-modal-no");if(!r||!o||!l||!c||!u){i(confirm(t||e));return}o.textContent=e,l.textContent=t,c.textContent=n||Ae.t("common.confirm")||"Confirm",u.textContent=a||Ae.t("common.cancel")||"Cancel",r.classList.remove("hidden");function m(){r.classList.add("hidden"),c.removeEventListener("click",d),u.removeEventListener("click",f),v.removeEventListener("click",f)}function d(){m(),i(!0)}function f(){m(),i(!1)}var v=r.querySelector(".modal-backdrop");c.addEventListener("click",d),u.addEventListener("click",f),v&&v.addEventListener("click",f)})}function x(e){var t=document.createElement("div");return t.textContent=e,t.innerHTML}function be(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function Nn(e){return 1-Math.pow(1-e,4)}function se(e,t,n,a,i){if(be()||t===n)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var r=K.getQualitySettings();if(r.scoreDuration===0)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var o=Math.min(a,r.scoreDuration||a);i=i||Nn;var l=null,c=null,u=!1,m=n;function d(f){if(!u){l||(l=f);var v=f-l,E=Math.min(v/o,1),w=i(E),b=Math.round(t+(m-t)*w);e.textContent=b,E<1&&(c=requestAnimationFrame(d))}}return c=requestAnimationFrame(d),{cancel:function(){u=!0,c&&cancelAnimationFrame(c)},skipToEnd:function(){u=!0,c&&cancelAnimationFrame(c),e.textContent=m}}}function ot(e,t,n,a){a=a||{};var i=500;a.betWon?i=800:a.isBigScore?i=700:a.betLost&&(i=400),e.classList.add("score-animating");var r=null;a.betWon?r="score-glow-gold":a.betLost?(r="score-shake",e.classList.add("score-flash-red")):a.streakMilestone?r="score-burst":a.isBigScore&&(r="score-pop"),r&&!be()&&e.classList.add(r),se(e,t,n,i);function o(){e.classList.remove("score-animating"),r&&e.classList.remove(r),e.classList.remove("score-flash-red")}r&&!be()?e.addEventListener("animationend",function l(){e.removeEventListener("animationend",l),o()}):setTimeout(o,i+50)}function Ne(e,t,n){if(n=n||{},!be()){var a=document.createElement("div");a.className="points-popup",a.textContent=n.text||"+"+t,n.isStreak?a.classList.add("points-popup--streak"):n.isBetWin&&a.classList.add("points-popup--gold");var i=e.getBoundingClientRect();a.style.left=i.left+i.width/2+"px",a.style.top=i.top+"px",document.body.appendChild(a),a.addEventListener("animationend",function(){a.parentNode&&a.parentNode.removeChild(a)}),setTimeout(function(){a.parentNode&&a.parentNode.removeChild(a)},1200)}}var M={players:{},leaderboard:[],initialized:!1};function lt(){return M.initialized}var rt=[3,5,10,15,20,25];function dt(e,t){for(var n=0;n=a)return a}return null}function ct(e){var t=e.map(function(a){return a.name}),n={};return t.forEach(function(a,i){var r=M.leaderboard.indexOf(a);r===-1?n[a]="new":ir&&(n[a]="down")}),n}function ut(e,t){M.players={},e.forEach(function(n){M.players[n.name]={score:n.score,rank:n.rank||0,streak:n.streak||0}}),t&&(M.leaderboard=t.map(function(n){return n.name})),M.initialized=!0}var K=(function(){var e=window.matchMedia("(prefers-reduced-motion: reduce)"),t=e.matches;e.addEventListener("change",function(i){t=i.matches});var n=null;function a(){if(n!==null)return n;var i=navigator.hardwareConcurrency||2,r=navigator.deviceMemory||4,o=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;return i<=2||r<=2?n="low":i<=4||r<=4||o?n="medium":n="high",n}return a(),{prefersReducedMotion:function(){return t},getDeviceTier:a,getQualitySettings:function(){var i=a();if(t)return{confettiParticles:0,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!1};switch(i){case"low":return{confettiParticles:5,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!0};case"medium":return{confettiParticles:10,scoreDuration:300,leaderboardAnimation:"simplified",neonGlow:!1,enableAnimations:!0};default:return{confettiParticles:15,scoreDuration:500,leaderboardAnimation:"full",neonGlow:!0,enableAnimations:!0}}},ifMotionAllowed:function(i,r){t?r&&r():i()},withWillChange:function(i,r,o){i&&(i.style.willChange=r,setTimeout(function(){i&&i.style&&(i.style.willChange="auto")},(o||500)+100))}}})(),$=(function(){var e=[],t=!1,n=null,a=null,i=2e3;function r(){if(a&&(clearTimeout(a),a=null),e.length===0){t=!1,n=null;return}n=e.shift(),a=setTimeout(function(){n&&n.skipToEnd&&n.skipToEnd(),r()},i),n.run(function(){a&&(clearTimeout(a),a=null),r()})}return{add:function(o){e.push(o),t||(t=!0,r())},skipAll:function(){a&&(clearTimeout(a),a=null),n&&n.skipToEnd&&n.skipToEnd(),e.forEach(function(o){o.skipToEnd&&o.skipToEnd()}),e=[],t=!1,n=null},clear:function(){a&&(clearTimeout(a),a=null),e=[],t=!1,n=null},isRunning:function(){return t},getMaxDuration:function(){return i}}})(),X={VISIBLE_BUFFER:2,ENTRY_HEIGHT:48,MIN_PLAYERS_FOR_LAZY:10,ROOT_MARGIN:"96px 0px",DEFAULT_VIEWPORT_HEIGHT:280},h={observer:null,fullData:[],visibleRange:{start:0,end:10},listEl:null,isLazyEnabled:!1};function mt(e){e&&(h.observer&&h.listEl!==e&&(h.observer.disconnect(),h.observer=null),!h.observer&&(h.listEl=e,h.observer=new IntersectionObserver(function(t){t.forEach(function(n){if(!(!n.isIntersecting||!h.isLazyEnabled)){var a=h.fullData,i=h.visibleRange,r=X.VISIBLE_BUFFER;if(n.target.classList.contains("leaderboard-sentinel--top")){if(i.start>0){var o=Math.max(0,i.start-r);h.visibleRange.start=o,re()}}else if(n.target.classList.contains("leaderboard-sentinel--bottom")&&i.end0&&(l+=''),l+='';for(var c=n.start;c',r>0&&(l+=''),e.innerHTML=l,e.scrollTop=o,h.observer){var u=e.querySelectorAll(".leaderboard-sentinel");u.forEach(function(m){h.observer.observe(m)})}}}function Me(e){if(!e)return"";if(e.separator)return'...
';var t=e.name||"Unknown",n=e.rank||0,a=e.score||0,i=n<=3?"is-top-"+n:"",r=e.is_current?"is-current":"",o="";e.rank_change>0||e._rankChange==="up"?o="leaderboard-entry--climbing leaderboard-entry--slide-up":(e.rank_change<0||e._rankChange==="down")&&(o="leaderboard-entry--falling leaderboard-entry--slide-down");var l="";e.rank_change>0?l='\u25B2'+e.rank_change+"":e.rank_change<0&&(l='\u25BC'+Math.abs(e.rank_change)+"");var c="";if(e.streak>=2){var u=e.streak>=5?"streak-indicator--hot":"";c='\u{1F525}'+e.streak+""}var m=e.connected===!1?"leaderboard-entry--disconnected":"",d=e.connected===!1?'(away)':"",f=e._displayScore!==void 0?e._displayScore:a;return'#'+n+''+x(t)+d+''+c+l+''+f+"
"}function Re(e,t){for(var n=X,a=h.listEl&&h.listEl.clientHeight||n.DEFAULT_VIEWPORT_HEIGHT,i=Math.ceil(a/n.ENTRY_HEIGHT),r=n.VISIBLE_BUFFER,o=-1,l=0;l=e.length-i?(c=Math.max(0,e.length-i-r),u=e.length):(c=Math.max(0,o-Math.floor(i/2)-r),u=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:c,end:u}}function ft(){h.observer&&(h.observer.disconnect(),h.observer=null),h.isLazyEnabled=!1,h.fullData=[]}function vt(){var e;function t(){clearTimeout(e),e=setTimeout(function(){h.isLazyEnabled&&h.fullData.length>0&&(h.visibleRange=Re(h.fullData,s.playerName),re())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function gt(){var e=document.getElementById("qr-share-area");if(!(!e||e.tagName!=="DETAILS")){var t="beatify_qr_expanded",n=768,a=sessionStorage.getItem(t);a!==null?e.open=a==="true":e.open=window.innerWidth>=n,e.addEventListener("toggle",function(){sessionStorage.setItem(t,e.open.toString())})}}function pt(){var e=document.querySelectorAll(".lobby-container--compact .section-header-collapsible");e.forEach(function(t){t.addEventListener("click",function(){var n=t.closest(".section-collapsible");if(n){var a=n.classList.contains("collapsed");n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",a?"true":"false")}})})}var yt={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},g={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function bt(e){if(e){g.container=e;var t=!1;g.scrollHandler=function(){g.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){Te(),t=!1}),t=!0)};var n;g.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){g.isVirtual&&Te()},100)},e.addEventListener("scroll",g.scrollHandler,{passive:!0}),window.addEventListener("resize",g.resizeHandler)}}function ht(e,t){g.items=e,g.renderItem=t;var n=g.container;if(n){var a=n.scrollTop,i=g.isVirtual;e.length0&&(n.scrollTop=a,g.scrollTop=a)}}function Mn(){var e=g.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",g.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",g.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",g.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function Te(){var e=yt,t=g.items,n=g.container,a=g.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=g.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,c=Math.max(0,Math.floor(r/o)-l),u=Math.min(t.length,Math.ceil((r+i)/o)+l);g.topSpacer&&(g.topSpacer.style.height=c*o+"px"),g.bottomSpacer&&(g.bottomSpacer.style.height=(t.length-u)*o+"px");for(var m="",d=c;d"u"){console.warn("[Confetti] Library not loaded");return}O();var t=K.getQualitySettings(),n=t.confettiParticles;if(n===0){st();return}var a=K.getDeviceTier(),i=a==="low"?.5:a==="medium"?.75:1;switch(e=e||"exact",e){case"exact":var r=Math.round(2e3*i),o=Date.now()+r;(function v(){confetti({particleCount:n,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()0);var l=e.slice().sort(function(d,f){return d.connected!==f.connected?d.connected?-1:1:0}),c=Lt.map(function(d){return d.name}),u=l.filter(function(d){return c.indexOf(d.name)===-1}).map(function(d){return d.name});g.container||bt(t);var m=function(d){var f=u.indexOf(d.name)!==-1,v=d.name===s.playerName,E=d.connected===!1,w=["player-card",f?"is-new":"",v?"player-card--you":"",E?"player-card--disconnected":""].filter(Boolean).join(" "),b=E?'(away)':"";return''+x(d.name)+(v?''+U.t("leaderboard.you")+"":"")+b+"
"};ht(l,m),setTimeout(function(){var d=g.isVirtual?g.contentWrapper:t;if(d)for(var f=d.querySelectorAll(".is-new"),v=0;vQR code library not loaded
',t.onclick=wt,t.onkeydown=function(n){(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),wt())})}}function wt(){if(P){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:P,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function Oe(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function Bt(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",Oe),n&&n.addEventListener("click",Oe),document.addEventListener("keydown",function(a){a.key==="Escape"&&e&&!e.classList.contains("hidden")&&Oe()})}function On(){if(P){var e=document.getElementById("invite-modal"),t=document.getElementById("invite-modal-code"),n=document.getElementById("invite-modal-url");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:P,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',n&&(n.value=P),e.classList.remove("hidden"),document.body.style.overflow="hidden";var a=document.getElementById("invite-modal-close");a&&a.focus()}}}function oe(){var e=document.getElementById("invite-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="");var t=document.getElementById("invite-copy-feedback");t&&t.classList.add("hidden")}function Pn(){var e=document.getElementById("invite-modal-url"),t=document.getElementById("invite-copy-feedback");!e||!P||(navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(P).then(function(){xt(t)}).catch(function(){St(e,t)}):St(e,t))}function St(e,t){e.select(),e.setSelectionRange(0,99999);try{document.execCommand("copy"),xt(t)}catch(n){console.warn("[Beatify] Copy failed:",n)}}function xt(e){e&&(e.classList.remove("hidden"),setTimeout(function(){e.classList.add("hidden")},2e3))}function Ct(){var e=document.getElementById("invite-modal"),t=e?e.querySelector(".invite-modal-backdrop"):null,n=document.getElementById("invite-modal-close"),a=document.getElementById("invite-players-btn"),i=document.getElementById("invite-copy-btn");t&&t.addEventListener("click",oe),n&&n.addEventListener("click",oe),a&&a.addEventListener("click",On),i&&i.addEventListener("click",Pn),document.addEventListener("keydown",function(r){r.key==="Escape"&&e&&!e.classList.contains("hidden")&&oe()})}function _t(e){var t=document.getElementById("admin-controls"),n=document.getElementById("lobby-status");if(t){(!e||!Array.isArray(e))&&(e=[]);var a=e.find(function(r){return r.name===s.playerName}),i=a?.is_admin===!0;i?(t.classList.remove("hidden"),n&&n.classList.add("hidden")):(t.classList.add("hidden"),n&&n.classList.remove("hidden"))}}function kt(){var e=document.getElementById("start-game-btn");e?.addEventListener("click",function(){!s.ws||s.ws.readyState!==WebSocket.OPEN||(e.disabled=!0,e.textContent=U.t("game.starting"),s.ws.send(JSON.stringify({type:"admin",action:"start_game"})))})}function At(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=U.t("player.welcomeBack",{name:e}),t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},2e3))}function Tt(){var e=document.getElementById("volume-indicator");e&&(e.textContent=U.t("earlyReveal.message")||"All guesses in!",e.classList.remove("hidden"),e.classList.add("is-visible"),setTimeout(function(){e.classList.remove("is-visible"),setTimeout(function(){e.classList.add("hidden")},300)},1500))}var y=window.BeatifyUtils||{},Ee=null;function Rt(e){Y();var t=document.getElementById("timer");if(!t)return;t.classList.remove("timer--warning","timer--critical");function n(){var a=Date.now(),i=Math.max(0,Math.ceil((e-a)/1e3));t.textContent=i,i<=5?(t.classList.remove("timer--warning"),t.classList.add("timer--critical")):i<=10?(t.classList.remove("timer--critical"),t.classList.add("timer--warning")):t.classList.remove("timer--warning","timer--critical"),i===10?t.setAttribute("aria-label","10 seconds remaining"):i===5?t.setAttribute("aria-label","5 seconds!"):i===0?t.setAttribute("aria-label","Time is up!"):t.setAttribute("aria-label","Time remaining: "+i+" seconds"),i<=0&&Y()}n(),Ee=setInterval(n,1e3)}function Y(){Ee&&(clearInterval(Ee),Ee=null)}function Ot(e){var t=document.getElementById("current-round"),n=document.getElementById("total-rounds"),a=document.getElementById("last-round-banner");t&&(t.textContent=e.round||1),n&&(n.textContent=e.total_rounds||10),a&&(e.last_round?a.classList.remove("hidden"):a.classList.add("hidden"));var i=document.getElementById("intro-badge"),r=document.getElementById("intro-splash");if(i)if(e.is_intro_round){i.classList.remove("hidden");var o=i.querySelector("[data-i18n]");e.intro_stopped?(i.classList.add("intro-badge--stopped"),o&&(o.setAttribute("data-i18n","game.introStopped"),o.textContent=y.t("game.introStopped")||"Intro complete!")):(i.classList.remove("intro-badge--stopped"),o&&(o.setAttribute("data-i18n","game.introRound"),o.textContent=y.t("game.introRound")||"INTRO ROUND"),r&&!r._shown&&(r._shown=!0,r.classList.remove("hidden"),setTimeout(function(){r.classList.add("hidden")},2e3)))}else i.classList.add("hidden"),i.classList.remove("intro-badge--stopped"),r&&(r.classList.add("hidden"),r._shown=!1);var l=document.getElementById("album-cover"),c=document.getElementById("album-loading");if(l&&e.song){c&&c.classList.remove("hidden");var u=e.song.album_art||"/beatify/static/img/no-artwork.svg";l.onload=function(){c&&c.classList.add("hidden")},l.onerror=function(){l.src="/beatify/static/img/no-artwork.svg",c&&c.classList.add("hidden")},l.src=u}Gn(e.players),e.leaderboard&&Ue(e,"leaderboard-list"),Zn(e.players),e.artist_challenge!==void 0&&zn(e.artist_challenge,"PLAYING"),e.movie_challenge!==void 0&&Jn(e.movie_challenge,"PLAYING")}function Pt(e){if(e){var t=document.getElementById("album-cover"),n=document.getElementById("album-loading");if(t&&e.album_art){var a=e.album_art;if(t.src===a)return;t.style.transition="opacity 0.3s ease-in-out",t.style.opacity="0.5";var i=new Image;i.onload=function(){t.src=a,t.style.opacity="1",n&&n.classList.add("hidden")},i.onerror=function(){t.src="/beatify/static/img/no-artwork.svg",t.style.opacity="1",n&&n.classList.add("hidden")},i.src=a}console.log("[Metadata] Updated:",e.artist,"-",e.title)}}function Hn(e){if(!e)return"?";var t=e.trim();if(!t)return"?";var n=t.split(/[\s-]+/).filter(Boolean);return n.length>=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,Math.min(2,t.length)).toUpperCase()}function Gn(e){var t=document.getElementById("submission-tracker"),n=document.getElementById("submitted-players");if(!(!t||!n)){var a=e||[],i=a.filter(function(l){return l.submitted}).length,r=a.length,o=i===r&&r>0;t.classList.toggle("all-submitted",o),n.innerHTML=a.map(function(l){var c=Hn(l.name),u=l.name===s.playerName,m=l.connected===!1,d=["player-indicator",l.submitted?"is-submitted":"",u?"is-current-player":"",m?"player-indicator--disconnected":""].filter(Boolean).join(" "),f="";return l.steal_used&&(f+='\u{1F977}'),l.bet&&(f+='\u{1F3B2}'),''+f+'
'+x(c)+'
'+x(l.name)+" "}).join("")}}function Ue(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&<(),o=r?ct(a):{};a.forEach(function(d){d.is_current=d.name===s.playerName;var f=o[d.name];f&&(d._rankChange=f);var v=M.players[d.name],E=v?v.score:d.score;d._prevScore=E,d._displayScore=n?E:d.score});var l=Dn(a,s.playerName),c=a.length>=X.MIN_PLAYERS_FOR_LAZY;if(c)h.observer||mt(i),h.fullData=l,h.isLazyEnabled=!0,h.listEl=i,h.visibleRange=Re(l,s.playerName),re();else{h.isLazyEnabled=!1;var u="";l.forEach(function(d){u+=Me(d)}),i.innerHTML=u}var m=[];r&&l.forEach(function(d){!d.separator&&d._prevScore!==d.score&&m.push({name:d.name,prevScore:d._prevScore,newScore:d.score})}),r&&m.length>0&&requestAnimationFrame(function(){for(var d={},f=i.querySelectorAll(".leaderboard-entry[data-name]"),v=0;v8&&Vn(i),Fn(a),jn(a),ut(e.players||[],a)}}function Dn(e,t){if(e.length<=10)return e;for(var n=e.slice(0,5),a=e.slice(-3),i=-1,r=0;r=e.length-3?[].concat(n,[{separator:!0}],a):[].concat(n,[{separator:!0}],[e[i]],[{separator:!0}],a)}function Vn(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Fn(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=y.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function Ht(){var e=document.getElementById("leaderboard-toggle"),t=document.getElementById("game-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function jn(e,t){var n=t?[t]:["leaderboard-summary","reveal-leaderboard-summary"];n.forEach(function(a){var i=document.getElementById(a);if(!(!i||!e||e.length===0)){var r=e[0];r&&(i.textContent=r.name+": "+r.score)}})}function Gt(){var e=document.getElementById("reveal-leaderboard-toggle"),t=document.getElementById("reveal-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function Dt(){var e=document.getElementById("round-analytics-toggle"),t=document.getElementById("round-analytics");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}var G=!1,le=!1,de=!1,z=!1,H=null,Le=null,Wn=300,Nt=0,te=!1,ne=null,qn=500,Mt=0;function Vt(){var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!(!e||!t)){e.addEventListener("input",function(){t.textContent=this.value});var n=document.getElementById("bet-toggle");n&&n.addEventListener("click",function(){G||(le=!le,n.classList.toggle("is-active",le))});var a=document.getElementById("submit-btn");a&&a.addEventListener("click",Un);var i=document.getElementById("steal-btn");i&&i.addEventListener("click",$n);var r=document.getElementById("steal-modal-close");r&&r.addEventListener("click",qe);var o=document.getElementById("steal-modal");if(o){var l=o.querySelector(".steal-modal-backdrop");l&&l.addEventListener("click",qe)}}}function Un(){if(!G){var e=document.getElementById("year-slider"),t=document.getElementById("submit-btn");if(!(!e||!t)){var n=parseInt(e.value,10);t.disabled=!0,t.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"submit",year:n,bet:le})):(We(y.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function ze(){G=!0;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("bet-toggle");e&&e.classList.add("is-submitted"),t&&t.classList.add("hidden"),a&&a.classList.add("hidden"),n&&n.classList.remove("hidden")}function Ft(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(We(y.t("errors.timesUp")),G=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?ze():We(e.message||"Submission failed")}function We(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=y.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function jt(){G=!1,le=!1;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("year-slider"),i=document.getElementById("bet-toggle");if(e&&e.classList.remove("is-submitted"),t&&(t.disabled=!1,t.classList.remove("hidden","is-loading","is-error"),t.textContent=y.t("game.submitGuess")),i&&i.classList.remove("hidden","is-active"),n&&n.classList.add("hidden"),a){a.value=1990;var r=document.getElementById("selected-year");r&&(r.textContent="1990")}de=!1,Ye(),Qn(),Xn()}function zn(e,t){var n=document.getElementById("artist-challenge-container");if(n){if(!e||!e.options){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("artist-options"),i=document.getElementById("artist-result"),r=Array.from(a.querySelectorAll(".artist-option-btn")).map(function(m){return m.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(m,d){var f=document.createElement("button");f.className="artist-option-btn",f.dataset.artist=m,f.dataset.index=d,f.textContent=m,f.addEventListener("click",function(){Yn(m)}),a.appendChild(f)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(m){m.classList.add("is-disabled"),m.classList.remove("is-loading","is-wrong");var d=e.correct_artist||Le;d&&m.dataset.artist===d&&m.classList.add("is-winner")}),e.winner===s.playerName){var c=e.bonus_points||5;i.textContent=(y.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",c),i.className="artist-result is-winner"}else{var u=(y.t("artistChallenge.someoneBeatYou")||"{winner} got it first!").replace("{winner}",e.winner);i.textContent=u,i.className="artist-result is-late"}i.classList.remove("hidden"),z=!0}else z||i.classList.add("hidden")}}function Yn(e){var t=Date.now();if(!(t-Nt0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}De();var a=(y.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Ve(a,!0),te=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),De(),Ve(y.t("movieChallenge.wrongGuess")||"Not quite...",!1),te=!0;ne=null}function De(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Ve(e,t){var n=document.getElementById("movie-result");n&&(n.textContent=e,n.className="movie-result "+(t?"is-winner":"is-late"),n.classList.remove("hidden"))}function Xn(){te=!1,ne=null;var e=document.getElementById("movie-challenge-container");e&&e.classList.add("hidden");var t=document.getElementById("movie-options");t&&(t.innerHTML="");var n=document.getElementById("movie-result");n&&(n.classList.add("hidden"),n.className="movie-result hidden")}function zt(e,t){var n=document.getElementById("movie-reveal-section");if(n){if(!e||!e.correct_movie){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("movie-reveal-name");a&&(a.textContent=e.correct_movie);var i=document.getElementById("movie-reveal-winners");if(i&&e.results){var r=e.results.winners||[];if(r.length>0){i.innerHTML="",i.classList.remove("hidden");var o=document.createElement("div");o.className="movie-reveal-winners-title",o.textContent=y.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(c){var u=document.createElement("div");u.className="movie-reveal-winner-entry",c.name===t?u.classList.add("is-you"):u.classList.add("is-other"),u.textContent=c.name+" \u2014 +"+c.bonus+" ("+c.time+"s)",i.appendChild(u)})}else{i.innerHTML="",i.classList.remove("hidden");var l=document.createElement("div");l.className="movie-reveal-no-winner",l.textContent=y.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}function Zn(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){de=t.steal_available&&!G;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");de?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):Ye()}}}function Ye(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden")}function $n(){!de||G||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function ea(e){var t=document.getElementById("steal-modal"),n=document.getElementById("steal-target-list");if(!(!t||!n)){if(n.innerHTML="",!e||e.length===0){var a=document.createElement("p");a.className="steal-no-targets",a.textContent=y.t("steal.waitForSubmit"),n.appendChild(a)}else e.forEach(function(i){var r=document.createElement("button");r.className="steal-target-btn",r.textContent=i,r.addEventListener("click",function(){ta(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function qe(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function ta(e){var t=y.t("steal.confirm").replace("{name}",e),n=await Z(y.t("steal.confirmTitle")||"Steal Answer?",t,y.t("steal.confirmButton")||"Steal",y.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),qe())}function Yt(e){if(e.success){de=!1,G=!0,Ye();var t=document.getElementById("year-selector"),n=document.getElementById("submit-btn"),a=document.getElementById("submitted-confirmation");t&&t.classList.add("is-submitted"),n&&n.classList.add("hidden"),a&&a.classList.remove("hidden"),na(e.target,e.year);var i=document.getElementById("selected-year"),r=document.getElementById("year-slider");i&&(i.textContent=e.year),r&&(r.value=e.year)}}function Qt(e){ea(e.targets||[])}function na(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=y.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var Fe=!1,aa=500,ce=!1,Qe=.5;function we(){return Fe?!1:(Fe=!0,setTimeout(function(){Fe=!1},aa),!0)}function Je(){if(s.isAdmin){var e=document.getElementById("admin-control-bar");e&&(e.classList.remove("hidden"),document.body.classList.add("has-control-bar"))}}function ue(){var e=document.getElementById("admin-control-bar");e&&(e.classList.add("hidden"),document.body.classList.remove("has-control-bar"))}function Jt(){var e=document.getElementById("reaction-bar");e&&e.classList.remove("hidden")}function me(){var e=document.getElementById("reaction-bar");e&&e.classList.add("hidden")}function ia(e){s.hasReactedThisPhase||(s.hasReactedThisPhase=!0,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"reaction",emoji:e})))}function Kt(){var e=document.getElementById("reaction-bar");if(e){var t=e.querySelectorAll(".reaction-btn");t.forEach(function(n){n.addEventListener("click",function(){var a=n.getAttribute("data-emoji");a&&ia(a)})})}}function Xt(e,t){var n=document.getElementById("reaction-container");if(n){var a=document.createElement("div");a.className="reaction-bubble",a.textContent=e+" "+t,a.style.left=20+Math.random()*60+"%",n.appendChild(a),setTimeout(function(){a.remove()},3e3)}}function Se(e){var t=document.getElementById("stop-song-btn"),n=document.getElementById("next-round-admin-btn");if(e==="PLAYING"){if(Ke(),t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.skip"))}}else if(e==="REVEAL"){if(t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}else if(n){n.classList.add("is-disabled"),n.disabled=!0;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}function ra(){if(!ce&&we()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){console.warn("[Beatify] Cannot stop song: WebSocket not connected");return}var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-label");t&&(t.textContent=y.t("game.stopping"))}s.ws.send(JSON.stringify({type:"admin",action:"stop_song"}))}}function sa(){if(Qe>=1){Zt("max");return}we()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function oa(){if(Qe<=0){Zt("min");return}we()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function Zt(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=e==="max"?"\u{1F50A} Max":"\u{1F507} Min",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1e3))}async function la(){var e=await Z(y.t("admin.endGameConfirm")||"End Game?",y.t("admin.endGameWarning")||"All players will be disconnected.",y.t("admin.endGame")||"End Game",y.t("common.cancel"));if(e&&we()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(y.t("errors.CONNECTION_LOST"));return}var t=document.getElementById("end-game-btn");if(t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var je=!1,da=2e3;function $t(){if(!je&&s.ws&&s.ws.readyState===WebSocket.OPEN){je=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=y.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"})),setTimeout(function(){je=!1,e&&(e.disabled=!1),t&&(t.disabled=!1)},da)}}function ca(){$t()}function en(){var e=document.getElementById("stop-song-btn"),t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn"),a=document.getElementById("next-round-admin-btn"),i=document.getElementById("end-game-btn");e&&e.addEventListener("click",ra),t&&t.addEventListener("click",sa),n&&n.addEventListener("click",oa),a&&a.addEventListener("click",ca),i&&i.addEventListener("click",la)}function tn(){ce=!0;var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-stopped"),e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u2713"),n&&(n.textContent=y.t("game.stopped"))}}function Ke(){ce=!1;var e=document.getElementById("stop-song-btn");if(e){e.classList.remove("is-stopped"),e.classList.remove("is-disabled"),e.disabled=!1;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u23F9\uFE0F"),n&&(n.textContent=y.t("game.stop"))}}function nn(e){Qe=e,ua(e),ma(e)}function ua(e){var t=document.getElementById("volume-indicator");if(t){var n=Math.round(e*100);t.textContent="\u{1F50A} "+n+"%",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1500)}}function ma(e){var t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn");t&&t.classList.toggle("is-at-limit",e>=1),n&&n.classList.toggle("is-at-limit",e<=0)}function an(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",$t);var t=document.getElementById("reveal-view");t&&t.addEventListener("click",function(n){n.target.tagName==="BUTTON"||n.target.closest("button")||($.isRunning()&&$.skipAll(),O())})}function rn(e){var t=document.getElementById("intro-splash-modal");if(t){t.classList.remove("hidden");var n=document.getElementById("intro-splash-confirm-btn"),a=t.querySelector(".intro-splash-modal-waiting");n&&(e?(n.classList.remove("hidden"),a&&a.classList.add("hidden"),n.onclick=function(){s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"admin",action:"confirm_intro_splash"}))}):(n.classList.add("hidden"),a&&a.classList.remove("hidden")))}}function sn(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var p=window.BeatifyUtils||{};function Xe(e){var t=e.song||{},n=e.players||[],a=document.getElementById("reveal-round"),i=document.getElementById("reveal-total");a&&(a.textContent=e.round||1),i&&(i.textContent=e.total_rounds||10);var r=document.getElementById("intro-badge");if(r)if(e.is_intro_round){r.classList.remove("hidden"),r.classList.add("intro-badge--stopped");var o=r.querySelector("[data-i18n]");o&&(o.setAttribute("data-i18n","game.introStopped"),o.textContent=p.t("game.introStopped")||"Intro complete!")}else r.classList.add("hidden");var l=document.getElementById("reveal-album-cover");l&&(l.src=t.album_art||"/beatify/static/img/no-artwork.svg");var c=document.getElementById("correct-year");c&&(c.textContent=t.year||"????");var u=document.getElementById("song-title"),m=document.getElementById("song-artist");u&&(u.textContent=t.title||"Unknown Song"),m&&(m.textContent=t.artist||"Unknown Artist");var d=document.getElementById("fun-fact-container"),f=document.getElementById("fun-fact"),v=d?d.querySelector(".fun-fact-header"):null,E=p.getLocalizedSongField(t,"fun_fact");if(f&&(f.textContent=E||""),v&&(v.style.display=E?"flex":"none"),pa(t),ga(e.song_difficulty),d){var w=document.getElementById("song-rich-info"),b=w&&w.innerHTML.trim()!=="",L=E&&E.trim()!=="";d.classList.toggle("hidden",!L&&!b)}for(var S=null,C=0;C'+p.t("analytics.noSubmissions")+"",n.classList.remove("hidden");return}var i="";if(e.average_guess!==null&&t){var r=Math.round(e.average_guess-t);r===0?i=p.t("analytics.onTarget"):r>0?i=p.t("analytics.yearsLate",{years:r}):i=p.t("analytics.yearsEarly",{years:Math.abs(r)})}var o=va(e.all_guesses,t),l="";if(e.exact_match_players&&e.exact_match_players.length>0&&(l+='🎯'+p.t("analytics.exactMatches")+':'+e.exact_match_players.map(x).join(", ")+"
"),e.speed_champion&&e.speed_champion.names){var c=e.speed_champion.names.map(x).join(", ");l+='⚡'+p.t("analytics.speedChampion")+':'+c+'('+e.speed_champion.time+"s)
"}if(e.furthest_players&&e.furthest_players.length>0&&e.all_guesses&&e.all_guesses.length>0){var u=e.all_guesses[e.all_guesses.length-1].years_off;u>0&&(l+='😅'+p.t("analytics.furthestGuess")+':'+e.furthest_players.map(x).join(", ")+'('+u+" years)
")}var m=e.average_guess!==null?Math.round(e.average_guess):"?";a.innerHTML=''+p.t("analytics.averageGuess")+''+m+'
'+e.accuracy_percentage+'%'+p.t("analytics.accuracy",{percent:""}).replace("%","")+'
'+i+'
'+p.t("analytics.histogram")+"
"+o+""+(l?''+l+"
":""),n.classList.remove("hidden")}function va(e,t){var n=7;if(!e||e.length===0)return''+p.t("analytics.noGuesses")+"
";for(var a=e.map(function(ae){return ae.guess}),i=Math.min.apply(null,a),r=Math.max.apply(null,a),o=r-i,l=Math.max(1,Math.ceil(o/n)),c=l*n,u=c-o-1,m=i-Math.floor(u/2),d=[],f=0;f=v&&t<=E})}for(var w=0;w=d[L].start&&b<=d[L].end){d[L].count++;break}for(var S=1,C=0;CS&&(S=d[C].count);for(var k="",B=0;B0?Math.max(R,10):0,F=A.count>0?''+A.count+"":"",ke=l===1?String(A.start):A.start+"-"+String(A.end).slice(-2);k+='"}return''+k+"
"}function ga(e){var t=document.getElementById("song-difficulty");if(t){if(!e){t.classList.add("hidden");return}for(var n="",a=0;a★';t.innerHTML=''+n+'
'+p.t("difficulty."+e.label)+''+e.accuracy+"% "+p.t("difficulty.accuracy")+"",t.classList.remove("hidden")}}function pa(e){var t=document.getElementById("song-rich-info");if(t){var n=[],a=ya(e.chart_info||{});a.length>0&&(n=n.concat(a));var i=ba(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=p.getLocalizedSongField(e,"awards")||[],o=La(r);o.length>0&&(n=n.concat(o)),n.length>0?t.innerHTML=''+n.join("")+"
":t.innerHTML=""}}function ya(e){if(!e)return[];var t=[];if(e.billboard_peak&&e.billboard_peak>0){var n=e.weeks_on_chart?' \xB7 '+e.weeks_on_chart+" "+p.t("reveal.weeksShort")+"":"";t.push('\u{1F4CA}#'+e.billboard_peak+" "+p.t("reveal.chartBillboard")+n+"")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.german_peak+" "+p.t("reveal.chartGerman")+""),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.uk_peak+" "+p.t("reveal.chartUK")+""),t}function ba(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+""+x(a)+"")}return t}function ha(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"song-badge--diamond":t.indexOf("platinum")!==-1?"song-badge--platinum":t.indexOf("gold")!==-1?"song-badge--gold":"song-badge--platinum"}function Ea(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"\u{1F48E}":t.indexOf("platinum")!==-1?"\u{1F4BF}":t.indexOf("gold")!==-1?"\u{1F947}":"\u{1F4BF}"}function La(e){if(!e||e.length===0)return[];for(var t=[],n=e.slice(0,3),a=0;a'+o+""+x(i)+"")}return e.length>3&&t.push('+'+(e.length-3)+" more"),t}function wa(e){var t=e.toLowerCase();return t.indexOf("grammy")!==-1?"song-badge--grammy":t.indexOf("eurovision")!==-1?"song-badge--eurovision":t.indexOf("oscar")!==-1||t.indexOf("academy award")!==-1?"song-badge--oscar":t.indexOf("hall of fame")!==-1?"song-badge--halloffame":"song-badge--award"}function Sa(e){var t=e.toLowerCase();return t.indexOf("eurovision")!==-1?"\u{1F3A4}":t.indexOf("grammy")!==-1?"\u{1F3C6}":t.indexOf("hall of fame")!==-1?"\u2B50":"\u{1F3C6}"}function Ia(e,t){var n=document.getElementById("reveal-emotion"),a=document.getElementById("personal-result");if(!n)return;var i=n.classList.contains("reveal-emotion-inline")||document.querySelector(".reveal-container--compact");n.className=i?"reveal-emotion-inline":"reveal-emotion",n.innerHTML="",n.classList.add("hidden"),a&&a.classList.remove("is-delayed"),O();var r=p.t("reveal.emotions");function o(v){return v[Math.floor(Math.random()*v.length)]}function l(v){return v===1?p.t("reveal.offByYear"):p.t("reveal.offByYears",{years:v})}var c="missed",u=o(r.missed),m=o(r.missedSub);if(e&&!e.missed_round){var d=e.years_off||0;d===0?(c="exact",u=o(r.exact),m=o(r.exactSub)):d<=2?(c="close",u=o(r.close),m=o(r.closeSub)+" "+l(d)):d<=5?(c="close",u=o(r.close),m=l(d)):(c="wrong",u=o(r.wrong),m=o(r.wrongSub)+" "+l(d))}else e&&e.missed_round&&(c="missed",u=o(r.missed),m=o(r.missedSub));var f=''+u+"";m&&(f+=''+m+"
"),n.innerHTML=f,n.classList.add("reveal-emotion--"+c),n.classList.remove("hidden"),c==="exact"&&ee(),a&&c!=="missed"&&a.classList.add("is-delayed")}function Ba(e,t){var n=document.getElementById("result-content");if(n){if(!e){n.innerHTML=''+p.t("reveal.playerNotFound")+"
";return}if(e.missed_round){var a='\u23F0
'+p.t("reveal.noSubmission")+"
",i=e.previous_streak||0;i>=2&&(a+='\u{1F494}Lost '+i+"-streak!
"),a+='0 pts
',n.innerHTML=a;return}var r=e.years_off||0,o=r===0?p.t("reveal.exact"):r===1?p.t("reveal.yearOff",{years:1}):p.t("reveal.yearsOff",{years:r}),l=r===0?"is-exact":r<=3?"is-close":"is-far",c=e.speed_multiplier||1,u=e.base_score||0,m=c>1,d=e.streak_bonus||0,f=e.artist_bonus||0,v="";m&&u>0&&(v=''+p.t("reveal.baseScore")+''+u+' pts
'+p.t("reveal.speedBonus")+''+c.toFixed(2)+"x
");var E="";e.bet_outcome==="won"?E='\u{1F3B2} '+p.t("reveal.betWon").replace("! 2x points","")+'2x
':e.bet_outcome==="lost"&&(E='\u{1F3B2} '+p.t("reveal.betLost")+'-
');var w="";d>0&&(w=''+e.streak+'-streak bonus!+'+d+" pts
");var b="";f>0&&(b='\u{1F3A4} '+(p.t("artistChallenge.artistBonus")||"Artist Bonus")+'+'+f+" pts
");var L=e.round_score+d+f,S=d>0||f>0,C=e.round_score>=20,k=M.players[e.name],B=k?k.score:e.score-L,A=k?k.streak:0,R=dt(A,e.streak||0);n.innerHTML=''+p.t("reveal.yourGuess")+''+(e.guess||"n/a")+'
'+p.t("reveal.correctYear")+''+t+'
'+p.t("reveal.accuracy")+''+o+"
"+v+E+'+0 pts
'+w+b+(S?''+p.t("reveal.total")+': +0 pts
':"");var D=n.querySelector(".score-value");D&&(ot(D,0,e.round_score,{betWon:e.bet_outcome==="won",betLost:e.bet_outcome==="lost",streakMilestone:R,isBigScore:C}),e.bet_outcome==="won"&&e.round_score>0&&setTimeout(function(){var T=document.getElementById("personal-result-score");T&&Ne(T,e.round_score,{isBetWin:!0})},200));var V=n.querySelector(".total-value");V&&S&&(setTimeout(function(){se(V,0,L,600)},300),R&&setTimeout(function(){var T=n.querySelector(".result-total");if(T){var F={3:20,5:50,10:100}[R]||0;Ne(T,F,{isStreak:!0,text:"+"+F+" "+R+"-Streak!"})}},500))}}function xa(e){var t=document.getElementById("reveal-results-cards");if(t){if(!e||e.length===0){t.innerHTML="";return}var n=e.slice().sort(function(i,r){return(r.round_score||0)-(i.round_score||0)}),a='",t.innerHTML=a}}var _=window.BeatifyUtils||{};function ln(e){window.scrollTo(0,0);var t=e.leaderboard||[];t.forEach(function(b){b.is_current=b.name===s.playerName}),[1,2,3].forEach(function(b){var L=t.find(function(k){return k.rank===b}),S=document.getElementById("podium-"+b+"-name"),C=document.getElementById("podium-"+b+"-score");S&&(S.textContent=L?x(L.name):"---"),C&&(C.textContent=L?L.score:"0")});var n=t.find(function(b){return b.is_current}),a=document.getElementById("your-final-rank"),i=document.getElementById("your-final-score"),r=document.getElementById("stat-best-streak"),o=document.getElementById("stat-rounds"),l=document.getElementById("stat-bets");n&&(a&&(a.textContent="#"+n.rank),i&&(i.textContent=n.score+" "+_.t("leaderboard.points")),r&&(r.textContent=n.best_streak||0),o&&(o.textContent=n.rounds_played||0),l&&(l.textContent=n.bets_won||0));var c=document.getElementById("final-leaderboard-list");c&&(c.innerHTML=t.map(function(b){var L=b.is_current?"is-current":"",S=b.connected===!1?"final-entry--disconnected":"",C=b.connected===!1?'(away)':"";return'#'+b.rank+''+x(b.name)+C+''+b.score+"
"}).join("")),Ca(e.superlatives),_a(e.highlights),ka(e.share_data);var u=document.getElementById("end-admin-controls"),m=document.getElementById("end-player-message");if(n&&n.is_admin){u&&u.classList.remove("hidden"),m&&m.classList.add("hidden");var d=document.getElementById("new-game-btn");d&&(d.onclick=Ta);var f=document.getElementById("player-rematch-btn");f&&(f.onclick=function(){f.disabled=!0;var b=f.textContent;f.textContent="\u23F3",fetch("/beatify/api/rematch-game",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"}}).then(function(L){if(!L.ok)return L.json().then(function(S){throw new Error(S.message||"Rematch failed")});f.textContent="\u23F3"}).catch(function(L){console.error("[Player] Rematch failed:",L),alert(L.message||"Failed to start rematch"),f.disabled=!1,f.textContent=b})})}else u&&u.classList.add("hidden"),m&&m.classList.remove("hidden");if(n){var v=e.total_rounds||10,E=n.best_streak||0,w=E===v&&v>0;w?ee("perfect"):n.rank===1&&ee("winner")}}function Ca(e){var t=document.getElementById("superlatives-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n="";e.forEach(function(a,i){var r="";switch(a.value_label){case"avg_time":r=a.value+"s "+_.t("superlatives.avgTime");break;case"streak":r=a.value+" "+_.t("superlatives.streak");break;case"bets":r=a.value+" "+_.t("superlatives.bets");break;case"points":r=a.value+" "+_.t("superlatives.points");break;case"close_guesses":r=a.value+" "+_.t("superlatives.closeGuesses");break;default:r=a.value}n+=''+a.emoji+'
'+_.t("superlatives."+a.title)+'
'+x(a.player_name)+'
'+r+"
"}),t.innerHTML=n,t.classList.remove("hidden")}}function _a(e){var t=document.getElementById("highlights-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n=document.getElementById("highlights-list");if(n){var a="";e.forEach(function(i,r){var o=_.t("highlights."+i.description,i.description_params)||i.description;o===i.description&&i.description_params&&(o=_.t("highlights."+i.description)||i.description,Object.keys(i.description_params).forEach(function(l){o=o.replace("{"+l+"}",x(i.description_params[l]))})),a+=''+(i.emoji||"\u2728")+'
'+o+'
'+_.t("highlights.roundLabel",{round:i.round})+"
"}),n.innerHTML=a,t.classList.remove("hidden")}}}function ka(e){var t=document.getElementById("share-container");if(t){if(!e||!e.emoji_grids){t.classList.add("hidden");return}var n=e.emoji_grids[s.playerName];if(!n){var a=Object.keys(e.emoji_grids);a.length===1&&(n=e.emoji_grids[a[0]])}if(!n){t.classList.add("hidden");return}var i=document.getElementById("share-emoji-grid");if(i){var r=n.split(`
-`).map(function(c){return''+_.escapeHtml(c)+"
"}).join("");i.innerHTML=r,i.dataset.rawText=n}var o=document.getElementById("share-copy-btn");o&&(o.onclick=function(){navigator.clipboard.writeText(n).then(function(){var c=document.getElementById("share-toast");c&&(c.classList.remove("hidden"),setTimeout(function(){c.classList.add("hidden")},2e3))})});var l=document.getElementById("share-save-btn");l&&(l.onclick=function(){Aa(n,e.playlist_name,e)}),t.classList.remove("hidden")}}function Aa(e,t,n){var a=document.createElement("canvas");a.width=800,a.height=800;var i=a.getContext("2d"),r=i.createLinearGradient(0,0,0,800);r.addColorStop(0,"#0f0c29"),r.addColorStop(.5,"#302b63"),r.addColorStop(1,"#24243e"),i.fillStyle=r,i.fillRect(0,0,800,800);var o=i.createLinearGradient(0,0,800,0);o.addColorStop(0,"#e94560"),o.addColorStop(1,"#0f3460"),i.fillStyle=o,i.fillRect(0,0,800,4);var l=new Image;l.src="/beatify/static/img/icon-256.png",l.onerror=function(){c(null)},l.onload=function(){c(l)};function c(u){u?(i.drawImage(u,28,20,64,64),i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="left",i.fillText("Beatify",104,60)):(i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="center",i.fillText("\u{1F3B5} Beatify",400,55)),i.textAlign="center";var m=(t||"").toUpperCase();i.font="bold 13px system-ui, sans-serif";var d=i.measureText(m).width+32,f=400-d/2;i.fillStyle="rgba(233,69,96,0.18)",i.beginPath(),i.roundRect(f,96,d,30,15),i.fill(),i.strokeStyle="#e94560",i.lineWidth=1,i.stroke(),i.fillStyle="#e94560",i.fillText(m,400,116);for(var v=e.split(`
-`).filter(function(N){return N.trim()!==""}),E="",w=[],b="",L="",S="",C="",k=0;k=ge&&(s.isReconnecting=!1,pe(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}function Q(e){s.playerName=e,fn(e);var t=window.location.protocol==="https:"?"wss:":"ws:",n=t+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(n),s.ws.onopen=function(){s.reconnectAttempts=0,s.isReconnecting=!1,pe(),gn();var a={type:"join",name:e};s.isAdmin&&(a.is_admin=!0),s.ws.send(JSON.stringify(a))},s.ws.onmessage=function(a){try{var i=JSON.parse(a.data);bn(i)}catch(r){console.error("Failed to parse WebSocket message:",r)}},s.ws.onclose=function(){if(s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=ge&&(s.isReconnecting=!1,pe(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}s.connectWithSession=_e;s.connectWebSocket=Q;function bn(e){var t=document.getElementById("join-btn"),n=document.getElementById("name-input");if(e.type==="state"){var a=e.players||[],i=a.find(function(u){return u.name===s.playerName});if(i&&(s.isAdmin=i.is_admin===!0),e.language&&(Ha(e.language),typeof BeatifyI18n<"u"&&e.language!==BeatifyI18n.getLanguage()&&BeatifyI18n.setLanguage(e.language).then(function(){BeatifyI18n.initPageTranslations(),Pe(a),e.difficulty&&he(e.difficulty),e.phase==="REVEAL"&&Xe(e),(e.phase==="PLAYING"||e.phase==="REVEAL")&&Se(e.phase)})),e.phase==="LOBBY"){Y(),ue(),me(),s.currentRoundNumber=0,q("warmup");var r=document.getElementById("start-game-btn");r&&(r.disabled=!1,r.innerHTML='\u{1F389}'+ve.t("lobby.startGame")+""),I("lobby-view"),Pe(a),e.difficulty&&he(e.difficulty),e.join_url&&It(e.join_url),_t(a)}else if(e.phase==="PLAYING"){var o=e.round||1;o!==s.currentRoundNumber&&(s.currentRoundNumber=o,jt()),q("party"),I("game-view"),oe(),Ot(e),e.intro_splash_pending?rn(s.isAdmin):sn(),e.difficulty&&he(e.difficulty),e.deadline&&Rt(e.deadline),Vt(),Ht(),Je(),Se("PLAYING"),me()}else e.phase==="REVEAL"?(Y(),e.early_reveal&&Tt(),q("party"),I("reveal-view"),Xe(e),Gt(),Dt(),Je(),Se("REVEAL"),s.hasReactedThisPhase=!1,Jt()):e.phase==="PAUSED"?(Y(),ue(),me(),q("warmup"),I("paused-view"),dn(e)):e.phase==="END"&&(Y(),ue(),me(),s.currentRoundNumber=0,q("warmup"),I("end-view"),ln(e),fe())}else if(e.type==="join_ack"){e.session_id&&Oa(e.session_id);try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}}else if(e.type==="reconnect_ack")e.success&&e.name?(s.playerName=e.name,fn(e.name),At(e.name)):(Ze(),fe(),s.playerName=null,I("join-view"));else if(e.type==="submit_ack")ze();else if(e.type==="metadata_update")Pt(e.song);else if(e.type==="error"){if(e.code==="ROUND_EXPIRED"||e.code==="ALREADY_SUBMITTED"){Ft(e);return}if(e.code==="GAME_ENDED"){I("end-view");return}if(e.code==="NOT_ADMIN"){s.isAdmin=!1,ue(),console.warn("Admin action rejected: not admin");return}if(e.code==="SESSION_TAKEOVER"){s.isReconnecting=!1,pe(),s.playerName=null,et(),console.warn("Session taken over by another tab");return}if(e.code==="SESSION_NOT_FOUND"){Ze(),s.intentionalLeave=!0,s.ws&&s.ws.close(),I("join-view");return}if(e.code==="ADMIN_CANNOT_LEAVE"){s.intentionalLeave=!1,alert(e.message||"Host cannot leave. End the game instead.");return}if(e.code==="INVALID_ACTION"&&e.message==="No song playing"){Ke(),console.warn("[Beatify] Stop song failed: No song playing");return}I("join-view"),Wa(e.message),t&&(t.disabled=!1,t.textContent=ve.t("join.joinButton")),n&&n.focus(),s.playerName=null,fe()}else if(e.type==="song_stopped")tn();else if(e.type==="volume_changed")nn(e.level);else if(e.type==="game_ended")ja();else if(e.type==="rematch_started"){console.log("[Player] Rematch started - transitioning to lobby"),$.clear(),O(),I("lobby-view");var l=document.getElementById("player-rematch-btn");l&&(l.disabled=!1,l.textContent="\u{1F501}");var c=$e();c&&(s.ws&&s.ws.readyState===WebSocket.OPEN?(s.reconnectAttempts=0,s.ws.send(JSON.stringify({type:"reconnect",session_id:c}))):(s.reconnectAttempts=0,_e()))}else e.type==="left"?Fa():e.type==="steal_targets"?Qt(e):e.type==="steal_ack"?Yt(e):e.type==="artist_guess_ack"?Wt(e):e.type==="movie_guess_ack"?Ut(e):e.type==="player_reaction"&&Xt(e.player_name,e.emoji)}function Fa(){fe(),Ze(),s.playerName=null,s.isAdmin=!1,I("join-view")}function ja(){var e=s.isAdmin;fe();try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}ft(),Et(),$.clear(),O(),s.playerName=null,s.isAdmin=!1,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.close(),s.ws=null;var t=document.getElementById("end-view");if(!(!t||!t.classList.contains("hidden"))){var n=document.getElementById("end-player-message");n&&(n.innerHTML='Thanks for playing!
Scan the QR code again to join the next game.
',n.classList.remove("hidden")),I("end-view")}}function Wa(e){var t=document.getElementById("name-validation-msg");t&&(t.textContent=e,t.classList.remove("hidden"))}function tt(e){var t=(e||"").trim();return t?t.length>Ma?{valid:!1,error:"Name too long (max 20 characters)"}:{valid:!0,name:t}:{valid:!1,error:"Please enter a name"}}function cn(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");if(!(!e||!t)){var a=tt(e.value);a.valid&&(t.disabled=!0,t.textContent=ve.t("game.joining"),n&&n.classList.add("hidden"),Q(a.name))}}function qa(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");!e||!t||(e.addEventListener("input",function(){var a=tt(this.value);t.disabled=!a.valid,n&&(n.textContent=!a.valid&&this.value?a.error:"",n.classList.toggle("hidden",a.valid||!this.value))}),t.addEventListener("click",cn),e.addEventListener("keypress",function(a){a.key==="Enter"&&!t.disabled&&cn()}))}function Ua(){var e=document.getElementById("retry-connection-btn");e&&e.addEventListener("click",function(){s.playerName?(s.reconnectAttempts=0,I("loading-view"),Q(s.playerName)):Ce()})}async function un(){var e=K.getDeviceTier();document.body.classList.add("device-tier-"+e);var t=await ve.waitForI18n();if(!t)console.error("[Player] BeatifyI18n module failed to load - UI will use fallback text");else{var n=Ga();await BeatifyI18n.init(n),BeatifyI18n.initPageTranslations()}var a=document.getElementById("dashboard-hint-url");a&&(a.textContent=window.location.origin+"/beatify/dashboard");var i=document.getElementById("player-dashboard-url");if(i&&(i.href=window.location.origin+"/beatify/dashboard"),qa(),Bt(),Ct(),kt(),an(),en(),Ua(),vt(),gt(),pt(),Kt(),Va()&&s.playerName){Q(s.playerName);return}var r=Pa();if(r&&s.gameId){console.log("[Beatify] Auto-reconnecting as:",r),Q(r);return}if(r){var o=document.getElementById("name-input"),l=document.getElementById("join-btn");if(o&&(o.value=r,l)){var c=tt(r);l.disabled=!c.valid}}}Ce();document.getElementById("refresh-btn")?.addEventListener("click",function(){I("loading-view"),Ce()});document.getElementById("retry-btn")?.addEventListener("click",function(){I("loading-view"),Ce()});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",un):un();"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/static/sw.js",{scope:"/beatify/"}).then(function(e){console.log("[Beatify] SW registered:",e.scope)}).catch(function(e){console.warn("[Beatify] SW registration failed:",e)})});document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible"){var e=s.ws;(!e||e.readyState===WebSocket.CLOSING||e.readyState===WebSocket.CLOSED)&&s.playerName&&(console.log("[Beatify] Page visible, WebSocket dead \u2014 reconnecting immediately."),s.reconnectAttempts=0,_e())}});
+var Ae=window.BeatifyUtils||{},s={ws:null,playerName:null,isAdmin:!1,reconnectAttempts:0,isReconnecting:!1,intentionalLeave:!1,hasReactedThisPhase:!1,currentRoundNumber:0,gameId:new URLSearchParams(window.location.search).get("game"),connectWithSession:null,connectWebSocket:null},wn=document.getElementById("loading-view"),Ln=document.getElementById("not-found-view"),Sn=document.getElementById("ended-view"),In=document.getElementById("in-progress-view"),Bn=document.getElementById("join-view"),xn=document.getElementById("lobby-view"),Cn=document.getElementById("game-view"),_n=document.getElementById("reveal-view"),kn=document.getElementById("paused-view"),An=document.getElementById("end-view"),Tn=document.getElementById("connection-lost-view"),Nn=[wn,Ln,Sn,In,Bn,xn,Cn,_n,kn,An,Tn];function B(e){Ae.showView(Nn,e),(e==="join-view"||e==="loading-view"||e==="not-found-view"||e==="ended-view"||e==="in-progress-view"||e==="connection-lost-view")&&q("calm"),e==="join-view"&&setTimeout(function(){var t=document.getElementById("name-input");t&&t.focus()},100)}function Z(e,t,n,a){return new Promise(function(i){var r=document.getElementById("confirm-modal"),o=document.getElementById("confirm-modal-title"),l=document.getElementById("confirm-modal-message"),d=document.getElementById("confirm-modal-yes"),u=document.getElementById("confirm-modal-no");if(!r||!o||!l||!d||!u){i(confirm(t||e));return}o.textContent=e,l.textContent=t,d.textContent=n||Ae.t("common.confirm")||"Confirm",u.textContent=a||Ae.t("common.cancel")||"Cancel",r.classList.remove("hidden");function m(){r.classList.add("hidden"),d.removeEventListener("click",c),u.removeEventListener("click",f),v.removeEventListener("click",f)}function c(){m(),i(!0)}function f(){m(),i(!1)}var v=r.querySelector(".modal-backdrop");d.addEventListener("click",c),u.addEventListener("click",f),v&&v.addEventListener("click",f)})}function x(e){var t=document.createElement("div");return t.textContent=e,t.innerHTML}function be(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function Mn(e){return 1-Math.pow(1-e,4)}function se(e,t,n,a,i){if(be()||t===n)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var r=K.getQualitySettings();if(r.scoreDuration===0)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var o=Math.min(a,r.scoreDuration||a);i=i||Mn;var l=null,d=null,u=!1,m=n;function c(f){if(!u){l||(l=f);var v=f-l,w=Math.min(v/o,1),L=i(w),b=Math.round(t+(m-t)*L);e.textContent=b,w<1&&(d=requestAnimationFrame(c))}}return d=requestAnimationFrame(c),{cancel:function(){u=!0,d&&cancelAnimationFrame(d)},skipToEnd:function(){u=!0,d&&cancelAnimationFrame(d),e.textContent=m}}}function ot(e,t,n,a){a=a||{};var i=500;a.betWon?i=800:a.isBigScore?i=700:a.betLost&&(i=400),e.classList.add("score-animating");var r=null;a.betWon?r="score-glow-gold":a.betLost?(r="score-shake",e.classList.add("score-flash-red")):a.streakMilestone?r="score-burst":a.isBigScore&&(r="score-pop"),r&&!be()&&e.classList.add(r),se(e,t,n,i);function o(){e.classList.remove("score-animating"),r&&e.classList.remove(r),e.classList.remove("score-flash-red")}r&&!be()?e.addEventListener("animationend",function l(){e.removeEventListener("animationend",l),o()}):setTimeout(o,i+50)}function Ne(e,t,n){if(n=n||{},!be()){var a=document.createElement("div");a.className="points-popup",a.textContent=n.text||"+"+t,n.isStreak?a.classList.add("points-popup--streak"):n.isBetWin&&a.classList.add("points-popup--gold");var i=e.getBoundingClientRect();a.style.left=i.left+i.width/2+"px",a.style.top=i.top+"px",document.body.appendChild(a),a.addEventListener("animationend",function(){a.parentNode&&a.parentNode.removeChild(a)}),setTimeout(function(){a.parentNode&&a.parentNode.removeChild(a)},1200)}}var M={players:{},leaderboard:[],initialized:!1};function lt(){return M.initialized}var rt=[3,5,10,15,20,25];function dt(e,t){for(var n=0;n=a)return a}return null}function ct(e){var t=e.map(function(a){return a.name}),n={};return t.forEach(function(a,i){var r=M.leaderboard.indexOf(a);r===-1?n[a]="new":ir&&(n[a]="down")}),n}function ut(e,t){M.players={},e.forEach(function(n){M.players[n.name]={score:n.score,rank:n.rank||0,streak:n.streak||0}}),t&&(M.leaderboard=t.map(function(n){return n.name})),M.initialized=!0}var K=(function(){var e=window.matchMedia("(prefers-reduced-motion: reduce)"),t=e.matches;e.addEventListener("change",function(i){t=i.matches});var n=null;function a(){if(n!==null)return n;var i=navigator.hardwareConcurrency||2,r=navigator.deviceMemory||4,o=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;return i<=2||r<=2?n="low":i<=4||r<=4||o?n="medium":n="high",n}return a(),{prefersReducedMotion:function(){return t},getDeviceTier:a,getQualitySettings:function(){var i=a();if(t)return{confettiParticles:0,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!1};switch(i){case"low":return{confettiParticles:5,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!0};case"medium":return{confettiParticles:10,scoreDuration:300,leaderboardAnimation:"simplified",neonGlow:!1,enableAnimations:!0};default:return{confettiParticles:15,scoreDuration:500,leaderboardAnimation:"full",neonGlow:!0,enableAnimations:!0}}},ifMotionAllowed:function(i,r){t?r&&r():i()},withWillChange:function(i,r,o){i&&(i.style.willChange=r,setTimeout(function(){i&&i.style&&(i.style.willChange="auto")},(o||500)+100))}}})(),$=(function(){var e=[],t=!1,n=null,a=null,i=2e3;function r(){if(a&&(clearTimeout(a),a=null),e.length===0){t=!1,n=null;return}n=e.shift(),a=setTimeout(function(){n&&n.skipToEnd&&n.skipToEnd(),r()},i),n.run(function(){a&&(clearTimeout(a),a=null),r()})}return{add:function(o){e.push(o),t||(t=!0,r())},skipAll:function(){a&&(clearTimeout(a),a=null),n&&n.skipToEnd&&n.skipToEnd(),e.forEach(function(o){o.skipToEnd&&o.skipToEnd()}),e=[],t=!1,n=null},clear:function(){a&&(clearTimeout(a),a=null),e=[],t=!1,n=null},isRunning:function(){return t},getMaxDuration:function(){return i}}})(),X={VISIBLE_BUFFER:2,ENTRY_HEIGHT:48,MIN_PLAYERS_FOR_LAZY:10,ROOT_MARGIN:"96px 0px",DEFAULT_VIEWPORT_HEIGHT:280},h={observer:null,fullData:[],visibleRange:{start:0,end:10},listEl:null,isLazyEnabled:!1};function mt(e){e&&(h.observer&&h.listEl!==e&&(h.observer.disconnect(),h.observer=null),!h.observer&&(h.listEl=e,h.observer=new IntersectionObserver(function(t){t.forEach(function(n){if(!(!n.isIntersecting||!h.isLazyEnabled)){var a=h.fullData,i=h.visibleRange,r=X.VISIBLE_BUFFER;if(n.target.classList.contains("leaderboard-sentinel--top")){if(i.start>0){var o=Math.max(0,i.start-r);h.visibleRange.start=o,re()}}else if(n.target.classList.contains("leaderboard-sentinel--bottom")&&i.end0&&(l+=''),l+='';for(var d=n.start;d',r>0&&(l+=''),e.innerHTML=l,e.scrollTop=o,h.observer){var u=e.querySelectorAll(".leaderboard-sentinel");u.forEach(function(m){h.observer.observe(m)})}}}function Me(e){if(!e)return"";if(e.separator)return'...
';var t=e.name||"Unknown",n=e.rank||0,a=e.score||0,i=n<=3?"is-top-"+n:"",r=e.is_current?"is-current":"",o="";e.rank_change>0||e._rankChange==="up"?o="leaderboard-entry--climbing leaderboard-entry--slide-up":(e.rank_change<0||e._rankChange==="down")&&(o="leaderboard-entry--falling leaderboard-entry--slide-down");var l="";e.rank_change>0?l='\u25B2'+e.rank_change+"":e.rank_change<0&&(l='\u25BC'+Math.abs(e.rank_change)+"");var d="";if(e.streak>=2){var u=e.streak>=5?"streak-indicator--hot":"";d='\u{1F525}'+e.streak+""}var m=e.connected===!1?"leaderboard-entry--disconnected":"",c=e.connected===!1?'(away)':"",f=e._displayScore!==void 0?e._displayScore:a;return'#'+n+''+x(t)+c+''+d+l+''+f+"
"}function Re(e,t){for(var n=X,a=h.listEl&&h.listEl.clientHeight||n.DEFAULT_VIEWPORT_HEIGHT,i=Math.ceil(a/n.ENTRY_HEIGHT),r=n.VISIBLE_BUFFER,o=-1,l=0;l=e.length-i?(d=Math.max(0,e.length-i-r),u=e.length):(d=Math.max(0,o-Math.floor(i/2)-r),u=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:d,end:u}}function ft(){h.observer&&(h.observer.disconnect(),h.observer=null),h.isLazyEnabled=!1,h.fullData=[]}function vt(){var e;function t(){clearTimeout(e),e=setTimeout(function(){h.isLazyEnabled&&h.fullData.length>0&&(h.visibleRange=Re(h.fullData,s.playerName),re())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function gt(){var e=document.getElementById("qr-share-area");if(!(!e||e.tagName!=="DETAILS")){var t="beatify_qr_expanded",n=768,a=sessionStorage.getItem(t);a!==null?e.open=a==="true":e.open=window.innerWidth>=n,e.addEventListener("toggle",function(){sessionStorage.setItem(t,e.open.toString())})}}function pt(){var e=document.querySelectorAll(".lobby-container--compact .section-header-collapsible");e.forEach(function(t){t.addEventListener("click",function(){var n=t.closest(".section-collapsible");if(n){var a=n.classList.contains("collapsed");n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",a?"true":"false")}})})}var yt={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},g={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function bt(e){if(e){g.container=e;var t=!1;g.scrollHandler=function(){g.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){Te(),t=!1}),t=!0)};var n;g.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){g.isVirtual&&Te()},100)},e.addEventListener("scroll",g.scrollHandler,{passive:!0}),window.addEventListener("resize",g.resizeHandler)}}function ht(e,t){g.items=e,g.renderItem=t;var n=g.container;if(n){var a=n.scrollTop,i=g.isVirtual;e.length0&&(n.scrollTop=a,g.scrollTop=a)}}function Rn(){var e=g.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",g.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",g.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",g.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function Te(){var e=yt,t=g.items,n=g.container,a=g.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=g.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,d=Math.max(0,Math.floor(r/o)-l),u=Math.min(t.length,Math.ceil((r+i)/o)+l);g.topSpacer&&(g.topSpacer.style.height=d*o+"px"),g.bottomSpacer&&(g.bottomSpacer.style.height=(t.length-u)*o+"px");for(var m="",c=d;c"u"){console.warn("[Confetti] Library not loaded");return}O();var t=K.getQualitySettings(),n=t.confettiParticles;if(n===0){st();return}var a=K.getDeviceTier(),i=a==="low"?.5:a==="medium"?.75:1;switch(e=e||"exact",e){case"exact":var r=Math.round(2e3*i),o=Date.now()+r;(function v(){confetti({particleCount:n,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()0);var l=e.slice().sort(function(c,f){return c.connected!==f.connected?c.connected?-1:1:0}),d=wt.map(function(c){return c.name}),u=l.filter(function(c){return d.indexOf(c.name)===-1}).map(function(c){return c.name});g.container||bt(t);var m=function(c){var f=u.indexOf(c.name)!==-1,v=c.name===s.playerName,w=c.connected===!1,L=["player-card",f?"is-new":"",v?"player-card--you":"",w?"player-card--disconnected":""].filter(Boolean).join(" "),b=w?'(away)':"";return''+x(c.name)+(v?''+U.t("leaderboard.you")+"":"")+b+"
"};ht(l,m),setTimeout(function(){var c=g.isVirtual?g.contentWrapper:t;if(c)for(var f=c.querySelectorAll(".is-new"),v=0;vQR code library not loaded',t.onclick=Lt,t.onkeydown=function(n){(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),Lt())})}}function Lt(){if(P){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:P,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function Oe(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function Bt(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",Oe),n&&n.addEventListener("click",Oe),document.addEventListener("keydown",function(a){a.key==="Escape"&&e&&!e.classList.contains("hidden")&&Oe()})}function Pn(){if(P){var e=document.getElementById("invite-modal"),t=document.getElementById("invite-modal-code"),n=document.getElementById("invite-modal-url");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:P,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',n&&(n.value=P),e.classList.remove("hidden"),document.body.style.overflow="hidden";var a=document.getElementById("invite-modal-close");a&&a.focus()}}}function oe(){var e=document.getElementById("invite-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="");var t=document.getElementById("invite-copy-feedback");t&&t.classList.add("hidden")}function Hn(){var e=document.getElementById("invite-modal-url"),t=document.getElementById("invite-copy-feedback");!e||!P||(navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(P).then(function(){xt(t)}).catch(function(){St(e,t)}):St(e,t))}function St(e,t){e.select(),e.setSelectionRange(0,99999);try{document.execCommand("copy"),xt(t)}catch(n){console.warn("[Beatify] Copy failed:",n)}}function xt(e){e&&(e.classList.remove("hidden"),setTimeout(function(){e.classList.add("hidden")},2e3))}function Ct(){var e=document.getElementById("invite-modal"),t=e?e.querySelector(".invite-modal-backdrop"):null,n=document.getElementById("invite-modal-close"),a=document.getElementById("invite-players-btn"),i=document.getElementById("invite-copy-btn");t&&t.addEventListener("click",oe),n&&n.addEventListener("click",oe),a&&a.addEventListener("click",Pn),i&&i.addEventListener("click",Hn),document.addEventListener("keydown",function(r){r.key==="Escape"&&e&&!e.classList.contains("hidden")&&oe()})}function _t(e){var t=document.getElementById("admin-controls"),n=document.getElementById("lobby-status");if(t){(!e||!Array.isArray(e))&&(e=[]);var a=e.find(function(r){return r.name===s.playerName}),i=a?.is_admin===!0;i?(t.classList.remove("hidden"),n&&n.classList.add("hidden")):(t.classList.add("hidden"),n&&n.classList.remove("hidden"))}}function kt(){var e=document.getElementById("start-game-btn");e?.addEventListener("click",function(){!s.ws||s.ws.readyState!==WebSocket.OPEN||(e.disabled=!0,e.textContent=U.t("game.starting"),s.ws.send(JSON.stringify({type:"admin",action:"start_game"})))})}function At(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=U.t("player.welcomeBack",{name:e}),t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},2e3))}function Tt(){var e=document.getElementById("volume-indicator");e&&(e.textContent=U.t("earlyReveal.message")||"All guesses in!",e.classList.remove("hidden"),e.classList.add("is-visible"),setTimeout(function(){e.classList.remove("is-visible"),setTimeout(function(){e.classList.add("hidden")},300)},1500))}var y=window.BeatifyUtils||{},Ee=null;function Rt(e){Y();var t=document.getElementById("timer");if(!t)return;t.classList.remove("timer--warning","timer--critical");function n(){var a=Date.now(),i=Math.max(0,Math.ceil((e-a)/1e3));t.textContent=i,i<=5?(t.classList.remove("timer--warning"),t.classList.add("timer--critical")):i<=10?(t.classList.remove("timer--critical"),t.classList.add("timer--warning")):t.classList.remove("timer--warning","timer--critical"),i===10?t.setAttribute("aria-label","10 seconds remaining"):i===5?t.setAttribute("aria-label","5 seconds!"):i===0?t.setAttribute("aria-label","Time is up!"):t.setAttribute("aria-label","Time remaining: "+i+" seconds"),i<=0&&Y()}n(),Ee=setInterval(n,1e3)}function Y(){Ee&&(clearInterval(Ee),Ee=null)}function Ot(e){var t=document.getElementById("current-round"),n=document.getElementById("total-rounds"),a=document.getElementById("last-round-banner");t&&(t.textContent=e.round||1),n&&(n.textContent=e.total_rounds||10),a&&(e.last_round?a.classList.remove("hidden"):a.classList.add("hidden"));var i=document.getElementById("closest-wins-badge");i&&(e.closest_wins_mode?i.classList.remove("hidden"):i.classList.add("hidden"));var r=document.getElementById("intro-badge"),o=document.getElementById("intro-splash");if(r)if(e.is_intro_round){r.classList.remove("hidden");var l=r.querySelector("[data-i18n]");e.intro_stopped?(r.classList.add("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=y.t("game.introStopped")||"Intro complete!")):(r.classList.remove("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introRound"),l.textContent=y.t("game.introRound")||"INTRO ROUND"),o&&!o._shown&&(o._shown=!0,o.classList.remove("hidden"),setTimeout(function(){o.classList.add("hidden")},2e3)))}else r.classList.add("hidden"),r.classList.remove("intro-badge--stopped"),o&&(o.classList.add("hidden"),o._shown=!1);var d=document.getElementById("album-cover"),u=document.getElementById("album-loading");if(d&&e.song){u&&u.classList.remove("hidden");var m=e.song.album_art||"/beatify/static/img/no-artwork.svg";d.onload=function(){u&&u.classList.add("hidden")},d.onerror=function(){d.src="/beatify/static/img/no-artwork.svg",u&&u.classList.add("hidden")},d.src=m}Dn(e.players),e.leaderboard&&Ue(e,"leaderboard-list"),$n(e.players),e.artist_challenge!==void 0&&Yn(e.artist_challenge,"PLAYING"),e.movie_challenge!==void 0&&Kn(e.movie_challenge,"PLAYING")}function Pt(e){if(e){var t=document.getElementById("album-cover"),n=document.getElementById("album-loading");if(t&&e.album_art){var a=e.album_art;if(t.src===a)return;t.style.transition="opacity 0.3s ease-in-out",t.style.opacity="0.5";var i=new Image;i.onload=function(){t.src=a,t.style.opacity="1",n&&n.classList.add("hidden")},i.onerror=function(){t.src="/beatify/static/img/no-artwork.svg",t.style.opacity="1",n&&n.classList.add("hidden")},i.src=a}console.log("[Metadata] Updated:",e.artist,"-",e.title)}}function Gn(e){if(!e)return"?";var t=e.trim();if(!t)return"?";var n=t.split(/[\s-]+/).filter(Boolean);return n.length>=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,Math.min(2,t.length)).toUpperCase()}function Dn(e){var t=document.getElementById("submission-tracker"),n=document.getElementById("submitted-players");if(!(!t||!n)){var a=e||[],i=a.filter(function(l){return l.submitted}).length,r=a.length,o=i===r&&r>0;t.classList.toggle("all-submitted",o),n.innerHTML=a.map(function(l){var d=Gn(l.name),u=l.name===s.playerName,m=l.connected===!1,c=["player-indicator",l.submitted?"is-submitted":"",u?"is-current-player":"",m?"player-indicator--disconnected":""].filter(Boolean).join(" "),f="";return l.steal_used&&(f+='\u{1F977}'),l.bet&&(f+='\u{1F3B2}'),''+f+'
'+x(d)+'
'+x(l.name)+" "}).join("")}}function Ue(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&<(),o=r?ct(a):{};a.forEach(function(c){c.is_current=c.name===s.playerName;var f=o[c.name];f&&(c._rankChange=f);var v=M.players[c.name],w=v?v.score:c.score;c._prevScore=w,c._displayScore=n?w:c.score});var l=Vn(a,s.playerName),d=a.length>=X.MIN_PLAYERS_FOR_LAZY;if(d)h.observer||mt(i),h.fullData=l,h.isLazyEnabled=!0,h.listEl=i,h.visibleRange=Re(l,s.playerName),re();else{h.isLazyEnabled=!1;var u="";l.forEach(function(c){u+=Me(c)}),i.innerHTML=u}var m=[];r&&l.forEach(function(c){!c.separator&&c._prevScore!==c.score&&m.push({name:c.name,prevScore:c._prevScore,newScore:c.score})}),r&&m.length>0&&requestAnimationFrame(function(){for(var c={},f=i.querySelectorAll(".leaderboard-entry[data-name]"),v=0;v8&&Fn(i),Wn(a),jn(a),ut(e.players||[],a)}}function Vn(e,t){if(e.length<=10)return e;for(var n=e.slice(0,5),a=e.slice(-3),i=-1,r=0;r=e.length-3?[].concat(n,[{separator:!0}],a):[].concat(n,[{separator:!0}],[e[i]],[{separator:!0}],a)}function Fn(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Wn(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=y.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function Ht(){var e=document.getElementById("leaderboard-toggle"),t=document.getElementById("game-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function jn(e,t){var n=t?[t]:["leaderboard-summary","reveal-leaderboard-summary"];n.forEach(function(a){var i=document.getElementById(a);if(!(!i||!e||e.length===0)){var r=e[0];r&&(i.textContent=r.name+": "+r.score)}})}function Gt(){var e=document.getElementById("reveal-leaderboard-toggle"),t=document.getElementById("reveal-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function Dt(){var e=document.getElementById("round-analytics-toggle"),t=document.getElementById("round-analytics");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}var G=!1,le=!1,de=!1,z=!1,H=null,we=null,qn=300,Nt=0,te=!1,ne=null,Un=500,Mt=0;function Vt(){var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!(!e||!t)){e.addEventListener("input",function(){t.textContent=this.value});var n=document.getElementById("bet-toggle");n&&n.addEventListener("click",function(){G||(le=!le,n.classList.toggle("is-active",le))});var a=document.getElementById("submit-btn");a&&a.addEventListener("click",zn);var i=document.getElementById("steal-btn");i&&i.addEventListener("click",ea);var r=document.getElementById("steal-modal-close");r&&r.addEventListener("click",je);var o=document.getElementById("steal-modal");if(o){var l=o.querySelector(".steal-modal-backdrop");l&&l.addEventListener("click",je)}}}function zn(){if(!G){var e=document.getElementById("year-slider"),t=document.getElementById("submit-btn");if(!(!e||!t)){var n=parseInt(e.value,10);t.disabled=!0,t.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"submit",year:n,bet:le})):(We(y.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function ze(){G=!0;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("bet-toggle");e&&e.classList.add("is-submitted"),t&&t.classList.add("hidden"),a&&a.classList.add("hidden"),n&&n.classList.remove("hidden")}function Ft(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(We(y.t("errors.timesUp")),G=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?ze():We(e.message||"Submission failed")}function We(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=y.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function Wt(){G=!1,le=!1;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("year-slider"),i=document.getElementById("bet-toggle");if(e&&e.classList.remove("is-submitted"),t&&(t.disabled=!1,t.classList.remove("hidden","is-loading","is-error"),t.textContent=y.t("game.submitGuess")),i&&i.classList.remove("hidden","is-active"),n&&n.classList.add("hidden"),a){a.value=1990;var r=document.getElementById("selected-year");r&&(r.textContent="1990")}de=!1,Ye(),Jn(),Zn()}function Yn(e,t){var n=document.getElementById("artist-challenge-container");if(n){if(!e||!e.options){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("artist-options"),i=document.getElementById("artist-result"),r=Array.from(a.querySelectorAll(".artist-option-btn")).map(function(m){return m.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(m,c){var f=document.createElement("button");f.className="artist-option-btn",f.dataset.artist=m,f.dataset.index=c,f.textContent=m,f.addEventListener("click",function(){Qn(m)}),a.appendChild(f)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(m){m.classList.add("is-disabled"),m.classList.remove("is-loading","is-wrong");var c=e.correct_artist||we;c&&m.dataset.artist===c&&m.classList.add("is-winner")}),e.winner===s.playerName){var d=e.bonus_points||5;i.textContent=(y.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",d),i.className="artist-result is-winner"}else{var u=(y.t("artistChallenge.someoneBeatYou")||"{winner} got it first!").replace("{winner}",e.winner);i.textContent=u,i.className="artist-result is-late"}i.classList.remove("hidden"),z=!0}else z||i.classList.add("hidden")}}function Qn(e){var t=Date.now();if(!(t-Nt0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}De();var a=(y.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Ve(a,!0),te=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),De(),Ve(y.t("movieChallenge.wrongGuess")||"Not quite...",!1),te=!0;ne=null}function De(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Ve(e,t){var n=document.getElementById("movie-result");n&&(n.textContent=e,n.className="movie-result "+(t?"is-winner":"is-late"),n.classList.remove("hidden"))}function Zn(){te=!1,ne=null;var e=document.getElementById("movie-challenge-container");e&&e.classList.add("hidden");var t=document.getElementById("movie-options");t&&(t.innerHTML="");var n=document.getElementById("movie-result");n&&(n.classList.add("hidden"),n.className="movie-result hidden")}function zt(e,t){var n=document.getElementById("movie-reveal-section");if(n){if(!e||!e.correct_movie){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("movie-reveal-name");a&&(a.textContent=e.correct_movie);var i=document.getElementById("movie-reveal-winners");if(i&&e.results){var r=e.results.winners||[];if(r.length>0){i.innerHTML="",i.classList.remove("hidden");var o=document.createElement("div");o.className="movie-reveal-winners-title",o.textContent=y.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(d){var u=document.createElement("div");u.className="movie-reveal-winner-entry",d.name===t?u.classList.add("is-you"):u.classList.add("is-other"),u.textContent=d.name+" \u2014 +"+d.bonus+" ("+d.time+"s)",i.appendChild(u)})}else{i.innerHTML="",i.classList.remove("hidden");var l=document.createElement("div");l.className="movie-reveal-no-winner",l.textContent=y.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}function $n(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){de=t.steal_available&&!G;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");de?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):Ye()}}}function Ye(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden")}function ea(){!de||G||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function ta(e){var t=document.getElementById("steal-modal"),n=document.getElementById("steal-target-list");if(!(!t||!n)){if(n.innerHTML="",!e||e.length===0){var a=document.createElement("p");a.className="steal-no-targets",a.textContent=y.t("steal.waitForSubmit"),n.appendChild(a)}else e.forEach(function(i){var r=document.createElement("button");r.className="steal-target-btn",r.textContent=i,r.addEventListener("click",function(){na(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function je(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function na(e){var t=y.t("steal.confirm").replace("{name}",e),n=await Z(y.t("steal.confirmTitle")||"Steal Answer?",t,y.t("steal.confirmButton")||"Steal",y.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),je())}function Yt(e){if(e.success){de=!1,G=!0,Ye();var t=document.getElementById("year-selector"),n=document.getElementById("submit-btn"),a=document.getElementById("submitted-confirmation");t&&t.classList.add("is-submitted"),n&&n.classList.add("hidden"),a&&a.classList.remove("hidden"),aa(e.target,e.year);var i=document.getElementById("selected-year"),r=document.getElementById("year-slider");i&&(i.textContent=e.year),r&&(r.value=e.year)}}function Qt(e){ta(e.targets||[])}function aa(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=y.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var Fe=!1,ia=500,ce=!1,Qe=.5;function Le(){return Fe?!1:(Fe=!0,setTimeout(function(){Fe=!1},ia),!0)}function Je(){if(s.isAdmin){var e=document.getElementById("admin-control-bar");e&&(e.classList.remove("hidden"),document.body.classList.add("has-control-bar"))}}function ue(){var e=document.getElementById("admin-control-bar");e&&(e.classList.add("hidden"),document.body.classList.remove("has-control-bar"))}function Jt(){var e=document.getElementById("reaction-bar");e&&e.classList.remove("hidden")}function me(){var e=document.getElementById("reaction-bar");e&&e.classList.add("hidden")}function ra(e){s.hasReactedThisPhase||(s.hasReactedThisPhase=!0,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"reaction",emoji:e})))}function Kt(){var e=document.getElementById("reaction-bar");if(e){var t=e.querySelectorAll(".reaction-btn");t.forEach(function(n){n.addEventListener("click",function(){var a=n.getAttribute("data-emoji");a&&ra(a)})})}}function Xt(e,t){var n=document.getElementById("reaction-container");if(n){var a=document.createElement("div");a.className="reaction-bubble",a.textContent=e+" "+t,a.style.left=20+Math.random()*60+"%",n.appendChild(a),setTimeout(function(){a.remove()},3e3)}}function Se(e){var t=document.getElementById("stop-song-btn"),n=document.getElementById("next-round-admin-btn");if(e==="PLAYING"){if(Ke(),t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.skip"))}}else if(e==="REVEAL"){if(t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}else if(n){n.classList.add("is-disabled"),n.disabled=!0;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}function sa(){if(!ce&&Le()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){console.warn("[Beatify] Cannot stop song: WebSocket not connected");return}var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-label");t&&(t.textContent=y.t("game.stopping"))}s.ws.send(JSON.stringify({type:"admin",action:"stop_song"}))}}function oa(){if(Qe>=1){Zt("max");return}Le()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function la(){if(Qe<=0){Zt("min");return}Le()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function Zt(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=e==="max"?"\u{1F50A} Max":"\u{1F507} Min",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1e3))}async function da(){var e=await Z(y.t("admin.endGameConfirm")||"End Game?",y.t("admin.endGameWarning")||"All players will be disconnected.",y.t("admin.endGame")||"End Game",y.t("common.cancel"));if(e&&Le()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(y.t("errors.CONNECTION_LOST"));return}var t=document.getElementById("end-game-btn");if(t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var qe=!1;function $t(){if(!qe&&s.ws&&s.ws.readyState===WebSocket.OPEN){qe=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=y.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"}))}}function en(){qe=!1}function ca(){$t()}function tn(){var e=document.getElementById("stop-song-btn"),t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn"),a=document.getElementById("next-round-admin-btn"),i=document.getElementById("end-game-btn");e&&e.addEventListener("click",sa),t&&t.addEventListener("click",oa),n&&n.addEventListener("click",la),a&&a.addEventListener("click",ca),i&&i.addEventListener("click",da)}function nn(){ce=!0;var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-stopped"),e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u2713"),n&&(n.textContent=y.t("game.stopped"))}}function Ke(){ce=!1;var e=document.getElementById("stop-song-btn");if(e){e.classList.remove("is-stopped"),e.classList.remove("is-disabled"),e.disabled=!1;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u23F9\uFE0F"),n&&(n.textContent=y.t("game.stop"))}}function an(e){Qe=e,ua(e),ma(e)}function ua(e){var t=document.getElementById("volume-indicator");if(t){var n=Math.round(e*100);t.textContent="\u{1F50A} "+n+"%",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1500)}}function ma(e){var t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn");t&&t.classList.toggle("is-at-limit",e>=1),n&&n.classList.toggle("is-at-limit",e<=0)}function rn(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",$t);var t=document.getElementById("reveal-view");t&&t.addEventListener("click",function(n){n.target.tagName==="BUTTON"||n.target.closest("button")||($.isRunning()&&$.skipAll(),O())})}function sn(e){var t=document.getElementById("intro-splash-modal");if(t){t.classList.remove("hidden");var n=document.getElementById("intro-splash-confirm-btn"),a=t.querySelector(".intro-splash-modal-waiting");n&&(e?(n.classList.remove("hidden"),a&&a.classList.add("hidden"),n.onclick=function(){s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"admin",action:"confirm_intro_splash"}))}):(n.classList.add("hidden"),a&&a.classList.remove("hidden")))}}function on(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var p=window.BeatifyUtils||{};function Xe(e){var t=e.song||{},n=e.players||[],a=document.getElementById("reveal-round"),i=document.getElementById("reveal-total");a&&(a.textContent=e.round||1),i&&(i.textContent=e.total_rounds||10);var r=document.getElementById("closest-wins-badge");r&&(e.closest_wins_mode?r.classList.remove("hidden"):r.classList.add("hidden"));var o=document.getElementById("intro-badge");if(o)if(e.is_intro_round){o.classList.remove("hidden"),o.classList.add("intro-badge--stopped");var l=o.querySelector("[data-i18n]");l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=p.t("game.introStopped")||"Intro complete!")}else o.classList.add("hidden");var d=document.getElementById("reveal-album-cover");d&&(d.src=t.album_art||"/beatify/static/img/no-artwork.svg");var u=document.getElementById("correct-year");u&&(u.textContent=t.year||"????");var m=document.getElementById("song-title"),c=document.getElementById("song-artist");m&&(m.textContent=t.title||"Unknown Song"),c&&(c.textContent=t.artist||"Unknown Artist");var f=document.getElementById("fun-fact-container"),v=document.getElementById("fun-fact"),w=f?f.querySelector(".fun-fact-header"):null,L=p.getLocalizedSongField(t,"fun_fact");if(v&&(v.textContent=L||""),w&&(w.style.display=L?"flex":"none"),pa(t),ga(e.song_difficulty),f){var b=document.getElementById("song-rich-info"),E=b&&b.innerHTML.trim()!=="",S=L&&L.trim()!=="";f.classList.toggle("hidden",!S&&!E)}for(var I=null,_=0;_'+p.t("analytics.noSubmissions")+"",n.classList.remove("hidden");return}var i="";if(e.average_guess!==null&&t){var r=Math.round(e.average_guess-t);r===0?i=p.t("analytics.onTarget"):r>0?i=p.t("analytics.yearsLate",{years:r}):i=p.t("analytics.yearsEarly",{years:Math.abs(r)})}var o=va(e.all_guesses,t),l="";if(e.exact_match_players&&e.exact_match_players.length>0&&(l+='🎯'+p.t("analytics.exactMatches")+':'+e.exact_match_players.map(x).join(", ")+"
"),e.speed_champion&&e.speed_champion.names){var d=e.speed_champion.names.map(x).join(", ");l+='⚡'+p.t("analytics.speedChampion")+':'+d+'('+e.speed_champion.time+"s)
"}if(e.furthest_players&&e.furthest_players.length>0&&e.all_guesses&&e.all_guesses.length>0){var u=e.all_guesses[e.all_guesses.length-1].years_off;u>0&&(l+='😅'+p.t("analytics.furthestGuess")+':'+e.furthest_players.map(x).join(", ")+'('+u+" years)
")}var m=e.average_guess!==null?Math.round(e.average_guess):"?";a.innerHTML=''+p.t("analytics.averageGuess")+''+m+'
'+e.accuracy_percentage+'%'+p.t("analytics.accuracy",{percent:""}).replace("%","")+'
'+i+'
'+p.t("analytics.histogram")+"
"+o+""+(l?''+l+"
":""),n.classList.remove("hidden")}function va(e,t){var n=7;if(!e||e.length===0)return''+p.t("analytics.noGuesses")+"
";for(var a=e.map(function(ae){return ae.guess}),i=Math.min.apply(null,a),r=Math.max.apply(null,a),o=r-i,l=Math.max(1,Math.ceil(o/n)),d=l*n,u=d-o-1,m=i-Math.floor(u/2),c=[],f=0;f=v&&t<=w})}for(var L=0;L=c[E].start&&b<=c[E].end){c[E].count++;break}for(var S=1,I=0;IS&&(S=c[I].count);for(var _="",C=0;C0?Math.max(R,10):0,F=k.count>0?''+k.count+"":"",ke=l===1?String(k.start):k.start+"-"+String(k.end).slice(-2);_+='"}return''+_+"
"}function ga(e){var t=document.getElementById("song-difficulty");if(t){if(!e){t.classList.add("hidden");return}for(var n="",a=0;a★';t.innerHTML=''+n+'
'+p.t("difficulty."+e.label)+''+e.accuracy+"% "+p.t("difficulty.accuracy")+"",t.classList.remove("hidden")}}function pa(e){var t=document.getElementById("song-rich-info");if(t){var n=[],a=ya(e.chart_info||{});a.length>0&&(n=n.concat(a));var i=ba(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=p.getLocalizedSongField(e,"awards")||[],o=wa(r);o.length>0&&(n=n.concat(o)),n.length>0?t.innerHTML=''+n.join("")+"
":t.innerHTML=""}}function ya(e){if(!e)return[];var t=[];if(e.billboard_peak&&e.billboard_peak>0){var n=e.weeks_on_chart?' \xB7 '+e.weeks_on_chart+" "+p.t("reveal.weeksShort")+"":"";t.push('\u{1F4CA}#'+e.billboard_peak+" "+p.t("reveal.chartBillboard")+n+"")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.german_peak+" "+p.t("reveal.chartGerman")+""),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.uk_peak+" "+p.t("reveal.chartUK")+""),t}function ba(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+""+x(a)+"")}return t}function ha(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"song-badge--diamond":t.indexOf("platinum")!==-1?"song-badge--platinum":t.indexOf("gold")!==-1?"song-badge--gold":"song-badge--platinum"}function Ea(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"\u{1F48E}":t.indexOf("platinum")!==-1?"\u{1F4BF}":t.indexOf("gold")!==-1?"\u{1F947}":"\u{1F4BF}"}function wa(e){if(!e||e.length===0)return[];for(var t=[],n=e.slice(0,3),a=0;a'+o+""+x(i)+"")}return e.length>3&&t.push('+'+(e.length-3)+" more"),t}function La(e){var t=e.toLowerCase();return t.indexOf("grammy")!==-1?"song-badge--grammy":t.indexOf("eurovision")!==-1?"song-badge--eurovision":t.indexOf("oscar")!==-1||t.indexOf("academy award")!==-1?"song-badge--oscar":t.indexOf("hall of fame")!==-1?"song-badge--halloffame":"song-badge--award"}function Sa(e){var t=e.toLowerCase();return t.indexOf("eurovision")!==-1?"\u{1F3A4}":t.indexOf("grammy")!==-1?"\u{1F3C6}":t.indexOf("hall of fame")!==-1?"\u2B50":"\u{1F3C6}"}function Ia(e,t){var n=document.getElementById("reveal-emotion"),a=document.getElementById("personal-result");if(!n)return;var i=n.classList.contains("reveal-emotion-inline")||document.querySelector(".reveal-container--compact");n.className=i?"reveal-emotion-inline":"reveal-emotion",n.innerHTML="",n.classList.add("hidden"),a&&a.classList.remove("is-delayed"),O();var r=p.t("reveal.emotions");function o(v){return v[Math.floor(Math.random()*v.length)]}function l(v){return v===1?p.t("reveal.offByYear"):p.t("reveal.offByYears",{years:v})}var d="missed",u=o(r.missed),m=o(r.missedSub);if(e&&!e.missed_round){var c=e.years_off||0;c===0?(d="exact",u=o(r.exact),m=o(r.exactSub)):c<=2?(d="close",u=o(r.close),m=o(r.closeSub)+" "+l(c)):c<=5?(d="close",u=o(r.close),m=l(c)):(d="wrong",u=o(r.wrong),m=o(r.wrongSub)+" "+l(c))}else e&&e.missed_round&&(d="missed",u=o(r.missed),m=o(r.missedSub));var f=''+u+"";m&&(f+=''+m+"
"),n.innerHTML=f,n.classList.add("reveal-emotion--"+d),n.classList.remove("hidden"),d==="exact"&&ee(),a&&d!=="missed"&&a.classList.add("is-delayed")}function Ba(e,t){var n=document.getElementById("result-content");if(n){if(!e){n.innerHTML=''+p.t("reveal.playerNotFound")+"
";return}if(e.missed_round){var a='\u23F0
'+p.t("reveal.noSubmission")+"
",i=e.previous_streak||0;i>=2&&(a+='\u{1F494}Lost '+i+"-streak!
"),a+='0 pts
',n.innerHTML=a;return}var r=e.years_off||0,o=r===0?p.t("reveal.exact"):r===1?p.t("reveal.yearOff",{years:1}):p.t("reveal.yearsOff",{years:r}),l=r===0?"is-exact":r<=3?"is-close":"is-far",d=e.speed_multiplier||1,u=e.base_score||0,m=d>1,c=e.streak_bonus||0,f=e.artist_bonus||0,v="";m&&u>0&&(v=''+p.t("reveal.baseScore")+''+u+' pts
'+p.t("reveal.speedBonus")+''+d.toFixed(2)+"x
");var w="";e.bet_outcome==="won"?w='\u{1F3B2} '+p.t("reveal.betWon").replace("! 2x points","")+'2x
':e.bet_outcome==="lost"&&(w='\u{1F3B2} '+p.t("reveal.betLost")+'-
');var L="";c>0&&(L=''+e.streak+'-streak bonus!+'+c+" pts
");var b="";f>0&&(b='\u{1F3A4} '+(p.t("artistChallenge.artistBonus")||"Artist Bonus")+'+'+f+" pts
");var E=e.round_score+c+f,S=c>0||f>0,I=e.round_score>=20,_=M.players[e.name],C=_?_.score:e.score-E,k=_?_.streak:0,R=dt(k,e.streak||0);n.innerHTML=''+p.t("reveal.yourGuess")+''+(e.guess||"n/a")+'
'+p.t("reveal.correctYear")+''+t+'
'+p.t("reveal.accuracy")+''+o+"
"+v+w+'+0 pts
'+L+b+(S?''+p.t("reveal.total")+': +0 pts
':"");var D=n.querySelector(".score-value");D&&(ot(D,0,e.round_score,{betWon:e.bet_outcome==="won",betLost:e.bet_outcome==="lost",streakMilestone:R,isBigScore:I}),e.bet_outcome==="won"&&e.round_score>0&&setTimeout(function(){var T=document.getElementById("personal-result-score");T&&Ne(T,e.round_score,{isBetWin:!0})},200));var V=n.querySelector(".total-value");V&&S&&(setTimeout(function(){se(V,0,E,600)},300),R&&setTimeout(function(){var T=n.querySelector(".result-total");if(T){var F={3:20,5:50,10:100}[R]||0;Ne(T,F,{isStreak:!0,text:"+"+F+" "+R+"-Streak!"})}},500))}}function xa(e,t){var n=document.getElementById("reveal-results-cards");if(n){if(!e||e.length===0){n.innerHTML="";return}var a=null;t&&e.forEach(function(o){!o.missed_round&&o.years_off!=null&&(a===null||o.years_off';i.forEach(function(o){var l=o.name===s.playerName,d=o.missed_round===!0,u=o.years_off||0,m=o.round_score||0,c=d?"is-score-zero":m>=10?"is-score-high":m>=1?"is-score-medium":"is-score-zero",f=t&&!d&&a!==null&&(o.years_off||0)===a,v=f?" is-closest-winner":"",w=d?"\u2014":o.guess||"n/a",L=d?p.t("reveal.noGuessShort"):u===0?p.t("reveal.exact"):p.t("reveal.shortOff",{years:u}),b=o.bet?'\u{1F3B2}':"",E=f?'\u{1F3AF}':"",S="";o.artist_bonus&&o.artist_bonus>0&&(S='\u{1F3A4} +'+o.artist_bonus+"");var I="";if(o.stole_from)I='\u{1F977}'+p.t("steal.stolenFrom",{name:x(o.stole_from)})+"
";else if(o.was_stolen_by&&o.was_stolen_by.length>0){var _=o.was_stolen_by.map(x).join(", ");I='\u{1F3AF}'+p.t("steal.stolenBy",{name:_})+"
"}r+=''+x(o.name)+b+E+'
'+w+'
'+L+"
"+I+'
+'+m+S+"
"}),r+="",n.innerHTML=r}}var A=window.BeatifyUtils||{};function dn(e){window.scrollTo(0,0);var t=e.leaderboard||[];t.forEach(function(b){b.is_current=b.name===s.playerName}),[1,2,3].forEach(function(b){var E=t.find(function(_){return _.rank===b}),S=document.getElementById("podium-"+b+"-name"),I=document.getElementById("podium-"+b+"-score");S&&(S.textContent=E?x(E.name):"---"),I&&(I.textContent=E?E.score:"0")});var n=t.find(function(b){return b.is_current}),a=document.getElementById("your-final-rank"),i=document.getElementById("your-final-score"),r=document.getElementById("stat-best-streak"),o=document.getElementById("stat-rounds"),l=document.getElementById("stat-bets");n&&(a&&(a.textContent="#"+n.rank),i&&(i.textContent=n.score+" "+A.t("leaderboard.points")),r&&(r.textContent=n.best_streak||0),o&&(o.textContent=n.rounds_played||0),l&&(l.textContent=n.bets_won||0));var d=document.getElementById("final-leaderboard-list");d&&(d.innerHTML=t.map(function(b){var E=b.is_current?"is-current":"",S=b.connected===!1?"final-entry--disconnected":"",I=b.connected===!1?'(away)':"";return'#'+b.rank+''+x(b.name)+I+''+b.score+"
"}).join("")),Ca(e.superlatives),_a(e.highlights),ka(e.share_data);var u=document.getElementById("end-admin-controls"),m=document.getElementById("end-player-message");if(n&&n.is_admin){u&&u.classList.remove("hidden"),m&&m.classList.add("hidden");var c=document.getElementById("new-game-btn");c&&(c.onclick=Ta);var f=document.getElementById("player-rematch-btn");f&&(f.onclick=function(){f.disabled=!0;var b=f.textContent;f.textContent="\u23F3",fetch("/beatify/api/rematch-game",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"}}).then(function(E){if(!E.ok)return E.json().then(function(S){throw new Error(S.message||"Rematch failed")});f.textContent="\u23F3"}).catch(function(E){console.error("[Player] Rematch failed:",E),alert(E.message||"Failed to start rematch"),f.disabled=!1,f.textContent=b})})}else u&&u.classList.add("hidden"),m&&m.classList.remove("hidden");if(n){var v=e.total_rounds||10,w=n.best_streak||0,L=w===v&&v>0;L?ee("perfect"):n.rank===1&&ee("winner")}}function Ca(e){var t=document.getElementById("superlatives-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n="";e.forEach(function(a,i){var r="";switch(a.value_label){case"avg_time":r=a.value+"s "+A.t("superlatives.avgTime");break;case"streak":r=a.value+" "+A.t("superlatives.streak");break;case"bets":r=a.value+" "+A.t("superlatives.bets");break;case"points":r=a.value+" "+A.t("superlatives.points");break;case"close_guesses":r=a.value+" "+A.t("superlatives.closeGuesses");break;default:r=a.value}n+=''+a.emoji+'
'+A.t("superlatives."+a.title)+'
'+x(a.player_name)+'
'+r+"
"}),t.innerHTML=n,t.classList.remove("hidden")}}function _a(e){var t=document.getElementById("highlights-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n=document.getElementById("highlights-list");if(n){var a="";e.forEach(function(i,r){var o=A.t("highlights."+i.description,i.description_params)||i.description;o===i.description&&i.description_params&&(o=A.t("highlights."+i.description)||i.description,Object.keys(i.description_params).forEach(function(l){o=o.replace("{"+l+"}",x(i.description_params[l]))})),a+=''+(i.emoji||"\u2728")+'
'+o+'
'+A.t("highlights.roundLabel",{round:i.round})+"
"}),n.innerHTML=a,t.classList.remove("hidden")}}}function ka(e){var t=document.getElementById("share-container");if(t){if(!e||!e.emoji_grids){t.classList.add("hidden");return}var n=e.emoji_grids[s.playerName];if(!n){var a=Object.keys(e.emoji_grids);a.length===1&&(n=e.emoji_grids[a[0]])}if(!n){t.classList.add("hidden");return}var i=document.getElementById("share-emoji-grid");if(i){var r=n.split(`
+`).map(function(d){return''+A.escapeHtml(d)+"
"}).join("");i.innerHTML=r,i.dataset.rawText=n}var o=document.getElementById("share-copy-btn");o&&(o.onclick=function(){navigator.clipboard.writeText(n).then(function(){var d=document.getElementById("share-toast");d&&(d.classList.remove("hidden"),setTimeout(function(){d.classList.add("hidden")},2e3))})});var l=document.getElementById("share-save-btn");l&&(l.onclick=function(){Aa(n,e.playlist_name,e)}),t.classList.remove("hidden")}}function Aa(e,t,n){var a=document.createElement("canvas");a.width=800,a.height=800;var i=a.getContext("2d"),r=i.createLinearGradient(0,0,0,800);r.addColorStop(0,"#0f0c29"),r.addColorStop(.5,"#302b63"),r.addColorStop(1,"#24243e"),i.fillStyle=r,i.fillRect(0,0,800,800);var o=i.createLinearGradient(0,0,800,0);o.addColorStop(0,"#e94560"),o.addColorStop(1,"#0f3460"),i.fillStyle=o,i.fillRect(0,0,800,4);var l=new Image;l.src="/beatify/static/img/icon-256.png",l.onerror=function(){d(null)},l.onload=function(){d(l)};function d(u){u?(i.drawImage(u,28,20,64,64),i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="left",i.fillText("Beatify",104,60)):(i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="center",i.fillText("\u{1F3B5} Beatify",400,55)),i.textAlign="center";var m=(t||"").toUpperCase();i.font="bold 13px system-ui, sans-serif";var c=i.measureText(m).width+32,f=400-c/2;i.fillStyle="rgba(233,69,96,0.18)",i.beginPath(),i.roundRect(f,96,c,30,15),i.fill(),i.strokeStyle="#e94560",i.lineWidth=1,i.stroke(),i.fillStyle="#e94560",i.fillText(m,400,116);for(var v=e.split(`
+`).filter(function(N){return N.trim()!==""}),w="",L=[],b="",E="",S="",I="",_=0;_=ge&&(s.isReconnecting=!1,pe(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}function Q(e){if(s.playerName=e,vn(e),!(s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN))){var t=window.location.protocol==="https:"?"wss:":"ws:",n=t+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(n),s.ws.onopen=function(){s.reconnectAttempts=0,s.isReconnecting=!1,pe(),pn();var a={type:"join",name:e};s.isAdmin&&(a.is_admin=!0),s.ws.send(JSON.stringify(a))},s.ws.onmessage=function(a){try{var i=JSON.parse(a.data);hn(i)}catch(r){console.error("Failed to parse WebSocket message:",r)}},s.ws.onclose=function(){if(s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=ge&&(s.isReconnecting=!1,pe(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}s.connectWithSession=_e;s.connectWebSocket=Q;function hn(e){var t=document.getElementById("join-btn"),n=document.getElementById("name-input");if(e.type==="state"){var a=e.players||[],i=a.find(function(u){return u.name===s.playerName});if(i&&(s.isAdmin=i.is_admin===!0),e.language&&(Ha(e.language),typeof BeatifyI18n<"u"&&e.language!==BeatifyI18n.getLanguage()&&BeatifyI18n.setLanguage(e.language).then(function(){BeatifyI18n.initPageTranslations(),Pe(a),e.difficulty&&he(e.difficulty),e.phase==="REVEAL"&&Xe(e),(e.phase==="PLAYING"||e.phase==="REVEAL")&&Se(e.phase)})),e.phase==="LOBBY"){Y(),ue(),me(),s.currentRoundNumber=0,q("warmup");var r=document.getElementById("start-game-btn");r&&(r.disabled=!1,r.innerHTML='\u{1F389}'+ve.t("lobby.startGame")+""),B("lobby-view"),Pe(a),e.difficulty&&he(e.difficulty),e.join_url&&It(e.join_url),_t(a)}else if(e.phase==="PLAYING"){var o=e.round||1;o!==s.currentRoundNumber&&(s.currentRoundNumber=o,Wt()),en(),q("party"),B("game-view"),oe(),Ot(e),e.intro_splash_pending?sn(s.isAdmin):on(),e.difficulty&&he(e.difficulty),e.deadline&&Rt(e.deadline),Vt(),Ht(),Je(),Se("PLAYING"),me()}else e.phase==="REVEAL"?(Y(),e.early_reveal&&Tt(),q("party"),B("reveal-view"),Xe(e),Gt(),Dt(),Je(),Se("REVEAL"),s.hasReactedThisPhase=!1,Jt()):e.phase==="PAUSED"?(Y(),ue(),me(),q("warmup"),B("paused-view"),cn(e)):e.phase==="END"&&(Y(),ue(),me(),s.currentRoundNumber=0,q("warmup"),B("end-view"),dn(e),fe())}else if(e.type==="join_ack"){e.session_id&&Oa(e.session_id);try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}}else if(e.type==="reconnect_ack")e.success&&e.name?(s.playerName=e.name,vn(e.name),At(e.name)):(Ze(),fe(),s.playerName=null,B("join-view"));else if(e.type==="submit_ack")ze();else if(e.type==="metadata_update")Pt(e.song);else if(e.type==="error"){if(e.code==="ROUND_EXPIRED"||e.code==="ALREADY_SUBMITTED"){Ft(e);return}if(e.code==="GAME_ENDED"){B("end-view");return}if(e.code==="NOT_ADMIN"){s.isAdmin=!1,ue(),console.warn("Admin action rejected: not admin");return}if(e.code==="SESSION_TAKEOVER"){s.isReconnecting=!1,pe(),s.playerName=null,et(),console.warn("Session taken over by another tab");return}if(e.code==="SESSION_NOT_FOUND"){Ze(),s.intentionalLeave=!0,s.ws&&s.ws.close(),B("join-view");return}if(e.code==="ADMIN_CANNOT_LEAVE"){s.intentionalLeave=!1,alert(e.message||"Host cannot leave. End the game instead.");return}if(e.code==="INVALID_ACTION"&&e.message==="No song playing"){Ke(),console.warn("[Beatify] Stop song failed: No song playing");return}B("join-view"),ja(e.message),t&&(t.disabled=!1,t.textContent=ve.t("join.joinButton")),n&&n.focus(),s.playerName=null,fe()}else if(e.type==="song_stopped")nn();else if(e.type==="volume_changed")an(e.level);else if(e.type==="game_ended")Wa();else if(e.type==="rematch_started"){console.log("[Player] Rematch started - transitioning to lobby"),$.clear(),O(),B("lobby-view");var l=document.getElementById("player-rematch-btn");l&&(l.disabled=!1,l.textContent="\u{1F501}");var d=$e();d&&(s.ws&&s.ws.readyState===WebSocket.OPEN?(s.reconnectAttempts=0,s.ws.send(JSON.stringify({type:"reconnect",session_id:d}))):(s.reconnectAttempts=0,_e()))}else e.type==="left"?Fa():e.type==="steal_targets"?Qt(e):e.type==="steal_ack"?Yt(e):e.type==="artist_guess_ack"?jt(e):e.type==="movie_guess_ack"?Ut(e):e.type==="player_reaction"&&Xt(e.player_name,e.emoji)}function Fa(){fe(),Ze(),s.playerName=null,s.isAdmin=!1,B("join-view")}function Wa(){var e=s.isAdmin;fe();try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}ft(),Et(),$.clear(),O(),s.playerName=null,s.isAdmin=!1,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.close(),s.ws=null;var t=document.getElementById("end-view");if(!(!t||!t.classList.contains("hidden"))){var n=document.getElementById("end-player-message");n&&(n.innerHTML='Thanks for playing!
Scan the QR code again to join the next game.
',n.classList.remove("hidden")),B("end-view")}}function ja(e){var t=document.getElementById("name-validation-msg");t&&(t.textContent=e,t.classList.remove("hidden"))}function tt(e){var t=(e||"").trim();return t?t.length>Ma?{valid:!1,error:"Name too long (max 20 characters)"}:{valid:!0,name:t}:{valid:!1,error:"Please enter a name"}}function un(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");if(!(!e||!t)){var a=tt(e.value);a.valid&&(t.disabled=!0,t.textContent=ve.t("game.joining"),n&&n.classList.add("hidden"),Q(a.name))}}function qa(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");!e||!t||(e.addEventListener("input",function(){var a=tt(this.value);t.disabled=!a.valid,n&&(n.textContent=!a.valid&&this.value?a.error:"",n.classList.toggle("hidden",a.valid||!this.value))}),t.addEventListener("click",un),e.addEventListener("keypress",function(a){a.key==="Enter"&&!t.disabled&&un()}))}function Ua(){var e=document.getElementById("retry-connection-btn");e&&e.addEventListener("click",function(){s.playerName?(s.reconnectAttempts=0,B("loading-view"),Q(s.playerName)):Ce()})}async function mn(){var e=K.getDeviceTier();document.body.classList.add("device-tier-"+e);var t=await ve.waitForI18n();if(!t)console.error("[Player] BeatifyI18n module failed to load - UI will use fallback text");else{var n=Ga();await BeatifyI18n.init(n),BeatifyI18n.initPageTranslations()}var a=document.getElementById("dashboard-hint-url");a&&(a.textContent=window.location.origin+"/beatify/dashboard");var i=document.getElementById("player-dashboard-url");if(i&&(i.href=window.location.origin+"/beatify/dashboard"),qa(),Bt(),Ct(),kt(),rn(),tn(),Ua(),vt(),gt(),pt(),Kt(),Va()&&s.playerName){Q(s.playerName);return}var r=Pa();if(r&&s.gameId){console.log("[Beatify] Auto-reconnecting as:",r),Q(r);return}if(r){var o=document.getElementById("name-input"),l=document.getElementById("join-btn");if(o&&(o.value=r,l)){var d=tt(r);l.disabled=!d.valid}}}Ce();document.getElementById("refresh-btn")?.addEventListener("click",function(){B("loading-view"),Ce()});document.getElementById("retry-btn")?.addEventListener("click",function(){B("loading-view"),Ce()});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",mn):mn();"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/static/sw.js",{scope:"/beatify/"}).then(function(e){console.log("[Beatify] SW registered:",e.scope)}).catch(function(e){console.warn("[Beatify] SW registration failed:",e)})});document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible"){var e=s.ws;(!e||e.readyState===WebSocket.CLOSING||e.readyState===WebSocket.CLOSED)&&s.playerName&&(console.log("[Beatify] Page visible, WebSocket dead \u2014 reconnecting immediately."),s.reconnectAttempts=0,_e())}});
diff --git a/tests/unit/test_media_player.py b/tests/unit/test_media_player.py
index 84686ad0..0303e73c 100644
--- a/tests/unit/test_media_player.py
+++ b/tests/unit/test_media_player.py
@@ -174,7 +174,7 @@ def always_queued(*args):
):
result = await svc.play_song(_make_song(title="New Song"))
- assert result is False # #418: returns False on timeout so round can retry
+ assert result is True # #345: return True on timeout — MA may still be buffering
assert poll_count >= 4 # but waited until timeout
@pytest.mark.asyncio
@@ -240,8 +240,8 @@ def realistic_flow(*args):
assert poll_count >= 8 # waited for the full realistic flow
@pytest.mark.asyncio
- async def test_ma_returns_false_on_timeout(self):
- """Should return False if playback never confirmed (#418: allow retry/skip)."""
+ async def test_ma_returns_true_even_on_timeout(self):
+ """Should return True even if playback never confirmed — MA may still be buffering (#345)."""
hass = _make_hass("buffering", media_title="Old Song")
svc = MediaPlayerService(hass, "media_player.test", platform="music_assistant")
@@ -254,7 +254,7 @@ async def test_ma_returns_false_on_timeout(self):
):
result = await svc.play_song(_make_song(title="New Song"))
- assert result is False
+ assert result is True
@pytest.mark.asyncio
async def test_ma_first_song_no_previous_title(self):
@@ -307,6 +307,86 @@ async def test_sonos_still_uses_blocking_true(self):
)
+ @pytest.mark.asyncio
+ async def test_ma_ignores_wrong_song_from_previous_request(self):
+ """If a previous slow song arrives, it must NOT be accepted as confirmation.
+
+ Regression test for race condition: retry fires Song 2 but Song 1
+ (from a previous timed-out request) starts playing first. The polling
+ must check that the EXPECTED title is playing, not just "any change".
+ """
+ hass = _make_hass("idle", media_title="")
+ svc = MediaPlayerService(hass, "media_player.test", platform="music_assistant")
+
+ poll_count = 0
+ idle_state = _make_state(
+ "idle",
+ media_title="",
+ media_position=0,
+ media_position_updated_at="2020-01-01T00:00:00+00:00",
+ )
+ # Wrong song arrives (from a previous timed-out request)
+ wrong_song = _make_state(
+ "playing",
+ media_title="What Is Love",
+ media_position=1,
+ media_position_updated_at="2020-01-01T00:00:05+00:00",
+ )
+ # Correct song finally arrives
+ correct_song = _make_state(
+ "playing",
+ media_title="Ready or Not",
+ media_position=1,
+ media_position_updated_at="2020-01-01T00:00:12+00:00",
+ )
+
+ def race_condition_flow(*args):
+ nonlocal poll_count
+ poll_count += 1
+ if poll_count <= 1:
+ return idle_state # before
+ if poll_count <= 5:
+ return wrong_song # WRONG song playing — must NOT confirm
+ return correct_song # correct song arrives
+
+ hass.states.get = MagicMock(side_effect=race_condition_flow)
+
+ with patch(
+ "custom_components.beatify.services.media_player.asyncio.sleep",
+ new_callable=AsyncMock,
+ ):
+ result = await svc.play_song(_make_song(title="Ready or Not"))
+
+ assert result is True
+ assert poll_count >= 6 # Must have waited past the wrong song
+
+ @pytest.mark.asyncio
+ async def test_ma_matches_title_with_suffix(self):
+ """MA may append suffixes like '(Official HD Video)' — match by substring."""
+ hass = _make_hass("idle", media_title="")
+ svc = MediaPlayerService(hass, "media_player.test", platform="music_assistant")
+
+ idle_state = _make_state(
+ "idle", media_title="", media_position=0,
+ media_position_updated_at="2020-01-01T00:00:00+00:00",
+ )
+ playing_with_suffix = _make_state(
+ "playing",
+ media_title="Ready Or Not (Official HD Video)",
+ media_position=1,
+ media_position_updated_at="2020-01-01T00:00:05+00:00",
+ )
+ hass.states.get = MagicMock(side_effect=[idle_state, playing_with_suffix])
+
+ with patch(
+ "custom_components.beatify.services.media_player.asyncio.sleep",
+ new_callable=AsyncMock,
+ ):
+ result = await svc.play_song(_make_song(title="Ready or Not"))
+
+ assert result is True
+
+
class TestAvailabilityCheck:
"""Tests for is_available() used in state.py pre-flight."""
From 44cff541aa6f95ea5c79d04869848850e6b07f18 Mon Sep 17 00:00:00 2001
From: Tobias Koops
Date: Sun, 5 Apr 2026 16:43:32 +0200
Subject: [PATCH 2/3] fix: address Gemini review feedback
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Use `song.get("title") or ""` to handle explicit None values (prevents
AttributeError on .lower())
- Reset button visual state (disabled + text) in resetNextRoundPending()
as defensive measure — updateRevealView() already handles this on each
REVEAL phase, but explicit reset ensures consistency
Co-Authored-By: Claude Opus 4.6 (1M context)
---
custom_components/beatify/services/media_player.py | 2 +-
custom_components/beatify/www/js/player-game.js | 14 ++++++++++++++
.../beatify/www/js/player.bundle.min.js | 2 +-
3 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/custom_components/beatify/services/media_player.py b/custom_components/beatify/services/media_player.py
index c274742e..67f02484 100644
--- a/custom_components/beatify/services/media_player.py
+++ b/custom_components/beatify/services/media_player.py
@@ -237,7 +237,7 @@ async def _play_via_music_assistant(self, song: dict[str, Any]) -> bool:
_LOGGER.debug("MA URI converted: %s → %s", raw_uri, uri)
_LOGGER.debug("MA playback: %s on %s", uri, self._entity_id)
- expected_title = song.get("title", "")
+ expected_title = song.get("title") or ""
# Capture state before to detect actual song change on speaker
state_before = self._hass.states.get(self._entity_id)
diff --git a/custom_components/beatify/www/js/player-game.js b/custom_components/beatify/www/js/player-game.js
index 5920de8d..2b85821f 100644
--- a/custom_components/beatify/www/js/player-game.js
+++ b/custom_components/beatify/www/js/player-game.js
@@ -1708,9 +1708,23 @@ export function handleNextRound() {
/**
* Reset next-round pending state. Called when a new game state arrives
* (phase change), so the button can be used again in the next reveal.
+ * Note: updateRevealView() in player-reveal.js already re-enables the
+ * button and resets its text on each REVEAL phase — this is a defensive
+ * measure to ensure consistent state even if the call order changes.
*/
export function resetNextRoundPending() {
nextRoundPending = false;
+ var revealBtn = document.getElementById('next-round-btn');
+ var barBtn = document.getElementById('next-round-admin-btn');
+ if (revealBtn) {
+ revealBtn.disabled = false;
+ revealBtn.textContent = utils.t('admin.nextRound');
+ }
+ if (barBtn) {
+ barBtn.disabled = false;
+ var labelEl = barBtn.querySelector('.control-label');
+ if (labelEl) labelEl.textContent = utils.t('admin.nextRound');
+ }
}
/**
diff --git a/custom_components/beatify/www/js/player.bundle.min.js b/custom_components/beatify/www/js/player.bundle.min.js
index 85634b67..3ac1c522 100644
--- a/custom_components/beatify/www/js/player.bundle.min.js
+++ b/custom_components/beatify/www/js/player.bundle.min.js
@@ -1,3 +1,3 @@
-var Ae=window.BeatifyUtils||{},s={ws:null,playerName:null,isAdmin:!1,reconnectAttempts:0,isReconnecting:!1,intentionalLeave:!1,hasReactedThisPhase:!1,currentRoundNumber:0,gameId:new URLSearchParams(window.location.search).get("game"),connectWithSession:null,connectWebSocket:null},wn=document.getElementById("loading-view"),Ln=document.getElementById("not-found-view"),Sn=document.getElementById("ended-view"),In=document.getElementById("in-progress-view"),Bn=document.getElementById("join-view"),xn=document.getElementById("lobby-view"),Cn=document.getElementById("game-view"),_n=document.getElementById("reveal-view"),kn=document.getElementById("paused-view"),An=document.getElementById("end-view"),Tn=document.getElementById("connection-lost-view"),Nn=[wn,Ln,Sn,In,Bn,xn,Cn,_n,kn,An,Tn];function B(e){Ae.showView(Nn,e),(e==="join-view"||e==="loading-view"||e==="not-found-view"||e==="ended-view"||e==="in-progress-view"||e==="connection-lost-view")&&q("calm"),e==="join-view"&&setTimeout(function(){var t=document.getElementById("name-input");t&&t.focus()},100)}function Z(e,t,n,a){return new Promise(function(i){var r=document.getElementById("confirm-modal"),o=document.getElementById("confirm-modal-title"),l=document.getElementById("confirm-modal-message"),d=document.getElementById("confirm-modal-yes"),u=document.getElementById("confirm-modal-no");if(!r||!o||!l||!d||!u){i(confirm(t||e));return}o.textContent=e,l.textContent=t,d.textContent=n||Ae.t("common.confirm")||"Confirm",u.textContent=a||Ae.t("common.cancel")||"Cancel",r.classList.remove("hidden");function m(){r.classList.add("hidden"),d.removeEventListener("click",c),u.removeEventListener("click",f),v.removeEventListener("click",f)}function c(){m(),i(!0)}function f(){m(),i(!1)}var v=r.querySelector(".modal-backdrop");d.addEventListener("click",c),u.addEventListener("click",f),v&&v.addEventListener("click",f)})}function x(e){var t=document.createElement("div");return t.textContent=e,t.innerHTML}function be(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function Mn(e){return 1-Math.pow(1-e,4)}function se(e,t,n,a,i){if(be()||t===n)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var r=K.getQualitySettings();if(r.scoreDuration===0)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var o=Math.min(a,r.scoreDuration||a);i=i||Mn;var l=null,d=null,u=!1,m=n;function c(f){if(!u){l||(l=f);var v=f-l,w=Math.min(v/o,1),L=i(w),b=Math.round(t+(m-t)*L);e.textContent=b,w<1&&(d=requestAnimationFrame(c))}}return d=requestAnimationFrame(c),{cancel:function(){u=!0,d&&cancelAnimationFrame(d)},skipToEnd:function(){u=!0,d&&cancelAnimationFrame(d),e.textContent=m}}}function ot(e,t,n,a){a=a||{};var i=500;a.betWon?i=800:a.isBigScore?i=700:a.betLost&&(i=400),e.classList.add("score-animating");var r=null;a.betWon?r="score-glow-gold":a.betLost?(r="score-shake",e.classList.add("score-flash-red")):a.streakMilestone?r="score-burst":a.isBigScore&&(r="score-pop"),r&&!be()&&e.classList.add(r),se(e,t,n,i);function o(){e.classList.remove("score-animating"),r&&e.classList.remove(r),e.classList.remove("score-flash-red")}r&&!be()?e.addEventListener("animationend",function l(){e.removeEventListener("animationend",l),o()}):setTimeout(o,i+50)}function Ne(e,t,n){if(n=n||{},!be()){var a=document.createElement("div");a.className="points-popup",a.textContent=n.text||"+"+t,n.isStreak?a.classList.add("points-popup--streak"):n.isBetWin&&a.classList.add("points-popup--gold");var i=e.getBoundingClientRect();a.style.left=i.left+i.width/2+"px",a.style.top=i.top+"px",document.body.appendChild(a),a.addEventListener("animationend",function(){a.parentNode&&a.parentNode.removeChild(a)}),setTimeout(function(){a.parentNode&&a.parentNode.removeChild(a)},1200)}}var M={players:{},leaderboard:[],initialized:!1};function lt(){return M.initialized}var rt=[3,5,10,15,20,25];function dt(e,t){for(var n=0;n=a)return a}return null}function ct(e){var t=e.map(function(a){return a.name}),n={};return t.forEach(function(a,i){var r=M.leaderboard.indexOf(a);r===-1?n[a]="new":ir&&(n[a]="down")}),n}function ut(e,t){M.players={},e.forEach(function(n){M.players[n.name]={score:n.score,rank:n.rank||0,streak:n.streak||0}}),t&&(M.leaderboard=t.map(function(n){return n.name})),M.initialized=!0}var K=(function(){var e=window.matchMedia("(prefers-reduced-motion: reduce)"),t=e.matches;e.addEventListener("change",function(i){t=i.matches});var n=null;function a(){if(n!==null)return n;var i=navigator.hardwareConcurrency||2,r=navigator.deviceMemory||4,o=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;return i<=2||r<=2?n="low":i<=4||r<=4||o?n="medium":n="high",n}return a(),{prefersReducedMotion:function(){return t},getDeviceTier:a,getQualitySettings:function(){var i=a();if(t)return{confettiParticles:0,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!1};switch(i){case"low":return{confettiParticles:5,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!0};case"medium":return{confettiParticles:10,scoreDuration:300,leaderboardAnimation:"simplified",neonGlow:!1,enableAnimations:!0};default:return{confettiParticles:15,scoreDuration:500,leaderboardAnimation:"full",neonGlow:!0,enableAnimations:!0}}},ifMotionAllowed:function(i,r){t?r&&r():i()},withWillChange:function(i,r,o){i&&(i.style.willChange=r,setTimeout(function(){i&&i.style&&(i.style.willChange="auto")},(o||500)+100))}}})(),$=(function(){var e=[],t=!1,n=null,a=null,i=2e3;function r(){if(a&&(clearTimeout(a),a=null),e.length===0){t=!1,n=null;return}n=e.shift(),a=setTimeout(function(){n&&n.skipToEnd&&n.skipToEnd(),r()},i),n.run(function(){a&&(clearTimeout(a),a=null),r()})}return{add:function(o){e.push(o),t||(t=!0,r())},skipAll:function(){a&&(clearTimeout(a),a=null),n&&n.skipToEnd&&n.skipToEnd(),e.forEach(function(o){o.skipToEnd&&o.skipToEnd()}),e=[],t=!1,n=null},clear:function(){a&&(clearTimeout(a),a=null),e=[],t=!1,n=null},isRunning:function(){return t},getMaxDuration:function(){return i}}})(),X={VISIBLE_BUFFER:2,ENTRY_HEIGHT:48,MIN_PLAYERS_FOR_LAZY:10,ROOT_MARGIN:"96px 0px",DEFAULT_VIEWPORT_HEIGHT:280},h={observer:null,fullData:[],visibleRange:{start:0,end:10},listEl:null,isLazyEnabled:!1};function mt(e){e&&(h.observer&&h.listEl!==e&&(h.observer.disconnect(),h.observer=null),!h.observer&&(h.listEl=e,h.observer=new IntersectionObserver(function(t){t.forEach(function(n){if(!(!n.isIntersecting||!h.isLazyEnabled)){var a=h.fullData,i=h.visibleRange,r=X.VISIBLE_BUFFER;if(n.target.classList.contains("leaderboard-sentinel--top")){if(i.start>0){var o=Math.max(0,i.start-r);h.visibleRange.start=o,re()}}else if(n.target.classList.contains("leaderboard-sentinel--bottom")&&i.end0&&(l+=''),l+='';for(var d=n.start;d',r>0&&(l+=''),e.innerHTML=l,e.scrollTop=o,h.observer){var u=e.querySelectorAll(".leaderboard-sentinel");u.forEach(function(m){h.observer.observe(m)})}}}function Me(e){if(!e)return"";if(e.separator)return'...
';var t=e.name||"Unknown",n=e.rank||0,a=e.score||0,i=n<=3?"is-top-"+n:"",r=e.is_current?"is-current":"",o="";e.rank_change>0||e._rankChange==="up"?o="leaderboard-entry--climbing leaderboard-entry--slide-up":(e.rank_change<0||e._rankChange==="down")&&(o="leaderboard-entry--falling leaderboard-entry--slide-down");var l="";e.rank_change>0?l='\u25B2'+e.rank_change+"":e.rank_change<0&&(l='\u25BC'+Math.abs(e.rank_change)+"");var d="";if(e.streak>=2){var u=e.streak>=5?"streak-indicator--hot":"";d='\u{1F525}'+e.streak+""}var m=e.connected===!1?"leaderboard-entry--disconnected":"",c=e.connected===!1?'(away)':"",f=e._displayScore!==void 0?e._displayScore:a;return'#'+n+''+x(t)+c+''+d+l+''+f+"
"}function Re(e,t){for(var n=X,a=h.listEl&&h.listEl.clientHeight||n.DEFAULT_VIEWPORT_HEIGHT,i=Math.ceil(a/n.ENTRY_HEIGHT),r=n.VISIBLE_BUFFER,o=-1,l=0;l=e.length-i?(d=Math.max(0,e.length-i-r),u=e.length):(d=Math.max(0,o-Math.floor(i/2)-r),u=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:d,end:u}}function ft(){h.observer&&(h.observer.disconnect(),h.observer=null),h.isLazyEnabled=!1,h.fullData=[]}function vt(){var e;function t(){clearTimeout(e),e=setTimeout(function(){h.isLazyEnabled&&h.fullData.length>0&&(h.visibleRange=Re(h.fullData,s.playerName),re())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function gt(){var e=document.getElementById("qr-share-area");if(!(!e||e.tagName!=="DETAILS")){var t="beatify_qr_expanded",n=768,a=sessionStorage.getItem(t);a!==null?e.open=a==="true":e.open=window.innerWidth>=n,e.addEventListener("toggle",function(){sessionStorage.setItem(t,e.open.toString())})}}function pt(){var e=document.querySelectorAll(".lobby-container--compact .section-header-collapsible");e.forEach(function(t){t.addEventListener("click",function(){var n=t.closest(".section-collapsible");if(n){var a=n.classList.contains("collapsed");n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",a?"true":"false")}})})}var yt={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},g={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function bt(e){if(e){g.container=e;var t=!1;g.scrollHandler=function(){g.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){Te(),t=!1}),t=!0)};var n;g.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){g.isVirtual&&Te()},100)},e.addEventListener("scroll",g.scrollHandler,{passive:!0}),window.addEventListener("resize",g.resizeHandler)}}function ht(e,t){g.items=e,g.renderItem=t;var n=g.container;if(n){var a=n.scrollTop,i=g.isVirtual;e.length0&&(n.scrollTop=a,g.scrollTop=a)}}function Rn(){var e=g.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",g.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",g.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",g.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function Te(){var e=yt,t=g.items,n=g.container,a=g.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=g.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,d=Math.max(0,Math.floor(r/o)-l),u=Math.min(t.length,Math.ceil((r+i)/o)+l);g.topSpacer&&(g.topSpacer.style.height=d*o+"px"),g.bottomSpacer&&(g.bottomSpacer.style.height=(t.length-u)*o+"px");for(var m="",c=d;c"u"){console.warn("[Confetti] Library not loaded");return}O();var t=K.getQualitySettings(),n=t.confettiParticles;if(n===0){st();return}var a=K.getDeviceTier(),i=a==="low"?.5:a==="medium"?.75:1;switch(e=e||"exact",e){case"exact":var r=Math.round(2e3*i),o=Date.now()+r;(function v(){confetti({particleCount:n,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()0);var l=e.slice().sort(function(c,f){return c.connected!==f.connected?c.connected?-1:1:0}),d=wt.map(function(c){return c.name}),u=l.filter(function(c){return d.indexOf(c.name)===-1}).map(function(c){return c.name});g.container||bt(t);var m=function(c){var f=u.indexOf(c.name)!==-1,v=c.name===s.playerName,w=c.connected===!1,L=["player-card",f?"is-new":"",v?"player-card--you":"",w?"player-card--disconnected":""].filter(Boolean).join(" "),b=w?'(away)':"";return''+x(c.name)+(v?''+U.t("leaderboard.you")+"":"")+b+"
"};ht(l,m),setTimeout(function(){var c=g.isVirtual?g.contentWrapper:t;if(c)for(var f=c.querySelectorAll(".is-new"),v=0;vQR code library not loaded',t.onclick=Lt,t.onkeydown=function(n){(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),Lt())})}}function Lt(){if(P){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:P,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function Oe(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function Bt(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",Oe),n&&n.addEventListener("click",Oe),document.addEventListener("keydown",function(a){a.key==="Escape"&&e&&!e.classList.contains("hidden")&&Oe()})}function Pn(){if(P){var e=document.getElementById("invite-modal"),t=document.getElementById("invite-modal-code"),n=document.getElementById("invite-modal-url");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:P,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',n&&(n.value=P),e.classList.remove("hidden"),document.body.style.overflow="hidden";var a=document.getElementById("invite-modal-close");a&&a.focus()}}}function oe(){var e=document.getElementById("invite-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="");var t=document.getElementById("invite-copy-feedback");t&&t.classList.add("hidden")}function Hn(){var e=document.getElementById("invite-modal-url"),t=document.getElementById("invite-copy-feedback");!e||!P||(navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(P).then(function(){xt(t)}).catch(function(){St(e,t)}):St(e,t))}function St(e,t){e.select(),e.setSelectionRange(0,99999);try{document.execCommand("copy"),xt(t)}catch(n){console.warn("[Beatify] Copy failed:",n)}}function xt(e){e&&(e.classList.remove("hidden"),setTimeout(function(){e.classList.add("hidden")},2e3))}function Ct(){var e=document.getElementById("invite-modal"),t=e?e.querySelector(".invite-modal-backdrop"):null,n=document.getElementById("invite-modal-close"),a=document.getElementById("invite-players-btn"),i=document.getElementById("invite-copy-btn");t&&t.addEventListener("click",oe),n&&n.addEventListener("click",oe),a&&a.addEventListener("click",Pn),i&&i.addEventListener("click",Hn),document.addEventListener("keydown",function(r){r.key==="Escape"&&e&&!e.classList.contains("hidden")&&oe()})}function _t(e){var t=document.getElementById("admin-controls"),n=document.getElementById("lobby-status");if(t){(!e||!Array.isArray(e))&&(e=[]);var a=e.find(function(r){return r.name===s.playerName}),i=a?.is_admin===!0;i?(t.classList.remove("hidden"),n&&n.classList.add("hidden")):(t.classList.add("hidden"),n&&n.classList.remove("hidden"))}}function kt(){var e=document.getElementById("start-game-btn");e?.addEventListener("click",function(){!s.ws||s.ws.readyState!==WebSocket.OPEN||(e.disabled=!0,e.textContent=U.t("game.starting"),s.ws.send(JSON.stringify({type:"admin",action:"start_game"})))})}function At(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=U.t("player.welcomeBack",{name:e}),t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},2e3))}function Tt(){var e=document.getElementById("volume-indicator");e&&(e.textContent=U.t("earlyReveal.message")||"All guesses in!",e.classList.remove("hidden"),e.classList.add("is-visible"),setTimeout(function(){e.classList.remove("is-visible"),setTimeout(function(){e.classList.add("hidden")},300)},1500))}var y=window.BeatifyUtils||{},Ee=null;function Rt(e){Y();var t=document.getElementById("timer");if(!t)return;t.classList.remove("timer--warning","timer--critical");function n(){var a=Date.now(),i=Math.max(0,Math.ceil((e-a)/1e3));t.textContent=i,i<=5?(t.classList.remove("timer--warning"),t.classList.add("timer--critical")):i<=10?(t.classList.remove("timer--critical"),t.classList.add("timer--warning")):t.classList.remove("timer--warning","timer--critical"),i===10?t.setAttribute("aria-label","10 seconds remaining"):i===5?t.setAttribute("aria-label","5 seconds!"):i===0?t.setAttribute("aria-label","Time is up!"):t.setAttribute("aria-label","Time remaining: "+i+" seconds"),i<=0&&Y()}n(),Ee=setInterval(n,1e3)}function Y(){Ee&&(clearInterval(Ee),Ee=null)}function Ot(e){var t=document.getElementById("current-round"),n=document.getElementById("total-rounds"),a=document.getElementById("last-round-banner");t&&(t.textContent=e.round||1),n&&(n.textContent=e.total_rounds||10),a&&(e.last_round?a.classList.remove("hidden"):a.classList.add("hidden"));var i=document.getElementById("closest-wins-badge");i&&(e.closest_wins_mode?i.classList.remove("hidden"):i.classList.add("hidden"));var r=document.getElementById("intro-badge"),o=document.getElementById("intro-splash");if(r)if(e.is_intro_round){r.classList.remove("hidden");var l=r.querySelector("[data-i18n]");e.intro_stopped?(r.classList.add("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=y.t("game.introStopped")||"Intro complete!")):(r.classList.remove("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introRound"),l.textContent=y.t("game.introRound")||"INTRO ROUND"),o&&!o._shown&&(o._shown=!0,o.classList.remove("hidden"),setTimeout(function(){o.classList.add("hidden")},2e3)))}else r.classList.add("hidden"),r.classList.remove("intro-badge--stopped"),o&&(o.classList.add("hidden"),o._shown=!1);var d=document.getElementById("album-cover"),u=document.getElementById("album-loading");if(d&&e.song){u&&u.classList.remove("hidden");var m=e.song.album_art||"/beatify/static/img/no-artwork.svg";d.onload=function(){u&&u.classList.add("hidden")},d.onerror=function(){d.src="/beatify/static/img/no-artwork.svg",u&&u.classList.add("hidden")},d.src=m}Dn(e.players),e.leaderboard&&Ue(e,"leaderboard-list"),$n(e.players),e.artist_challenge!==void 0&&Yn(e.artist_challenge,"PLAYING"),e.movie_challenge!==void 0&&Kn(e.movie_challenge,"PLAYING")}function Pt(e){if(e){var t=document.getElementById("album-cover"),n=document.getElementById("album-loading");if(t&&e.album_art){var a=e.album_art;if(t.src===a)return;t.style.transition="opacity 0.3s ease-in-out",t.style.opacity="0.5";var i=new Image;i.onload=function(){t.src=a,t.style.opacity="1",n&&n.classList.add("hidden")},i.onerror=function(){t.src="/beatify/static/img/no-artwork.svg",t.style.opacity="1",n&&n.classList.add("hidden")},i.src=a}console.log("[Metadata] Updated:",e.artist,"-",e.title)}}function Gn(e){if(!e)return"?";var t=e.trim();if(!t)return"?";var n=t.split(/[\s-]+/).filter(Boolean);return n.length>=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,Math.min(2,t.length)).toUpperCase()}function Dn(e){var t=document.getElementById("submission-tracker"),n=document.getElementById("submitted-players");if(!(!t||!n)){var a=e||[],i=a.filter(function(l){return l.submitted}).length,r=a.length,o=i===r&&r>0;t.classList.toggle("all-submitted",o),n.innerHTML=a.map(function(l){var d=Gn(l.name),u=l.name===s.playerName,m=l.connected===!1,c=["player-indicator",l.submitted?"is-submitted":"",u?"is-current-player":"",m?"player-indicator--disconnected":""].filter(Boolean).join(" "),f="";return l.steal_used&&(f+='\u{1F977}'),l.bet&&(f+='\u{1F3B2}'),''+f+'
'+x(d)+'
'+x(l.name)+" "}).join("")}}function Ue(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&<(),o=r?ct(a):{};a.forEach(function(c){c.is_current=c.name===s.playerName;var f=o[c.name];f&&(c._rankChange=f);var v=M.players[c.name],w=v?v.score:c.score;c._prevScore=w,c._displayScore=n?w:c.score});var l=Vn(a,s.playerName),d=a.length>=X.MIN_PLAYERS_FOR_LAZY;if(d)h.observer||mt(i),h.fullData=l,h.isLazyEnabled=!0,h.listEl=i,h.visibleRange=Re(l,s.playerName),re();else{h.isLazyEnabled=!1;var u="";l.forEach(function(c){u+=Me(c)}),i.innerHTML=u}var m=[];r&&l.forEach(function(c){!c.separator&&c._prevScore!==c.score&&m.push({name:c.name,prevScore:c._prevScore,newScore:c.score})}),r&&m.length>0&&requestAnimationFrame(function(){for(var c={},f=i.querySelectorAll(".leaderboard-entry[data-name]"),v=0;v8&&Fn(i),Wn(a),jn(a),ut(e.players||[],a)}}function Vn(e,t){if(e.length<=10)return e;for(var n=e.slice(0,5),a=e.slice(-3),i=-1,r=0;r=e.length-3?[].concat(n,[{separator:!0}],a):[].concat(n,[{separator:!0}],[e[i]],[{separator:!0}],a)}function Fn(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Wn(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=y.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function Ht(){var e=document.getElementById("leaderboard-toggle"),t=document.getElementById("game-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function jn(e,t){var n=t?[t]:["leaderboard-summary","reveal-leaderboard-summary"];n.forEach(function(a){var i=document.getElementById(a);if(!(!i||!e||e.length===0)){var r=e[0];r&&(i.textContent=r.name+": "+r.score)}})}function Gt(){var e=document.getElementById("reveal-leaderboard-toggle"),t=document.getElementById("reveal-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function Dt(){var e=document.getElementById("round-analytics-toggle"),t=document.getElementById("round-analytics");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}var G=!1,le=!1,de=!1,z=!1,H=null,we=null,qn=300,Nt=0,te=!1,ne=null,Un=500,Mt=0;function Vt(){var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!(!e||!t)){e.addEventListener("input",function(){t.textContent=this.value});var n=document.getElementById("bet-toggle");n&&n.addEventListener("click",function(){G||(le=!le,n.classList.toggle("is-active",le))});var a=document.getElementById("submit-btn");a&&a.addEventListener("click",zn);var i=document.getElementById("steal-btn");i&&i.addEventListener("click",ea);var r=document.getElementById("steal-modal-close");r&&r.addEventListener("click",je);var o=document.getElementById("steal-modal");if(o){var l=o.querySelector(".steal-modal-backdrop");l&&l.addEventListener("click",je)}}}function zn(){if(!G){var e=document.getElementById("year-slider"),t=document.getElementById("submit-btn");if(!(!e||!t)){var n=parseInt(e.value,10);t.disabled=!0,t.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"submit",year:n,bet:le})):(We(y.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function ze(){G=!0;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("bet-toggle");e&&e.classList.add("is-submitted"),t&&t.classList.add("hidden"),a&&a.classList.add("hidden"),n&&n.classList.remove("hidden")}function Ft(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(We(y.t("errors.timesUp")),G=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?ze():We(e.message||"Submission failed")}function We(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=y.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function Wt(){G=!1,le=!1;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("year-slider"),i=document.getElementById("bet-toggle");if(e&&e.classList.remove("is-submitted"),t&&(t.disabled=!1,t.classList.remove("hidden","is-loading","is-error"),t.textContent=y.t("game.submitGuess")),i&&i.classList.remove("hidden","is-active"),n&&n.classList.add("hidden"),a){a.value=1990;var r=document.getElementById("selected-year");r&&(r.textContent="1990")}de=!1,Ye(),Jn(),Zn()}function Yn(e,t){var n=document.getElementById("artist-challenge-container");if(n){if(!e||!e.options){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("artist-options"),i=document.getElementById("artist-result"),r=Array.from(a.querySelectorAll(".artist-option-btn")).map(function(m){return m.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(m,c){var f=document.createElement("button");f.className="artist-option-btn",f.dataset.artist=m,f.dataset.index=c,f.textContent=m,f.addEventListener("click",function(){Qn(m)}),a.appendChild(f)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(m){m.classList.add("is-disabled"),m.classList.remove("is-loading","is-wrong");var c=e.correct_artist||we;c&&m.dataset.artist===c&&m.classList.add("is-winner")}),e.winner===s.playerName){var d=e.bonus_points||5;i.textContent=(y.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",d),i.className="artist-result is-winner"}else{var u=(y.t("artistChallenge.someoneBeatYou")||"{winner} got it first!").replace("{winner}",e.winner);i.textContent=u,i.className="artist-result is-late"}i.classList.remove("hidden"),z=!0}else z||i.classList.add("hidden")}}function Qn(e){var t=Date.now();if(!(t-Nt0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}De();var a=(y.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Ve(a,!0),te=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),De(),Ve(y.t("movieChallenge.wrongGuess")||"Not quite...",!1),te=!0;ne=null}function De(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Ve(e,t){var n=document.getElementById("movie-result");n&&(n.textContent=e,n.className="movie-result "+(t?"is-winner":"is-late"),n.classList.remove("hidden"))}function Zn(){te=!1,ne=null;var e=document.getElementById("movie-challenge-container");e&&e.classList.add("hidden");var t=document.getElementById("movie-options");t&&(t.innerHTML="");var n=document.getElementById("movie-result");n&&(n.classList.add("hidden"),n.className="movie-result hidden")}function zt(e,t){var n=document.getElementById("movie-reveal-section");if(n){if(!e||!e.correct_movie){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("movie-reveal-name");a&&(a.textContent=e.correct_movie);var i=document.getElementById("movie-reveal-winners");if(i&&e.results){var r=e.results.winners||[];if(r.length>0){i.innerHTML="",i.classList.remove("hidden");var o=document.createElement("div");o.className="movie-reveal-winners-title",o.textContent=y.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(d){var u=document.createElement("div");u.className="movie-reveal-winner-entry",d.name===t?u.classList.add("is-you"):u.classList.add("is-other"),u.textContent=d.name+" \u2014 +"+d.bonus+" ("+d.time+"s)",i.appendChild(u)})}else{i.innerHTML="",i.classList.remove("hidden");var l=document.createElement("div");l.className="movie-reveal-no-winner",l.textContent=y.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}function $n(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){de=t.steal_available&&!G;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");de?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):Ye()}}}function Ye(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden")}function ea(){!de||G||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function ta(e){var t=document.getElementById("steal-modal"),n=document.getElementById("steal-target-list");if(!(!t||!n)){if(n.innerHTML="",!e||e.length===0){var a=document.createElement("p");a.className="steal-no-targets",a.textContent=y.t("steal.waitForSubmit"),n.appendChild(a)}else e.forEach(function(i){var r=document.createElement("button");r.className="steal-target-btn",r.textContent=i,r.addEventListener("click",function(){na(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function je(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function na(e){var t=y.t("steal.confirm").replace("{name}",e),n=await Z(y.t("steal.confirmTitle")||"Steal Answer?",t,y.t("steal.confirmButton")||"Steal",y.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),je())}function Yt(e){if(e.success){de=!1,G=!0,Ye();var t=document.getElementById("year-selector"),n=document.getElementById("submit-btn"),a=document.getElementById("submitted-confirmation");t&&t.classList.add("is-submitted"),n&&n.classList.add("hidden"),a&&a.classList.remove("hidden"),aa(e.target,e.year);var i=document.getElementById("selected-year"),r=document.getElementById("year-slider");i&&(i.textContent=e.year),r&&(r.value=e.year)}}function Qt(e){ta(e.targets||[])}function aa(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=y.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var Fe=!1,ia=500,ce=!1,Qe=.5;function Le(){return Fe?!1:(Fe=!0,setTimeout(function(){Fe=!1},ia),!0)}function Je(){if(s.isAdmin){var e=document.getElementById("admin-control-bar");e&&(e.classList.remove("hidden"),document.body.classList.add("has-control-bar"))}}function ue(){var e=document.getElementById("admin-control-bar");e&&(e.classList.add("hidden"),document.body.classList.remove("has-control-bar"))}function Jt(){var e=document.getElementById("reaction-bar");e&&e.classList.remove("hidden")}function me(){var e=document.getElementById("reaction-bar");e&&e.classList.add("hidden")}function ra(e){s.hasReactedThisPhase||(s.hasReactedThisPhase=!0,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"reaction",emoji:e})))}function Kt(){var e=document.getElementById("reaction-bar");if(e){var t=e.querySelectorAll(".reaction-btn");t.forEach(function(n){n.addEventListener("click",function(){var a=n.getAttribute("data-emoji");a&&ra(a)})})}}function Xt(e,t){var n=document.getElementById("reaction-container");if(n){var a=document.createElement("div");a.className="reaction-bubble",a.textContent=e+" "+t,a.style.left=20+Math.random()*60+"%",n.appendChild(a),setTimeout(function(){a.remove()},3e3)}}function Se(e){var t=document.getElementById("stop-song-btn"),n=document.getElementById("next-round-admin-btn");if(e==="PLAYING"){if(Ke(),t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.skip"))}}else if(e==="REVEAL"){if(t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}else if(n){n.classList.add("is-disabled"),n.disabled=!0;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}function sa(){if(!ce&&Le()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){console.warn("[Beatify] Cannot stop song: WebSocket not connected");return}var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-label");t&&(t.textContent=y.t("game.stopping"))}s.ws.send(JSON.stringify({type:"admin",action:"stop_song"}))}}function oa(){if(Qe>=1){Zt("max");return}Le()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function la(){if(Qe<=0){Zt("min");return}Le()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function Zt(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=e==="max"?"\u{1F50A} Max":"\u{1F507} Min",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1e3))}async function da(){var e=await Z(y.t("admin.endGameConfirm")||"End Game?",y.t("admin.endGameWarning")||"All players will be disconnected.",y.t("admin.endGame")||"End Game",y.t("common.cancel"));if(e&&Le()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(y.t("errors.CONNECTION_LOST"));return}var t=document.getElementById("end-game-btn");if(t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var qe=!1;function $t(){if(!qe&&s.ws&&s.ws.readyState===WebSocket.OPEN){qe=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=y.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"}))}}function en(){qe=!1}function ca(){$t()}function tn(){var e=document.getElementById("stop-song-btn"),t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn"),a=document.getElementById("next-round-admin-btn"),i=document.getElementById("end-game-btn");e&&e.addEventListener("click",sa),t&&t.addEventListener("click",oa),n&&n.addEventListener("click",la),a&&a.addEventListener("click",ca),i&&i.addEventListener("click",da)}function nn(){ce=!0;var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-stopped"),e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u2713"),n&&(n.textContent=y.t("game.stopped"))}}function Ke(){ce=!1;var e=document.getElementById("stop-song-btn");if(e){e.classList.remove("is-stopped"),e.classList.remove("is-disabled"),e.disabled=!1;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u23F9\uFE0F"),n&&(n.textContent=y.t("game.stop"))}}function an(e){Qe=e,ua(e),ma(e)}function ua(e){var t=document.getElementById("volume-indicator");if(t){var n=Math.round(e*100);t.textContent="\u{1F50A} "+n+"%",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1500)}}function ma(e){var t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn");t&&t.classList.toggle("is-at-limit",e>=1),n&&n.classList.toggle("is-at-limit",e<=0)}function rn(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",$t);var t=document.getElementById("reveal-view");t&&t.addEventListener("click",function(n){n.target.tagName==="BUTTON"||n.target.closest("button")||($.isRunning()&&$.skipAll(),O())})}function sn(e){var t=document.getElementById("intro-splash-modal");if(t){t.classList.remove("hidden");var n=document.getElementById("intro-splash-confirm-btn"),a=t.querySelector(".intro-splash-modal-waiting");n&&(e?(n.classList.remove("hidden"),a&&a.classList.add("hidden"),n.onclick=function(){s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"admin",action:"confirm_intro_splash"}))}):(n.classList.add("hidden"),a&&a.classList.remove("hidden")))}}function on(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var p=window.BeatifyUtils||{};function Xe(e){var t=e.song||{},n=e.players||[],a=document.getElementById("reveal-round"),i=document.getElementById("reveal-total");a&&(a.textContent=e.round||1),i&&(i.textContent=e.total_rounds||10);var r=document.getElementById("closest-wins-badge");r&&(e.closest_wins_mode?r.classList.remove("hidden"):r.classList.add("hidden"));var o=document.getElementById("intro-badge");if(o)if(e.is_intro_round){o.classList.remove("hidden"),o.classList.add("intro-badge--stopped");var l=o.querySelector("[data-i18n]");l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=p.t("game.introStopped")||"Intro complete!")}else o.classList.add("hidden");var d=document.getElementById("reveal-album-cover");d&&(d.src=t.album_art||"/beatify/static/img/no-artwork.svg");var u=document.getElementById("correct-year");u&&(u.textContent=t.year||"????");var m=document.getElementById("song-title"),c=document.getElementById("song-artist");m&&(m.textContent=t.title||"Unknown Song"),c&&(c.textContent=t.artist||"Unknown Artist");var f=document.getElementById("fun-fact-container"),v=document.getElementById("fun-fact"),w=f?f.querySelector(".fun-fact-header"):null,L=p.getLocalizedSongField(t,"fun_fact");if(v&&(v.textContent=L||""),w&&(w.style.display=L?"flex":"none"),pa(t),ga(e.song_difficulty),f){var b=document.getElementById("song-rich-info"),E=b&&b.innerHTML.trim()!=="",S=L&&L.trim()!=="";f.classList.toggle("hidden",!S&&!E)}for(var I=null,_=0;_'+p.t("analytics.noSubmissions")+"",n.classList.remove("hidden");return}var i="";if(e.average_guess!==null&&t){var r=Math.round(e.average_guess-t);r===0?i=p.t("analytics.onTarget"):r>0?i=p.t("analytics.yearsLate",{years:r}):i=p.t("analytics.yearsEarly",{years:Math.abs(r)})}var o=va(e.all_guesses,t),l="";if(e.exact_match_players&&e.exact_match_players.length>0&&(l+='🎯'+p.t("analytics.exactMatches")+':'+e.exact_match_players.map(x).join(", ")+"
"),e.speed_champion&&e.speed_champion.names){var d=e.speed_champion.names.map(x).join(", ");l+='⚡'+p.t("analytics.speedChampion")+':'+d+'('+e.speed_champion.time+"s)
"}if(e.furthest_players&&e.furthest_players.length>0&&e.all_guesses&&e.all_guesses.length>0){var u=e.all_guesses[e.all_guesses.length-1].years_off;u>0&&(l+='😅'+p.t("analytics.furthestGuess")+':'+e.furthest_players.map(x).join(", ")+'('+u+" years)
")}var m=e.average_guess!==null?Math.round(e.average_guess):"?";a.innerHTML=''+p.t("analytics.averageGuess")+''+m+'
'+e.accuracy_percentage+'%'+p.t("analytics.accuracy",{percent:""}).replace("%","")+'
'+i+'
'+p.t("analytics.histogram")+"
"+o+""+(l?''+l+"
":""),n.classList.remove("hidden")}function va(e,t){var n=7;if(!e||e.length===0)return''+p.t("analytics.noGuesses")+"
";for(var a=e.map(function(ae){return ae.guess}),i=Math.min.apply(null,a),r=Math.max.apply(null,a),o=r-i,l=Math.max(1,Math.ceil(o/n)),d=l*n,u=d-o-1,m=i-Math.floor(u/2),c=[],f=0;f=v&&t<=w})}for(var L=0;L=c[E].start&&b<=c[E].end){c[E].count++;break}for(var S=1,I=0;IS&&(S=c[I].count);for(var _="",C=0;C0?Math.max(R,10):0,F=k.count>0?''+k.count+"":"",ke=l===1?String(k.start):k.start+"-"+String(k.end).slice(-2);_+='"}return''+_+"
"}function ga(e){var t=document.getElementById("song-difficulty");if(t){if(!e){t.classList.add("hidden");return}for(var n="",a=0;a★';t.innerHTML=''+n+'
'+p.t("difficulty."+e.label)+''+e.accuracy+"% "+p.t("difficulty.accuracy")+"",t.classList.remove("hidden")}}function pa(e){var t=document.getElementById("song-rich-info");if(t){var n=[],a=ya(e.chart_info||{});a.length>0&&(n=n.concat(a));var i=ba(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=p.getLocalizedSongField(e,"awards")||[],o=wa(r);o.length>0&&(n=n.concat(o)),n.length>0?t.innerHTML=''+n.join("")+"
":t.innerHTML=""}}function ya(e){if(!e)return[];var t=[];if(e.billboard_peak&&e.billboard_peak>0){var n=e.weeks_on_chart?' \xB7 '+e.weeks_on_chart+" "+p.t("reveal.weeksShort")+"":"";t.push('\u{1F4CA}#'+e.billboard_peak+" "+p.t("reveal.chartBillboard")+n+"")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.german_peak+" "+p.t("reveal.chartGerman")+""),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.uk_peak+" "+p.t("reveal.chartUK")+""),t}function ba(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+""+x(a)+"")}return t}function ha(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"song-badge--diamond":t.indexOf("platinum")!==-1?"song-badge--platinum":t.indexOf("gold")!==-1?"song-badge--gold":"song-badge--platinum"}function Ea(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"\u{1F48E}":t.indexOf("platinum")!==-1?"\u{1F4BF}":t.indexOf("gold")!==-1?"\u{1F947}":"\u{1F4BF}"}function wa(e){if(!e||e.length===0)return[];for(var t=[],n=e.slice(0,3),a=0;a'+o+""+x(i)+"")}return e.length>3&&t.push('+'+(e.length-3)+" more"),t}function La(e){var t=e.toLowerCase();return t.indexOf("grammy")!==-1?"song-badge--grammy":t.indexOf("eurovision")!==-1?"song-badge--eurovision":t.indexOf("oscar")!==-1||t.indexOf("academy award")!==-1?"song-badge--oscar":t.indexOf("hall of fame")!==-1?"song-badge--halloffame":"song-badge--award"}function Sa(e){var t=e.toLowerCase();return t.indexOf("eurovision")!==-1?"\u{1F3A4}":t.indexOf("grammy")!==-1?"\u{1F3C6}":t.indexOf("hall of fame")!==-1?"\u2B50":"\u{1F3C6}"}function Ia(e,t){var n=document.getElementById("reveal-emotion"),a=document.getElementById("personal-result");if(!n)return;var i=n.classList.contains("reveal-emotion-inline")||document.querySelector(".reveal-container--compact");n.className=i?"reveal-emotion-inline":"reveal-emotion",n.innerHTML="",n.classList.add("hidden"),a&&a.classList.remove("is-delayed"),O();var r=p.t("reveal.emotions");function o(v){return v[Math.floor(Math.random()*v.length)]}function l(v){return v===1?p.t("reveal.offByYear"):p.t("reveal.offByYears",{years:v})}var d="missed",u=o(r.missed),m=o(r.missedSub);if(e&&!e.missed_round){var c=e.years_off||0;c===0?(d="exact",u=o(r.exact),m=o(r.exactSub)):c<=2?(d="close",u=o(r.close),m=o(r.closeSub)+" "+l(c)):c<=5?(d="close",u=o(r.close),m=l(c)):(d="wrong",u=o(r.wrong),m=o(r.wrongSub)+" "+l(c))}else e&&e.missed_round&&(d="missed",u=o(r.missed),m=o(r.missedSub));var f=''+u+"";m&&(f+=''+m+"
"),n.innerHTML=f,n.classList.add("reveal-emotion--"+d),n.classList.remove("hidden"),d==="exact"&&ee(),a&&d!=="missed"&&a.classList.add("is-delayed")}function Ba(e,t){var n=document.getElementById("result-content");if(n){if(!e){n.innerHTML=''+p.t("reveal.playerNotFound")+"
";return}if(e.missed_round){var a='\u23F0
'+p.t("reveal.noSubmission")+"
",i=e.previous_streak||0;i>=2&&(a+='\u{1F494}Lost '+i+"-streak!
"),a+='0 pts
',n.innerHTML=a;return}var r=e.years_off||0,o=r===0?p.t("reveal.exact"):r===1?p.t("reveal.yearOff",{years:1}):p.t("reveal.yearsOff",{years:r}),l=r===0?"is-exact":r<=3?"is-close":"is-far",d=e.speed_multiplier||1,u=e.base_score||0,m=d>1,c=e.streak_bonus||0,f=e.artist_bonus||0,v="";m&&u>0&&(v=''+p.t("reveal.baseScore")+''+u+' pts
'+p.t("reveal.speedBonus")+''+d.toFixed(2)+"x
");var w="";e.bet_outcome==="won"?w='\u{1F3B2} '+p.t("reveal.betWon").replace("! 2x points","")+'2x
':e.bet_outcome==="lost"&&(w='\u{1F3B2} '+p.t("reveal.betLost")+'-
');var L="";c>0&&(L=''+e.streak+'-streak bonus!+'+c+" pts
");var b="";f>0&&(b='\u{1F3A4} '+(p.t("artistChallenge.artistBonus")||"Artist Bonus")+'+'+f+" pts
");var E=e.round_score+c+f,S=c>0||f>0,I=e.round_score>=20,_=M.players[e.name],C=_?_.score:e.score-E,k=_?_.streak:0,R=dt(k,e.streak||0);n.innerHTML=''+p.t("reveal.yourGuess")+''+(e.guess||"n/a")+'
'+p.t("reveal.correctYear")+''+t+'
'+p.t("reveal.accuracy")+''+o+"
"+v+w+'+0 pts
'+L+b+(S?''+p.t("reveal.total")+': +0 pts
':"");var D=n.querySelector(".score-value");D&&(ot(D,0,e.round_score,{betWon:e.bet_outcome==="won",betLost:e.bet_outcome==="lost",streakMilestone:R,isBigScore:I}),e.bet_outcome==="won"&&e.round_score>0&&setTimeout(function(){var T=document.getElementById("personal-result-score");T&&Ne(T,e.round_score,{isBetWin:!0})},200));var V=n.querySelector(".total-value");V&&S&&(setTimeout(function(){se(V,0,E,600)},300),R&&setTimeout(function(){var T=n.querySelector(".result-total");if(T){var F={3:20,5:50,10:100}[R]||0;Ne(T,F,{isStreak:!0,text:"+"+F+" "+R+"-Streak!"})}},500))}}function xa(e,t){var n=document.getElementById("reveal-results-cards");if(n){if(!e||e.length===0){n.innerHTML="";return}var a=null;t&&e.forEach(function(o){!o.missed_round&&o.years_off!=null&&(a===null||o.years_off';i.forEach(function(o){var l=o.name===s.playerName,d=o.missed_round===!0,u=o.years_off||0,m=o.round_score||0,c=d?"is-score-zero":m>=10?"is-score-high":m>=1?"is-score-medium":"is-score-zero",f=t&&!d&&a!==null&&(o.years_off||0)===a,v=f?" is-closest-winner":"",w=d?"\u2014":o.guess||"n/a",L=d?p.t("reveal.noGuessShort"):u===0?p.t("reveal.exact"):p.t("reveal.shortOff",{years:u}),b=o.bet?'\u{1F3B2}':"",E=f?'\u{1F3AF}':"",S="";o.artist_bonus&&o.artist_bonus>0&&(S='\u{1F3A4} +'+o.artist_bonus+"");var I="";if(o.stole_from)I='\u{1F977}'+p.t("steal.stolenFrom",{name:x(o.stole_from)})+"
";else if(o.was_stolen_by&&o.was_stolen_by.length>0){var _=o.was_stolen_by.map(x).join(", ");I='\u{1F3AF}'+p.t("steal.stolenBy",{name:_})+"
"}r+=''+x(o.name)+b+E+'
'+w+'
'+L+"
"+I+'
+'+m+S+"
"}),r+="",n.innerHTML=r}}var A=window.BeatifyUtils||{};function dn(e){window.scrollTo(0,0);var t=e.leaderboard||[];t.forEach(function(b){b.is_current=b.name===s.playerName}),[1,2,3].forEach(function(b){var E=t.find(function(_){return _.rank===b}),S=document.getElementById("podium-"+b+"-name"),I=document.getElementById("podium-"+b+"-score");S&&(S.textContent=E?x(E.name):"---"),I&&(I.textContent=E?E.score:"0")});var n=t.find(function(b){return b.is_current}),a=document.getElementById("your-final-rank"),i=document.getElementById("your-final-score"),r=document.getElementById("stat-best-streak"),o=document.getElementById("stat-rounds"),l=document.getElementById("stat-bets");n&&(a&&(a.textContent="#"+n.rank),i&&(i.textContent=n.score+" "+A.t("leaderboard.points")),r&&(r.textContent=n.best_streak||0),o&&(o.textContent=n.rounds_played||0),l&&(l.textContent=n.bets_won||0));var d=document.getElementById("final-leaderboard-list");d&&(d.innerHTML=t.map(function(b){var E=b.is_current?"is-current":"",S=b.connected===!1?"final-entry--disconnected":"",I=b.connected===!1?'(away)':"";return'#'+b.rank+''+x(b.name)+I+''+b.score+"
"}).join("")),Ca(e.superlatives),_a(e.highlights),ka(e.share_data);var u=document.getElementById("end-admin-controls"),m=document.getElementById("end-player-message");if(n&&n.is_admin){u&&u.classList.remove("hidden"),m&&m.classList.add("hidden");var c=document.getElementById("new-game-btn");c&&(c.onclick=Ta);var f=document.getElementById("player-rematch-btn");f&&(f.onclick=function(){f.disabled=!0;var b=f.textContent;f.textContent="\u23F3",fetch("/beatify/api/rematch-game",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"}}).then(function(E){if(!E.ok)return E.json().then(function(S){throw new Error(S.message||"Rematch failed")});f.textContent="\u23F3"}).catch(function(E){console.error("[Player] Rematch failed:",E),alert(E.message||"Failed to start rematch"),f.disabled=!1,f.textContent=b})})}else u&&u.classList.add("hidden"),m&&m.classList.remove("hidden");if(n){var v=e.total_rounds||10,w=n.best_streak||0,L=w===v&&v>0;L?ee("perfect"):n.rank===1&&ee("winner")}}function Ca(e){var t=document.getElementById("superlatives-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n="";e.forEach(function(a,i){var r="";switch(a.value_label){case"avg_time":r=a.value+"s "+A.t("superlatives.avgTime");break;case"streak":r=a.value+" "+A.t("superlatives.streak");break;case"bets":r=a.value+" "+A.t("superlatives.bets");break;case"points":r=a.value+" "+A.t("superlatives.points");break;case"close_guesses":r=a.value+" "+A.t("superlatives.closeGuesses");break;default:r=a.value}n+=''+a.emoji+'
'+A.t("superlatives."+a.title)+'
'+x(a.player_name)+'
'+r+"
"}),t.innerHTML=n,t.classList.remove("hidden")}}function _a(e){var t=document.getElementById("highlights-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n=document.getElementById("highlights-list");if(n){var a="";e.forEach(function(i,r){var o=A.t("highlights."+i.description,i.description_params)||i.description;o===i.description&&i.description_params&&(o=A.t("highlights."+i.description)||i.description,Object.keys(i.description_params).forEach(function(l){o=o.replace("{"+l+"}",x(i.description_params[l]))})),a+=''+(i.emoji||"\u2728")+'
'+o+'
'+A.t("highlights.roundLabel",{round:i.round})+"
"}),n.innerHTML=a,t.classList.remove("hidden")}}}function ka(e){var t=document.getElementById("share-container");if(t){if(!e||!e.emoji_grids){t.classList.add("hidden");return}var n=e.emoji_grids[s.playerName];if(!n){var a=Object.keys(e.emoji_grids);a.length===1&&(n=e.emoji_grids[a[0]])}if(!n){t.classList.add("hidden");return}var i=document.getElementById("share-emoji-grid");if(i){var r=n.split(`
+var Ae=window.BeatifyUtils||{},s={ws:null,playerName:null,isAdmin:!1,reconnectAttempts:0,isReconnecting:!1,intentionalLeave:!1,hasReactedThisPhase:!1,currentRoundNumber:0,gameId:new URLSearchParams(window.location.search).get("game"),connectWithSession:null,connectWebSocket:null},wn=document.getElementById("loading-view"),Ln=document.getElementById("not-found-view"),Sn=document.getElementById("ended-view"),In=document.getElementById("in-progress-view"),Bn=document.getElementById("join-view"),xn=document.getElementById("lobby-view"),Cn=document.getElementById("game-view"),_n=document.getElementById("reveal-view"),kn=document.getElementById("paused-view"),An=document.getElementById("end-view"),Tn=document.getElementById("connection-lost-view"),Nn=[wn,Ln,Sn,In,Bn,xn,Cn,_n,kn,An,Tn];function B(e){Ae.showView(Nn,e),(e==="join-view"||e==="loading-view"||e==="not-found-view"||e==="ended-view"||e==="in-progress-view"||e==="connection-lost-view")&&q("calm"),e==="join-view"&&setTimeout(function(){var t=document.getElementById("name-input");t&&t.focus()},100)}function Z(e,t,n,a){return new Promise(function(i){var r=document.getElementById("confirm-modal"),o=document.getElementById("confirm-modal-title"),l=document.getElementById("confirm-modal-message"),d=document.getElementById("confirm-modal-yes"),u=document.getElementById("confirm-modal-no");if(!r||!o||!l||!d||!u){i(confirm(t||e));return}o.textContent=e,l.textContent=t,d.textContent=n||Ae.t("common.confirm")||"Confirm",u.textContent=a||Ae.t("common.cancel")||"Cancel",r.classList.remove("hidden");function m(){r.classList.add("hidden"),d.removeEventListener("click",c),u.removeEventListener("click",f),v.removeEventListener("click",f)}function c(){m(),i(!0)}function f(){m(),i(!1)}var v=r.querySelector(".modal-backdrop");d.addEventListener("click",c),u.addEventListener("click",f),v&&v.addEventListener("click",f)})}function x(e){var t=document.createElement("div");return t.textContent=e,t.innerHTML}function be(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function Mn(e){return 1-Math.pow(1-e,4)}function se(e,t,n,a,i){if(be()||t===n)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var r=K.getQualitySettings();if(r.scoreDuration===0)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var o=Math.min(a,r.scoreDuration||a);i=i||Mn;var l=null,d=null,u=!1,m=n;function c(f){if(!u){l||(l=f);var v=f-l,w=Math.min(v/o,1),L=i(w),b=Math.round(t+(m-t)*L);e.textContent=b,w<1&&(d=requestAnimationFrame(c))}}return d=requestAnimationFrame(c),{cancel:function(){u=!0,d&&cancelAnimationFrame(d)},skipToEnd:function(){u=!0,d&&cancelAnimationFrame(d),e.textContent=m}}}function ot(e,t,n,a){a=a||{};var i=500;a.betWon?i=800:a.isBigScore?i=700:a.betLost&&(i=400),e.classList.add("score-animating");var r=null;a.betWon?r="score-glow-gold":a.betLost?(r="score-shake",e.classList.add("score-flash-red")):a.streakMilestone?r="score-burst":a.isBigScore&&(r="score-pop"),r&&!be()&&e.classList.add(r),se(e,t,n,i);function o(){e.classList.remove("score-animating"),r&&e.classList.remove(r),e.classList.remove("score-flash-red")}r&&!be()?e.addEventListener("animationend",function l(){e.removeEventListener("animationend",l),o()}):setTimeout(o,i+50)}function Ne(e,t,n){if(n=n||{},!be()){var a=document.createElement("div");a.className="points-popup",a.textContent=n.text||"+"+t,n.isStreak?a.classList.add("points-popup--streak"):n.isBetWin&&a.classList.add("points-popup--gold");var i=e.getBoundingClientRect();a.style.left=i.left+i.width/2+"px",a.style.top=i.top+"px",document.body.appendChild(a),a.addEventListener("animationend",function(){a.parentNode&&a.parentNode.removeChild(a)}),setTimeout(function(){a.parentNode&&a.parentNode.removeChild(a)},1200)}}var M={players:{},leaderboard:[],initialized:!1};function lt(){return M.initialized}var rt=[3,5,10,15,20,25];function dt(e,t){for(var n=0;n=a)return a}return null}function ct(e){var t=e.map(function(a){return a.name}),n={};return t.forEach(function(a,i){var r=M.leaderboard.indexOf(a);r===-1?n[a]="new":ir&&(n[a]="down")}),n}function ut(e,t){M.players={},e.forEach(function(n){M.players[n.name]={score:n.score,rank:n.rank||0,streak:n.streak||0}}),t&&(M.leaderboard=t.map(function(n){return n.name})),M.initialized=!0}var K=(function(){var e=window.matchMedia("(prefers-reduced-motion: reduce)"),t=e.matches;e.addEventListener("change",function(i){t=i.matches});var n=null;function a(){if(n!==null)return n;var i=navigator.hardwareConcurrency||2,r=navigator.deviceMemory||4,o=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;return i<=2||r<=2?n="low":i<=4||r<=4||o?n="medium":n="high",n}return a(),{prefersReducedMotion:function(){return t},getDeviceTier:a,getQualitySettings:function(){var i=a();if(t)return{confettiParticles:0,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!1};switch(i){case"low":return{confettiParticles:5,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!0};case"medium":return{confettiParticles:10,scoreDuration:300,leaderboardAnimation:"simplified",neonGlow:!1,enableAnimations:!0};default:return{confettiParticles:15,scoreDuration:500,leaderboardAnimation:"full",neonGlow:!0,enableAnimations:!0}}},ifMotionAllowed:function(i,r){t?r&&r():i()},withWillChange:function(i,r,o){i&&(i.style.willChange=r,setTimeout(function(){i&&i.style&&(i.style.willChange="auto")},(o||500)+100))}}})(),$=(function(){var e=[],t=!1,n=null,a=null,i=2e3;function r(){if(a&&(clearTimeout(a),a=null),e.length===0){t=!1,n=null;return}n=e.shift(),a=setTimeout(function(){n&&n.skipToEnd&&n.skipToEnd(),r()},i),n.run(function(){a&&(clearTimeout(a),a=null),r()})}return{add:function(o){e.push(o),t||(t=!0,r())},skipAll:function(){a&&(clearTimeout(a),a=null),n&&n.skipToEnd&&n.skipToEnd(),e.forEach(function(o){o.skipToEnd&&o.skipToEnd()}),e=[],t=!1,n=null},clear:function(){a&&(clearTimeout(a),a=null),e=[],t=!1,n=null},isRunning:function(){return t},getMaxDuration:function(){return i}}})(),X={VISIBLE_BUFFER:2,ENTRY_HEIGHT:48,MIN_PLAYERS_FOR_LAZY:10,ROOT_MARGIN:"96px 0px",DEFAULT_VIEWPORT_HEIGHT:280},h={observer:null,fullData:[],visibleRange:{start:0,end:10},listEl:null,isLazyEnabled:!1};function mt(e){e&&(h.observer&&h.listEl!==e&&(h.observer.disconnect(),h.observer=null),!h.observer&&(h.listEl=e,h.observer=new IntersectionObserver(function(t){t.forEach(function(n){if(!(!n.isIntersecting||!h.isLazyEnabled)){var a=h.fullData,i=h.visibleRange,r=X.VISIBLE_BUFFER;if(n.target.classList.contains("leaderboard-sentinel--top")){if(i.start>0){var o=Math.max(0,i.start-r);h.visibleRange.start=o,re()}}else if(n.target.classList.contains("leaderboard-sentinel--bottom")&&i.end0&&(l+=''),l+='';for(var d=n.start;d',r>0&&(l+=''),e.innerHTML=l,e.scrollTop=o,h.observer){var u=e.querySelectorAll(".leaderboard-sentinel");u.forEach(function(m){h.observer.observe(m)})}}}function Me(e){if(!e)return"";if(e.separator)return'...
';var t=e.name||"Unknown",n=e.rank||0,a=e.score||0,i=n<=3?"is-top-"+n:"",r=e.is_current?"is-current":"",o="";e.rank_change>0||e._rankChange==="up"?o="leaderboard-entry--climbing leaderboard-entry--slide-up":(e.rank_change<0||e._rankChange==="down")&&(o="leaderboard-entry--falling leaderboard-entry--slide-down");var l="";e.rank_change>0?l='\u25B2'+e.rank_change+"":e.rank_change<0&&(l='\u25BC'+Math.abs(e.rank_change)+"");var d="";if(e.streak>=2){var u=e.streak>=5?"streak-indicator--hot":"";d='\u{1F525}'+e.streak+""}var m=e.connected===!1?"leaderboard-entry--disconnected":"",c=e.connected===!1?'(away)':"",f=e._displayScore!==void 0?e._displayScore:a;return'#'+n+''+x(t)+c+''+d+l+''+f+"
"}function Re(e,t){for(var n=X,a=h.listEl&&h.listEl.clientHeight||n.DEFAULT_VIEWPORT_HEIGHT,i=Math.ceil(a/n.ENTRY_HEIGHT),r=n.VISIBLE_BUFFER,o=-1,l=0;l=e.length-i?(d=Math.max(0,e.length-i-r),u=e.length):(d=Math.max(0,o-Math.floor(i/2)-r),u=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:d,end:u}}function ft(){h.observer&&(h.observer.disconnect(),h.observer=null),h.isLazyEnabled=!1,h.fullData=[]}function vt(){var e;function t(){clearTimeout(e),e=setTimeout(function(){h.isLazyEnabled&&h.fullData.length>0&&(h.visibleRange=Re(h.fullData,s.playerName),re())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function gt(){var e=document.getElementById("qr-share-area");if(!(!e||e.tagName!=="DETAILS")){var t="beatify_qr_expanded",n=768,a=sessionStorage.getItem(t);a!==null?e.open=a==="true":e.open=window.innerWidth>=n,e.addEventListener("toggle",function(){sessionStorage.setItem(t,e.open.toString())})}}function pt(){var e=document.querySelectorAll(".lobby-container--compact .section-header-collapsible");e.forEach(function(t){t.addEventListener("click",function(){var n=t.closest(".section-collapsible");if(n){var a=n.classList.contains("collapsed");n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",a?"true":"false")}})})}var yt={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},g={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function bt(e){if(e){g.container=e;var t=!1;g.scrollHandler=function(){g.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){Te(),t=!1}),t=!0)};var n;g.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){g.isVirtual&&Te()},100)},e.addEventListener("scroll",g.scrollHandler,{passive:!0}),window.addEventListener("resize",g.resizeHandler)}}function ht(e,t){g.items=e,g.renderItem=t;var n=g.container;if(n){var a=n.scrollTop,i=g.isVirtual;e.length0&&(n.scrollTop=a,g.scrollTop=a)}}function Rn(){var e=g.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",g.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",g.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",g.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function Te(){var e=yt,t=g.items,n=g.container,a=g.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=g.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,d=Math.max(0,Math.floor(r/o)-l),u=Math.min(t.length,Math.ceil((r+i)/o)+l);g.topSpacer&&(g.topSpacer.style.height=d*o+"px"),g.bottomSpacer&&(g.bottomSpacer.style.height=(t.length-u)*o+"px");for(var m="",c=d;c"u"){console.warn("[Confetti] Library not loaded");return}O();var t=K.getQualitySettings(),n=t.confettiParticles;if(n===0){st();return}var a=K.getDeviceTier(),i=a==="low"?.5:a==="medium"?.75:1;switch(e=e||"exact",e){case"exact":var r=Math.round(2e3*i),o=Date.now()+r;(function v(){confetti({particleCount:n,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()0);var l=e.slice().sort(function(c,f){return c.connected!==f.connected?c.connected?-1:1:0}),d=wt.map(function(c){return c.name}),u=l.filter(function(c){return d.indexOf(c.name)===-1}).map(function(c){return c.name});g.container||bt(t);var m=function(c){var f=u.indexOf(c.name)!==-1,v=c.name===s.playerName,w=c.connected===!1,L=["player-card",f?"is-new":"",v?"player-card--you":"",w?"player-card--disconnected":""].filter(Boolean).join(" "),b=w?'(away)':"";return''+x(c.name)+(v?''+U.t("leaderboard.you")+"":"")+b+"
"};ht(l,m),setTimeout(function(){var c=g.isVirtual?g.contentWrapper:t;if(c)for(var f=c.querySelectorAll(".is-new"),v=0;vQR code library not loaded',t.onclick=Lt,t.onkeydown=function(n){(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),Lt())})}}function Lt(){if(P){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:P,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function Oe(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function Bt(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",Oe),n&&n.addEventListener("click",Oe),document.addEventListener("keydown",function(a){a.key==="Escape"&&e&&!e.classList.contains("hidden")&&Oe()})}function Pn(){if(P){var e=document.getElementById("invite-modal"),t=document.getElementById("invite-modal-code"),n=document.getElementById("invite-modal-url");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:P,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',n&&(n.value=P),e.classList.remove("hidden"),document.body.style.overflow="hidden";var a=document.getElementById("invite-modal-close");a&&a.focus()}}}function oe(){var e=document.getElementById("invite-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="");var t=document.getElementById("invite-copy-feedback");t&&t.classList.add("hidden")}function Hn(){var e=document.getElementById("invite-modal-url"),t=document.getElementById("invite-copy-feedback");!e||!P||(navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(P).then(function(){xt(t)}).catch(function(){St(e,t)}):St(e,t))}function St(e,t){e.select(),e.setSelectionRange(0,99999);try{document.execCommand("copy"),xt(t)}catch(n){console.warn("[Beatify] Copy failed:",n)}}function xt(e){e&&(e.classList.remove("hidden"),setTimeout(function(){e.classList.add("hidden")},2e3))}function Ct(){var e=document.getElementById("invite-modal"),t=e?e.querySelector(".invite-modal-backdrop"):null,n=document.getElementById("invite-modal-close"),a=document.getElementById("invite-players-btn"),i=document.getElementById("invite-copy-btn");t&&t.addEventListener("click",oe),n&&n.addEventListener("click",oe),a&&a.addEventListener("click",Pn),i&&i.addEventListener("click",Hn),document.addEventListener("keydown",function(r){r.key==="Escape"&&e&&!e.classList.contains("hidden")&&oe()})}function _t(e){var t=document.getElementById("admin-controls"),n=document.getElementById("lobby-status");if(t){(!e||!Array.isArray(e))&&(e=[]);var a=e.find(function(r){return r.name===s.playerName}),i=a?.is_admin===!0;i?(t.classList.remove("hidden"),n&&n.classList.add("hidden")):(t.classList.add("hidden"),n&&n.classList.remove("hidden"))}}function kt(){var e=document.getElementById("start-game-btn");e?.addEventListener("click",function(){!s.ws||s.ws.readyState!==WebSocket.OPEN||(e.disabled=!0,e.textContent=U.t("game.starting"),s.ws.send(JSON.stringify({type:"admin",action:"start_game"})))})}function At(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=U.t("player.welcomeBack",{name:e}),t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},2e3))}function Tt(){var e=document.getElementById("volume-indicator");e&&(e.textContent=U.t("earlyReveal.message")||"All guesses in!",e.classList.remove("hidden"),e.classList.add("is-visible"),setTimeout(function(){e.classList.remove("is-visible"),setTimeout(function(){e.classList.add("hidden")},300)},1500))}var y=window.BeatifyUtils||{},Ee=null;function Rt(e){Y();var t=document.getElementById("timer");if(!t)return;t.classList.remove("timer--warning","timer--critical");function n(){var a=Date.now(),i=Math.max(0,Math.ceil((e-a)/1e3));t.textContent=i,i<=5?(t.classList.remove("timer--warning"),t.classList.add("timer--critical")):i<=10?(t.classList.remove("timer--critical"),t.classList.add("timer--warning")):t.classList.remove("timer--warning","timer--critical"),i===10?t.setAttribute("aria-label","10 seconds remaining"):i===5?t.setAttribute("aria-label","5 seconds!"):i===0?t.setAttribute("aria-label","Time is up!"):t.setAttribute("aria-label","Time remaining: "+i+" seconds"),i<=0&&Y()}n(),Ee=setInterval(n,1e3)}function Y(){Ee&&(clearInterval(Ee),Ee=null)}function Ot(e){var t=document.getElementById("current-round"),n=document.getElementById("total-rounds"),a=document.getElementById("last-round-banner");t&&(t.textContent=e.round||1),n&&(n.textContent=e.total_rounds||10),a&&(e.last_round?a.classList.remove("hidden"):a.classList.add("hidden"));var i=document.getElementById("closest-wins-badge");i&&(e.closest_wins_mode?i.classList.remove("hidden"):i.classList.add("hidden"));var r=document.getElementById("intro-badge"),o=document.getElementById("intro-splash");if(r)if(e.is_intro_round){r.classList.remove("hidden");var l=r.querySelector("[data-i18n]");e.intro_stopped?(r.classList.add("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=y.t("game.introStopped")||"Intro complete!")):(r.classList.remove("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introRound"),l.textContent=y.t("game.introRound")||"INTRO ROUND"),o&&!o._shown&&(o._shown=!0,o.classList.remove("hidden"),setTimeout(function(){o.classList.add("hidden")},2e3)))}else r.classList.add("hidden"),r.classList.remove("intro-badge--stopped"),o&&(o.classList.add("hidden"),o._shown=!1);var d=document.getElementById("album-cover"),u=document.getElementById("album-loading");if(d&&e.song){u&&u.classList.remove("hidden");var m=e.song.album_art||"/beatify/static/img/no-artwork.svg";d.onload=function(){u&&u.classList.add("hidden")},d.onerror=function(){d.src="/beatify/static/img/no-artwork.svg",u&&u.classList.add("hidden")},d.src=m}Dn(e.players),e.leaderboard&&Ue(e,"leaderboard-list"),$n(e.players),e.artist_challenge!==void 0&&Yn(e.artist_challenge,"PLAYING"),e.movie_challenge!==void 0&&Kn(e.movie_challenge,"PLAYING")}function Pt(e){if(e){var t=document.getElementById("album-cover"),n=document.getElementById("album-loading");if(t&&e.album_art){var a=e.album_art;if(t.src===a)return;t.style.transition="opacity 0.3s ease-in-out",t.style.opacity="0.5";var i=new Image;i.onload=function(){t.src=a,t.style.opacity="1",n&&n.classList.add("hidden")},i.onerror=function(){t.src="/beatify/static/img/no-artwork.svg",t.style.opacity="1",n&&n.classList.add("hidden")},i.src=a}console.log("[Metadata] Updated:",e.artist,"-",e.title)}}function Gn(e){if(!e)return"?";var t=e.trim();if(!t)return"?";var n=t.split(/[\s-]+/).filter(Boolean);return n.length>=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,Math.min(2,t.length)).toUpperCase()}function Dn(e){var t=document.getElementById("submission-tracker"),n=document.getElementById("submitted-players");if(!(!t||!n)){var a=e||[],i=a.filter(function(l){return l.submitted}).length,r=a.length,o=i===r&&r>0;t.classList.toggle("all-submitted",o),n.innerHTML=a.map(function(l){var d=Gn(l.name),u=l.name===s.playerName,m=l.connected===!1,c=["player-indicator",l.submitted?"is-submitted":"",u?"is-current-player":"",m?"player-indicator--disconnected":""].filter(Boolean).join(" "),f="";return l.steal_used&&(f+='\u{1F977}'),l.bet&&(f+='\u{1F3B2}'),''+f+'
'+x(d)+'
'+x(l.name)+" "}).join("")}}function Ue(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&<(),o=r?ct(a):{};a.forEach(function(c){c.is_current=c.name===s.playerName;var f=o[c.name];f&&(c._rankChange=f);var v=M.players[c.name],w=v?v.score:c.score;c._prevScore=w,c._displayScore=n?w:c.score});var l=Vn(a,s.playerName),d=a.length>=X.MIN_PLAYERS_FOR_LAZY;if(d)h.observer||mt(i),h.fullData=l,h.isLazyEnabled=!0,h.listEl=i,h.visibleRange=Re(l,s.playerName),re();else{h.isLazyEnabled=!1;var u="";l.forEach(function(c){u+=Me(c)}),i.innerHTML=u}var m=[];r&&l.forEach(function(c){!c.separator&&c._prevScore!==c.score&&m.push({name:c.name,prevScore:c._prevScore,newScore:c.score})}),r&&m.length>0&&requestAnimationFrame(function(){for(var c={},f=i.querySelectorAll(".leaderboard-entry[data-name]"),v=0;v8&&Fn(i),Wn(a),jn(a),ut(e.players||[],a)}}function Vn(e,t){if(e.length<=10)return e;for(var n=e.slice(0,5),a=e.slice(-3),i=-1,r=0;r=e.length-3?[].concat(n,[{separator:!0}],a):[].concat(n,[{separator:!0}],[e[i]],[{separator:!0}],a)}function Fn(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Wn(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=y.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function Ht(){var e=document.getElementById("leaderboard-toggle"),t=document.getElementById("game-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function jn(e,t){var n=t?[t]:["leaderboard-summary","reveal-leaderboard-summary"];n.forEach(function(a){var i=document.getElementById(a);if(!(!i||!e||e.length===0)){var r=e[0];r&&(i.textContent=r.name+": "+r.score)}})}function Gt(){var e=document.getElementById("reveal-leaderboard-toggle"),t=document.getElementById("reveal-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function Dt(){var e=document.getElementById("round-analytics-toggle"),t=document.getElementById("round-analytics");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}var G=!1,le=!1,de=!1,z=!1,H=null,we=null,qn=300,Nt=0,te=!1,ne=null,Un=500,Mt=0;function Vt(){var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!(!e||!t)){e.addEventListener("input",function(){t.textContent=this.value});var n=document.getElementById("bet-toggle");n&&n.addEventListener("click",function(){G||(le=!le,n.classList.toggle("is-active",le))});var a=document.getElementById("submit-btn");a&&a.addEventListener("click",zn);var i=document.getElementById("steal-btn");i&&i.addEventListener("click",ea);var r=document.getElementById("steal-modal-close");r&&r.addEventListener("click",je);var o=document.getElementById("steal-modal");if(o){var l=o.querySelector(".steal-modal-backdrop");l&&l.addEventListener("click",je)}}}function zn(){if(!G){var e=document.getElementById("year-slider"),t=document.getElementById("submit-btn");if(!(!e||!t)){var n=parseInt(e.value,10);t.disabled=!0,t.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"submit",year:n,bet:le})):(We(y.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function ze(){G=!0;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("bet-toggle");e&&e.classList.add("is-submitted"),t&&t.classList.add("hidden"),a&&a.classList.add("hidden"),n&&n.classList.remove("hidden")}function Ft(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(We(y.t("errors.timesUp")),G=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?ze():We(e.message||"Submission failed")}function We(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=y.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function Wt(){G=!1,le=!1;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("year-slider"),i=document.getElementById("bet-toggle");if(e&&e.classList.remove("is-submitted"),t&&(t.disabled=!1,t.classList.remove("hidden","is-loading","is-error"),t.textContent=y.t("game.submitGuess")),i&&i.classList.remove("hidden","is-active"),n&&n.classList.add("hidden"),a){a.value=1990;var r=document.getElementById("selected-year");r&&(r.textContent="1990")}de=!1,Ye(),Jn(),Zn()}function Yn(e,t){var n=document.getElementById("artist-challenge-container");if(n){if(!e||!e.options){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("artist-options"),i=document.getElementById("artist-result"),r=Array.from(a.querySelectorAll(".artist-option-btn")).map(function(m){return m.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(m,c){var f=document.createElement("button");f.className="artist-option-btn",f.dataset.artist=m,f.dataset.index=c,f.textContent=m,f.addEventListener("click",function(){Qn(m)}),a.appendChild(f)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(m){m.classList.add("is-disabled"),m.classList.remove("is-loading","is-wrong");var c=e.correct_artist||we;c&&m.dataset.artist===c&&m.classList.add("is-winner")}),e.winner===s.playerName){var d=e.bonus_points||5;i.textContent=(y.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",d),i.className="artist-result is-winner"}else{var u=(y.t("artistChallenge.someoneBeatYou")||"{winner} got it first!").replace("{winner}",e.winner);i.textContent=u,i.className="artist-result is-late"}i.classList.remove("hidden"),z=!0}else z||i.classList.add("hidden")}}function Qn(e){var t=Date.now();if(!(t-Nt0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}De();var a=(y.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Ve(a,!0),te=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),De(),Ve(y.t("movieChallenge.wrongGuess")||"Not quite...",!1),te=!0;ne=null}function De(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Ve(e,t){var n=document.getElementById("movie-result");n&&(n.textContent=e,n.className="movie-result "+(t?"is-winner":"is-late"),n.classList.remove("hidden"))}function Zn(){te=!1,ne=null;var e=document.getElementById("movie-challenge-container");e&&e.classList.add("hidden");var t=document.getElementById("movie-options");t&&(t.innerHTML="");var n=document.getElementById("movie-result");n&&(n.classList.add("hidden"),n.className="movie-result hidden")}function zt(e,t){var n=document.getElementById("movie-reveal-section");if(n){if(!e||!e.correct_movie){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("movie-reveal-name");a&&(a.textContent=e.correct_movie);var i=document.getElementById("movie-reveal-winners");if(i&&e.results){var r=e.results.winners||[];if(r.length>0){i.innerHTML="",i.classList.remove("hidden");var o=document.createElement("div");o.className="movie-reveal-winners-title",o.textContent=y.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(d){var u=document.createElement("div");u.className="movie-reveal-winner-entry",d.name===t?u.classList.add("is-you"):u.classList.add("is-other"),u.textContent=d.name+" \u2014 +"+d.bonus+" ("+d.time+"s)",i.appendChild(u)})}else{i.innerHTML="",i.classList.remove("hidden");var l=document.createElement("div");l.className="movie-reveal-no-winner",l.textContent=y.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}function $n(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){de=t.steal_available&&!G;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");de?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):Ye()}}}function Ye(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden")}function ea(){!de||G||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function ta(e){var t=document.getElementById("steal-modal"),n=document.getElementById("steal-target-list");if(!(!t||!n)){if(n.innerHTML="",!e||e.length===0){var a=document.createElement("p");a.className="steal-no-targets",a.textContent=y.t("steal.waitForSubmit"),n.appendChild(a)}else e.forEach(function(i){var r=document.createElement("button");r.className="steal-target-btn",r.textContent=i,r.addEventListener("click",function(){na(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function je(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function na(e){var t=y.t("steal.confirm").replace("{name}",e),n=await Z(y.t("steal.confirmTitle")||"Steal Answer?",t,y.t("steal.confirmButton")||"Steal",y.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),je())}function Yt(e){if(e.success){de=!1,G=!0,Ye();var t=document.getElementById("year-selector"),n=document.getElementById("submit-btn"),a=document.getElementById("submitted-confirmation");t&&t.classList.add("is-submitted"),n&&n.classList.add("hidden"),a&&a.classList.remove("hidden"),aa(e.target,e.year);var i=document.getElementById("selected-year"),r=document.getElementById("year-slider");i&&(i.textContent=e.year),r&&(r.value=e.year)}}function Qt(e){ta(e.targets||[])}function aa(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=y.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var Fe=!1,ia=500,ce=!1,Qe=.5;function Le(){return Fe?!1:(Fe=!0,setTimeout(function(){Fe=!1},ia),!0)}function Je(){if(s.isAdmin){var e=document.getElementById("admin-control-bar");e&&(e.classList.remove("hidden"),document.body.classList.add("has-control-bar"))}}function ue(){var e=document.getElementById("admin-control-bar");e&&(e.classList.add("hidden"),document.body.classList.remove("has-control-bar"))}function Jt(){var e=document.getElementById("reaction-bar");e&&e.classList.remove("hidden")}function me(){var e=document.getElementById("reaction-bar");e&&e.classList.add("hidden")}function ra(e){s.hasReactedThisPhase||(s.hasReactedThisPhase=!0,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"reaction",emoji:e})))}function Kt(){var e=document.getElementById("reaction-bar");if(e){var t=e.querySelectorAll(".reaction-btn");t.forEach(function(n){n.addEventListener("click",function(){var a=n.getAttribute("data-emoji");a&&ra(a)})})}}function Xt(e,t){var n=document.getElementById("reaction-container");if(n){var a=document.createElement("div");a.className="reaction-bubble",a.textContent=e+" "+t,a.style.left=20+Math.random()*60+"%",n.appendChild(a),setTimeout(function(){a.remove()},3e3)}}function Se(e){var t=document.getElementById("stop-song-btn"),n=document.getElementById("next-round-admin-btn");if(e==="PLAYING"){if(Ke(),t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.skip"))}}else if(e==="REVEAL"){if(t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}else if(n){n.classList.add("is-disabled"),n.disabled=!0;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}function sa(){if(!ce&&Le()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){console.warn("[Beatify] Cannot stop song: WebSocket not connected");return}var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-label");t&&(t.textContent=y.t("game.stopping"))}s.ws.send(JSON.stringify({type:"admin",action:"stop_song"}))}}function oa(){if(Qe>=1){Zt("max");return}Le()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function la(){if(Qe<=0){Zt("min");return}Le()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function Zt(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=e==="max"?"\u{1F50A} Max":"\u{1F507} Min",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1e3))}async function da(){var e=await Z(y.t("admin.endGameConfirm")||"End Game?",y.t("admin.endGameWarning")||"All players will be disconnected.",y.t("admin.endGame")||"End Game",y.t("common.cancel"));if(e&&Le()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(y.t("errors.CONNECTION_LOST"));return}var t=document.getElementById("end-game-btn");if(t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var qe=!1;function $t(){if(!qe&&s.ws&&s.ws.readyState===WebSocket.OPEN){qe=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=y.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"}))}}function en(){qe=!1;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!1,e.textContent=y.t("admin.nextRound")),t){t.disabled=!1;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("admin.nextRound"))}}function ca(){$t()}function tn(){var e=document.getElementById("stop-song-btn"),t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn"),a=document.getElementById("next-round-admin-btn"),i=document.getElementById("end-game-btn");e&&e.addEventListener("click",sa),t&&t.addEventListener("click",oa),n&&n.addEventListener("click",la),a&&a.addEventListener("click",ca),i&&i.addEventListener("click",da)}function nn(){ce=!0;var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-stopped"),e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u2713"),n&&(n.textContent=y.t("game.stopped"))}}function Ke(){ce=!1;var e=document.getElementById("stop-song-btn");if(e){e.classList.remove("is-stopped"),e.classList.remove("is-disabled"),e.disabled=!1;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u23F9\uFE0F"),n&&(n.textContent=y.t("game.stop"))}}function an(e){Qe=e,ua(e),ma(e)}function ua(e){var t=document.getElementById("volume-indicator");if(t){var n=Math.round(e*100);t.textContent="\u{1F50A} "+n+"%",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1500)}}function ma(e){var t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn");t&&t.classList.toggle("is-at-limit",e>=1),n&&n.classList.toggle("is-at-limit",e<=0)}function rn(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",$t);var t=document.getElementById("reveal-view");t&&t.addEventListener("click",function(n){n.target.tagName==="BUTTON"||n.target.closest("button")||($.isRunning()&&$.skipAll(),O())})}function sn(e){var t=document.getElementById("intro-splash-modal");if(t){t.classList.remove("hidden");var n=document.getElementById("intro-splash-confirm-btn"),a=t.querySelector(".intro-splash-modal-waiting");n&&(e?(n.classList.remove("hidden"),a&&a.classList.add("hidden"),n.onclick=function(){s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"admin",action:"confirm_intro_splash"}))}):(n.classList.add("hidden"),a&&a.classList.remove("hidden")))}}function on(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var p=window.BeatifyUtils||{};function Xe(e){var t=e.song||{},n=e.players||[],a=document.getElementById("reveal-round"),i=document.getElementById("reveal-total");a&&(a.textContent=e.round||1),i&&(i.textContent=e.total_rounds||10);var r=document.getElementById("closest-wins-badge");r&&(e.closest_wins_mode?r.classList.remove("hidden"):r.classList.add("hidden"));var o=document.getElementById("intro-badge");if(o)if(e.is_intro_round){o.classList.remove("hidden"),o.classList.add("intro-badge--stopped");var l=o.querySelector("[data-i18n]");l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=p.t("game.introStopped")||"Intro complete!")}else o.classList.add("hidden");var d=document.getElementById("reveal-album-cover");d&&(d.src=t.album_art||"/beatify/static/img/no-artwork.svg");var u=document.getElementById("correct-year");u&&(u.textContent=t.year||"????");var m=document.getElementById("song-title"),c=document.getElementById("song-artist");m&&(m.textContent=t.title||"Unknown Song"),c&&(c.textContent=t.artist||"Unknown Artist");var f=document.getElementById("fun-fact-container"),v=document.getElementById("fun-fact"),w=f?f.querySelector(".fun-fact-header"):null,L=p.getLocalizedSongField(t,"fun_fact");if(v&&(v.textContent=L||""),w&&(w.style.display=L?"flex":"none"),pa(t),ga(e.song_difficulty),f){var b=document.getElementById("song-rich-info"),E=b&&b.innerHTML.trim()!=="",S=L&&L.trim()!=="";f.classList.toggle("hidden",!S&&!E)}for(var I=null,_=0;_'+p.t("analytics.noSubmissions")+"",n.classList.remove("hidden");return}var i="";if(e.average_guess!==null&&t){var r=Math.round(e.average_guess-t);r===0?i=p.t("analytics.onTarget"):r>0?i=p.t("analytics.yearsLate",{years:r}):i=p.t("analytics.yearsEarly",{years:Math.abs(r)})}var o=va(e.all_guesses,t),l="";if(e.exact_match_players&&e.exact_match_players.length>0&&(l+='🎯'+p.t("analytics.exactMatches")+':'+e.exact_match_players.map(x).join(", ")+"
"),e.speed_champion&&e.speed_champion.names){var d=e.speed_champion.names.map(x).join(", ");l+='⚡'+p.t("analytics.speedChampion")+':'+d+'('+e.speed_champion.time+"s)
"}if(e.furthest_players&&e.furthest_players.length>0&&e.all_guesses&&e.all_guesses.length>0){var u=e.all_guesses[e.all_guesses.length-1].years_off;u>0&&(l+='😅'+p.t("analytics.furthestGuess")+':'+e.furthest_players.map(x).join(", ")+'('+u+" years)
")}var m=e.average_guess!==null?Math.round(e.average_guess):"?";a.innerHTML=''+p.t("analytics.averageGuess")+''+m+'
'+e.accuracy_percentage+'%'+p.t("analytics.accuracy",{percent:""}).replace("%","")+'
'+i+'
'+p.t("analytics.histogram")+"
"+o+""+(l?''+l+"
":""),n.classList.remove("hidden")}function va(e,t){var n=7;if(!e||e.length===0)return''+p.t("analytics.noGuesses")+"
";for(var a=e.map(function(ae){return ae.guess}),i=Math.min.apply(null,a),r=Math.max.apply(null,a),o=r-i,l=Math.max(1,Math.ceil(o/n)),d=l*n,u=d-o-1,m=i-Math.floor(u/2),c=[],f=0;f=v&&t<=w})}for(var L=0;L=c[E].start&&b<=c[E].end){c[E].count++;break}for(var S=1,I=0;IS&&(S=c[I].count);for(var _="",C=0;C0?Math.max(R,10):0,F=k.count>0?''+k.count+"":"",ke=l===1?String(k.start):k.start+"-"+String(k.end).slice(-2);_+='"}return''+_+"
"}function ga(e){var t=document.getElementById("song-difficulty");if(t){if(!e){t.classList.add("hidden");return}for(var n="",a=0;a★';t.innerHTML=''+n+'
'+p.t("difficulty."+e.label)+''+e.accuracy+"% "+p.t("difficulty.accuracy")+"",t.classList.remove("hidden")}}function pa(e){var t=document.getElementById("song-rich-info");if(t){var n=[],a=ya(e.chart_info||{});a.length>0&&(n=n.concat(a));var i=ba(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=p.getLocalizedSongField(e,"awards")||[],o=wa(r);o.length>0&&(n=n.concat(o)),n.length>0?t.innerHTML=''+n.join("")+"
":t.innerHTML=""}}function ya(e){if(!e)return[];var t=[];if(e.billboard_peak&&e.billboard_peak>0){var n=e.weeks_on_chart?' \xB7 '+e.weeks_on_chart+" "+p.t("reveal.weeksShort")+"":"";t.push('\u{1F4CA}#'+e.billboard_peak+" "+p.t("reveal.chartBillboard")+n+"")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.german_peak+" "+p.t("reveal.chartGerman")+""),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.uk_peak+" "+p.t("reveal.chartUK")+""),t}function ba(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+""+x(a)+"")}return t}function ha(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"song-badge--diamond":t.indexOf("platinum")!==-1?"song-badge--platinum":t.indexOf("gold")!==-1?"song-badge--gold":"song-badge--platinum"}function Ea(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"\u{1F48E}":t.indexOf("platinum")!==-1?"\u{1F4BF}":t.indexOf("gold")!==-1?"\u{1F947}":"\u{1F4BF}"}function wa(e){if(!e||e.length===0)return[];for(var t=[],n=e.slice(0,3),a=0;a'+o+""+x(i)+"")}return e.length>3&&t.push('+'+(e.length-3)+" more"),t}function La(e){var t=e.toLowerCase();return t.indexOf("grammy")!==-1?"song-badge--grammy":t.indexOf("eurovision")!==-1?"song-badge--eurovision":t.indexOf("oscar")!==-1||t.indexOf("academy award")!==-1?"song-badge--oscar":t.indexOf("hall of fame")!==-1?"song-badge--halloffame":"song-badge--award"}function Sa(e){var t=e.toLowerCase();return t.indexOf("eurovision")!==-1?"\u{1F3A4}":t.indexOf("grammy")!==-1?"\u{1F3C6}":t.indexOf("hall of fame")!==-1?"\u2B50":"\u{1F3C6}"}function Ia(e,t){var n=document.getElementById("reveal-emotion"),a=document.getElementById("personal-result");if(!n)return;var i=n.classList.contains("reveal-emotion-inline")||document.querySelector(".reveal-container--compact");n.className=i?"reveal-emotion-inline":"reveal-emotion",n.innerHTML="",n.classList.add("hidden"),a&&a.classList.remove("is-delayed"),O();var r=p.t("reveal.emotions");function o(v){return v[Math.floor(Math.random()*v.length)]}function l(v){return v===1?p.t("reveal.offByYear"):p.t("reveal.offByYears",{years:v})}var d="missed",u=o(r.missed),m=o(r.missedSub);if(e&&!e.missed_round){var c=e.years_off||0;c===0?(d="exact",u=o(r.exact),m=o(r.exactSub)):c<=2?(d="close",u=o(r.close),m=o(r.closeSub)+" "+l(c)):c<=5?(d="close",u=o(r.close),m=l(c)):(d="wrong",u=o(r.wrong),m=o(r.wrongSub)+" "+l(c))}else e&&e.missed_round&&(d="missed",u=o(r.missed),m=o(r.missedSub));var f=''+u+"";m&&(f+=''+m+"
"),n.innerHTML=f,n.classList.add("reveal-emotion--"+d),n.classList.remove("hidden"),d==="exact"&&ee(),a&&d!=="missed"&&a.classList.add("is-delayed")}function Ba(e,t){var n=document.getElementById("result-content");if(n){if(!e){n.innerHTML=''+p.t("reveal.playerNotFound")+"
";return}if(e.missed_round){var a='\u23F0
'+p.t("reveal.noSubmission")+"
",i=e.previous_streak||0;i>=2&&(a+='\u{1F494}Lost '+i+"-streak!
"),a+='0 pts
',n.innerHTML=a;return}var r=e.years_off||0,o=r===0?p.t("reveal.exact"):r===1?p.t("reveal.yearOff",{years:1}):p.t("reveal.yearsOff",{years:r}),l=r===0?"is-exact":r<=3?"is-close":"is-far",d=e.speed_multiplier||1,u=e.base_score||0,m=d>1,c=e.streak_bonus||0,f=e.artist_bonus||0,v="";m&&u>0&&(v=''+p.t("reveal.baseScore")+''+u+' pts
'+p.t("reveal.speedBonus")+''+d.toFixed(2)+"x
");var w="";e.bet_outcome==="won"?w='\u{1F3B2} '+p.t("reveal.betWon").replace("! 2x points","")+'2x
':e.bet_outcome==="lost"&&(w='\u{1F3B2} '+p.t("reveal.betLost")+'-
');var L="";c>0&&(L=''+e.streak+'-streak bonus!+'+c+" pts
");var b="";f>0&&(b='\u{1F3A4} '+(p.t("artistChallenge.artistBonus")||"Artist Bonus")+'+'+f+" pts
");var E=e.round_score+c+f,S=c>0||f>0,I=e.round_score>=20,_=M.players[e.name],C=_?_.score:e.score-E,k=_?_.streak:0,R=dt(k,e.streak||0);n.innerHTML=''+p.t("reveal.yourGuess")+''+(e.guess||"n/a")+'
'+p.t("reveal.correctYear")+''+t+'
'+p.t("reveal.accuracy")+''+o+"
"+v+w+'+0 pts
'+L+b+(S?''+p.t("reveal.total")+': +0 pts
':"");var D=n.querySelector(".score-value");D&&(ot(D,0,e.round_score,{betWon:e.bet_outcome==="won",betLost:e.bet_outcome==="lost",streakMilestone:R,isBigScore:I}),e.bet_outcome==="won"&&e.round_score>0&&setTimeout(function(){var T=document.getElementById("personal-result-score");T&&Ne(T,e.round_score,{isBetWin:!0})},200));var V=n.querySelector(".total-value");V&&S&&(setTimeout(function(){se(V,0,E,600)},300),R&&setTimeout(function(){var T=n.querySelector(".result-total");if(T){var F={3:20,5:50,10:100}[R]||0;Ne(T,F,{isStreak:!0,text:"+"+F+" "+R+"-Streak!"})}},500))}}function xa(e,t){var n=document.getElementById("reveal-results-cards");if(n){if(!e||e.length===0){n.innerHTML="";return}var a=null;t&&e.forEach(function(o){!o.missed_round&&o.years_off!=null&&(a===null||o.years_off';i.forEach(function(o){var l=o.name===s.playerName,d=o.missed_round===!0,u=o.years_off||0,m=o.round_score||0,c=d?"is-score-zero":m>=10?"is-score-high":m>=1?"is-score-medium":"is-score-zero",f=t&&!d&&a!==null&&(o.years_off||0)===a,v=f?" is-closest-winner":"",w=d?"\u2014":o.guess||"n/a",L=d?p.t("reveal.noGuessShort"):u===0?p.t("reveal.exact"):p.t("reveal.shortOff",{years:u}),b=o.bet?'\u{1F3B2}':"",E=f?'\u{1F3AF}':"",S="";o.artist_bonus&&o.artist_bonus>0&&(S='\u{1F3A4} +'+o.artist_bonus+"");var I="";if(o.stole_from)I='\u{1F977}'+p.t("steal.stolenFrom",{name:x(o.stole_from)})+"
";else if(o.was_stolen_by&&o.was_stolen_by.length>0){var _=o.was_stolen_by.map(x).join(", ");I='\u{1F3AF}'+p.t("steal.stolenBy",{name:_})+"
"}r+=''+x(o.name)+b+E+'
'+w+'
'+L+"
"+I+'
+'+m+S+"
"}),r+="",n.innerHTML=r}}var A=window.BeatifyUtils||{};function dn(e){window.scrollTo(0,0);var t=e.leaderboard||[];t.forEach(function(b){b.is_current=b.name===s.playerName}),[1,2,3].forEach(function(b){var E=t.find(function(_){return _.rank===b}),S=document.getElementById("podium-"+b+"-name"),I=document.getElementById("podium-"+b+"-score");S&&(S.textContent=E?x(E.name):"---"),I&&(I.textContent=E?E.score:"0")});var n=t.find(function(b){return b.is_current}),a=document.getElementById("your-final-rank"),i=document.getElementById("your-final-score"),r=document.getElementById("stat-best-streak"),o=document.getElementById("stat-rounds"),l=document.getElementById("stat-bets");n&&(a&&(a.textContent="#"+n.rank),i&&(i.textContent=n.score+" "+A.t("leaderboard.points")),r&&(r.textContent=n.best_streak||0),o&&(o.textContent=n.rounds_played||0),l&&(l.textContent=n.bets_won||0));var d=document.getElementById("final-leaderboard-list");d&&(d.innerHTML=t.map(function(b){var E=b.is_current?"is-current":"",S=b.connected===!1?"final-entry--disconnected":"",I=b.connected===!1?'(away)':"";return'#'+b.rank+''+x(b.name)+I+''+b.score+"
"}).join("")),Ca(e.superlatives),_a(e.highlights),ka(e.share_data);var u=document.getElementById("end-admin-controls"),m=document.getElementById("end-player-message");if(n&&n.is_admin){u&&u.classList.remove("hidden"),m&&m.classList.add("hidden");var c=document.getElementById("new-game-btn");c&&(c.onclick=Ta);var f=document.getElementById("player-rematch-btn");f&&(f.onclick=function(){f.disabled=!0;var b=f.textContent;f.textContent="\u23F3",fetch("/beatify/api/rematch-game",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"}}).then(function(E){if(!E.ok)return E.json().then(function(S){throw new Error(S.message||"Rematch failed")});f.textContent="\u23F3"}).catch(function(E){console.error("[Player] Rematch failed:",E),alert(E.message||"Failed to start rematch"),f.disabled=!1,f.textContent=b})})}else u&&u.classList.add("hidden"),m&&m.classList.remove("hidden");if(n){var v=e.total_rounds||10,w=n.best_streak||0,L=w===v&&v>0;L?ee("perfect"):n.rank===1&&ee("winner")}}function Ca(e){var t=document.getElementById("superlatives-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n="";e.forEach(function(a,i){var r="";switch(a.value_label){case"avg_time":r=a.value+"s "+A.t("superlatives.avgTime");break;case"streak":r=a.value+" "+A.t("superlatives.streak");break;case"bets":r=a.value+" "+A.t("superlatives.bets");break;case"points":r=a.value+" "+A.t("superlatives.points");break;case"close_guesses":r=a.value+" "+A.t("superlatives.closeGuesses");break;default:r=a.value}n+=''+a.emoji+'
'+A.t("superlatives."+a.title)+'
'+x(a.player_name)+'
'+r+"
"}),t.innerHTML=n,t.classList.remove("hidden")}}function _a(e){var t=document.getElementById("highlights-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n=document.getElementById("highlights-list");if(n){var a="";e.forEach(function(i,r){var o=A.t("highlights."+i.description,i.description_params)||i.description;o===i.description&&i.description_params&&(o=A.t("highlights."+i.description)||i.description,Object.keys(i.description_params).forEach(function(l){o=o.replace("{"+l+"}",x(i.description_params[l]))})),a+=''+(i.emoji||"\u2728")+'
'+o+'
'+A.t("highlights.roundLabel",{round:i.round})+"
"}),n.innerHTML=a,t.classList.remove("hidden")}}}function ka(e){var t=document.getElementById("share-container");if(t){if(!e||!e.emoji_grids){t.classList.add("hidden");return}var n=e.emoji_grids[s.playerName];if(!n){var a=Object.keys(e.emoji_grids);a.length===1&&(n=e.emoji_grids[a[0]])}if(!n){t.classList.add("hidden");return}var i=document.getElementById("share-emoji-grid");if(i){var r=n.split(`
`).map(function(d){return''+A.escapeHtml(d)+"
"}).join("");i.innerHTML=r,i.dataset.rawText=n}var o=document.getElementById("share-copy-btn");o&&(o.onclick=function(){navigator.clipboard.writeText(n).then(function(){var d=document.getElementById("share-toast");d&&(d.classList.remove("hidden"),setTimeout(function(){d.classList.add("hidden")},2e3))})});var l=document.getElementById("share-save-btn");l&&(l.onclick=function(){Aa(n,e.playlist_name,e)}),t.classList.remove("hidden")}}function Aa(e,t,n){var a=document.createElement("canvas");a.width=800,a.height=800;var i=a.getContext("2d"),r=i.createLinearGradient(0,0,0,800);r.addColorStop(0,"#0f0c29"),r.addColorStop(.5,"#302b63"),r.addColorStop(1,"#24243e"),i.fillStyle=r,i.fillRect(0,0,800,800);var o=i.createLinearGradient(0,0,800,0);o.addColorStop(0,"#e94560"),o.addColorStop(1,"#0f3460"),i.fillStyle=o,i.fillRect(0,0,800,4);var l=new Image;l.src="/beatify/static/img/icon-256.png",l.onerror=function(){d(null)},l.onload=function(){d(l)};function d(u){u?(i.drawImage(u,28,20,64,64),i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="left",i.fillText("Beatify",104,60)):(i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="center",i.fillText("\u{1F3B5} Beatify",400,55)),i.textAlign="center";var m=(t||"").toUpperCase();i.font="bold 13px system-ui, sans-serif";var c=i.measureText(m).width+32,f=400-c/2;i.fillStyle="rgba(233,69,96,0.18)",i.beginPath(),i.roundRect(f,96,c,30,15),i.fill(),i.strokeStyle="#e94560",i.lineWidth=1,i.stroke(),i.fillStyle="#e94560",i.fillText(m,400,116);for(var v=e.split(`
`).filter(function(N){return N.trim()!==""}),w="",L=[],b="",E="",S="",I="",_=0;_=ge&&(s.isReconnecting=!1,pe(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}function Q(e){if(s.playerName=e,vn(e),!(s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN))){var t=window.location.protocol==="https:"?"wss:":"ws:",n=t+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(n),s.ws.onopen=function(){s.reconnectAttempts=0,s.isReconnecting=!1,pe(),pn();var a={type:"join",name:e};s.isAdmin&&(a.is_admin=!0),s.ws.send(JSON.stringify(a))},s.ws.onmessage=function(a){try{var i=JSON.parse(a.data);hn(i)}catch(r){console.error("Failed to parse WebSocket message:",r)}},s.ws.onclose=function(){if(s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=ge&&(s.isReconnecting=!1,pe(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}s.connectWithSession=_e;s.connectWebSocket=Q;function hn(e){var t=document.getElementById("join-btn"),n=document.getElementById("name-input");if(e.type==="state"){var a=e.players||[],i=a.find(function(u){return u.name===s.playerName});if(i&&(s.isAdmin=i.is_admin===!0),e.language&&(Ha(e.language),typeof BeatifyI18n<"u"&&e.language!==BeatifyI18n.getLanguage()&&BeatifyI18n.setLanguage(e.language).then(function(){BeatifyI18n.initPageTranslations(),Pe(a),e.difficulty&&he(e.difficulty),e.phase==="REVEAL"&&Xe(e),(e.phase==="PLAYING"||e.phase==="REVEAL")&&Se(e.phase)})),e.phase==="LOBBY"){Y(),ue(),me(),s.currentRoundNumber=0,q("warmup");var r=document.getElementById("start-game-btn");r&&(r.disabled=!1,r.innerHTML='\u{1F389}'+ve.t("lobby.startGame")+""),B("lobby-view"),Pe(a),e.difficulty&&he(e.difficulty),e.join_url&&It(e.join_url),_t(a)}else if(e.phase==="PLAYING"){var o=e.round||1;o!==s.currentRoundNumber&&(s.currentRoundNumber=o,Wt()),en(),q("party"),B("game-view"),oe(),Ot(e),e.intro_splash_pending?sn(s.isAdmin):on(),e.difficulty&&he(e.difficulty),e.deadline&&Rt(e.deadline),Vt(),Ht(),Je(),Se("PLAYING"),me()}else e.phase==="REVEAL"?(Y(),e.early_reveal&&Tt(),q("party"),B("reveal-view"),Xe(e),Gt(),Dt(),Je(),Se("REVEAL"),s.hasReactedThisPhase=!1,Jt()):e.phase==="PAUSED"?(Y(),ue(),me(),q("warmup"),B("paused-view"),cn(e)):e.phase==="END"&&(Y(),ue(),me(),s.currentRoundNumber=0,q("warmup"),B("end-view"),dn(e),fe())}else if(e.type==="join_ack"){e.session_id&&Oa(e.session_id);try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}}else if(e.type==="reconnect_ack")e.success&&e.name?(s.playerName=e.name,vn(e.name),At(e.name)):(Ze(),fe(),s.playerName=null,B("join-view"));else if(e.type==="submit_ack")ze();else if(e.type==="metadata_update")Pt(e.song);else if(e.type==="error"){if(e.code==="ROUND_EXPIRED"||e.code==="ALREADY_SUBMITTED"){Ft(e);return}if(e.code==="GAME_ENDED"){B("end-view");return}if(e.code==="NOT_ADMIN"){s.isAdmin=!1,ue(),console.warn("Admin action rejected: not admin");return}if(e.code==="SESSION_TAKEOVER"){s.isReconnecting=!1,pe(),s.playerName=null,et(),console.warn("Session taken over by another tab");return}if(e.code==="SESSION_NOT_FOUND"){Ze(),s.intentionalLeave=!0,s.ws&&s.ws.close(),B("join-view");return}if(e.code==="ADMIN_CANNOT_LEAVE"){s.intentionalLeave=!1,alert(e.message||"Host cannot leave. End the game instead.");return}if(e.code==="INVALID_ACTION"&&e.message==="No song playing"){Ke(),console.warn("[Beatify] Stop song failed: No song playing");return}B("join-view"),ja(e.message),t&&(t.disabled=!1,t.textContent=ve.t("join.joinButton")),n&&n.focus(),s.playerName=null,fe()}else if(e.type==="song_stopped")nn();else if(e.type==="volume_changed")an(e.level);else if(e.type==="game_ended")Wa();else if(e.type==="rematch_started"){console.log("[Player] Rematch started - transitioning to lobby"),$.clear(),O(),B("lobby-view");var l=document.getElementById("player-rematch-btn");l&&(l.disabled=!1,l.textContent="\u{1F501}");var d=$e();d&&(s.ws&&s.ws.readyState===WebSocket.OPEN?(s.reconnectAttempts=0,s.ws.send(JSON.stringify({type:"reconnect",session_id:d}))):(s.reconnectAttempts=0,_e()))}else e.type==="left"?Fa():e.type==="steal_targets"?Qt(e):e.type==="steal_ack"?Yt(e):e.type==="artist_guess_ack"?jt(e):e.type==="movie_guess_ack"?Ut(e):e.type==="player_reaction"&&Xt(e.player_name,e.emoji)}function Fa(){fe(),Ze(),s.playerName=null,s.isAdmin=!1,B("join-view")}function Wa(){var e=s.isAdmin;fe();try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}ft(),Et(),$.clear(),O(),s.playerName=null,s.isAdmin=!1,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.close(),s.ws=null;var t=document.getElementById("end-view");if(!(!t||!t.classList.contains("hidden"))){var n=document.getElementById("end-player-message");n&&(n.innerHTML='Thanks for playing!
Scan the QR code again to join the next game.
',n.classList.remove("hidden")),B("end-view")}}function ja(e){var t=document.getElementById("name-validation-msg");t&&(t.textContent=e,t.classList.remove("hidden"))}function tt(e){var t=(e||"").trim();return t?t.length>Ma?{valid:!1,error:"Name too long (max 20 characters)"}:{valid:!0,name:t}:{valid:!1,error:"Please enter a name"}}function un(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");if(!(!e||!t)){var a=tt(e.value);a.valid&&(t.disabled=!0,t.textContent=ve.t("game.joining"),n&&n.classList.add("hidden"),Q(a.name))}}function qa(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");!e||!t||(e.addEventListener("input",function(){var a=tt(this.value);t.disabled=!a.valid,n&&(n.textContent=!a.valid&&this.value?a.error:"",n.classList.toggle("hidden",a.valid||!this.value))}),t.addEventListener("click",un),e.addEventListener("keypress",function(a){a.key==="Enter"&&!t.disabled&&un()}))}function Ua(){var e=document.getElementById("retry-connection-btn");e&&e.addEventListener("click",function(){s.playerName?(s.reconnectAttempts=0,B("loading-view"),Q(s.playerName)):Ce()})}async function mn(){var e=K.getDeviceTier();document.body.classList.add("device-tier-"+e);var t=await ve.waitForI18n();if(!t)console.error("[Player] BeatifyI18n module failed to load - UI will use fallback text");else{var n=Ga();await BeatifyI18n.init(n),BeatifyI18n.initPageTranslations()}var a=document.getElementById("dashboard-hint-url");a&&(a.textContent=window.location.origin+"/beatify/dashboard");var i=document.getElementById("player-dashboard-url");if(i&&(i.href=window.location.origin+"/beatify/dashboard"),qa(),Bt(),Ct(),kt(),rn(),tn(),Ua(),vt(),gt(),pt(),Kt(),Va()&&s.playerName){Q(s.playerName);return}var r=Pa();if(r&&s.gameId){console.log("[Beatify] Auto-reconnecting as:",r),Q(r);return}if(r){var o=document.getElementById("name-input"),l=document.getElementById("join-btn");if(o&&(o.value=r,l)){var d=tt(r);l.disabled=!d.valid}}}Ce();document.getElementById("refresh-btn")?.addEventListener("click",function(){B("loading-view"),Ce()});document.getElementById("retry-btn")?.addEventListener("click",function(){B("loading-view"),Ce()});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",mn):mn();"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/static/sw.js",{scope:"/beatify/"}).then(function(e){console.log("[Beatify] SW registered:",e.scope)}).catch(function(e){console.warn("[Beatify] SW registration failed:",e)})});document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible"){var e=s.ws;(!e||e.readyState===WebSocket.CLOSING||e.readyState===WebSocket.CLOSED)&&s.playerName&&(console.log("[Beatify] Page visible, WebSocket dead \u2014 reconnecting immediately."),s.reconnectAttempts=0,_e())}});
From 48c9ea620fcf6dec9570246be3f6651098fe280d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Markus=20Holzh=C3=A4user?=
Date: Sun, 12 Apr 2026 17:24:58 +0200
Subject: [PATCH 3/3] fix: add empty title guard and hard failure detection to
MA playback
- If expected_title is empty, skip title verification instead of
matching everything (empty string is always "in" any string)
- Return False on timeout when speaker state is idle/unavailable/off
(hard failure), only return True when buffering/playing (#345)
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../beatify/services/media_player.py | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/custom_components/beatify/services/media_player.py b/custom_components/beatify/services/media_player.py
index 67f02484..a9a1d0b5 100644
--- a/custom_components/beatify/services/media_player.py
+++ b/custom_components/beatify/services/media_player.py
@@ -262,6 +262,8 @@ async def _play_via_music_assistant(self, song: dict[str, Any]) -> bool:
# - media_position >= 1 (pos=0 means only queued in MA, not yet playing)
# - media_position_updated_at changed (speaker is actively reporting)
expected_lower = expected_title.lower()
+ if not expected_lower:
+ _LOGGER.warning("MA playback: no expected title — skipping title verification")
elapsed = 0.0
while elapsed < PLAYBACK_TIMEOUT:
try:
@@ -281,7 +283,8 @@ async def _play_via_music_assistant(self, song: dict[str, Any]) -> bool:
# Match expected title (substring, case-insensitive) — MA may
# append suffixes like "(Official HD Video)" or "(7″ mix)"
title_matches = (
- expected_lower in current_title.lower() if current_title else False
+ (not expected_lower) # no title to verify — accept any
+ or (expected_lower in current_title.lower() if current_title else False)
)
if title_matches and position_fresh and actually_playing:
@@ -296,12 +299,20 @@ async def _play_via_music_assistant(self, song: dict[str, Any]) -> bool:
elapsed += 0.5
current_state = self._hass.states.get(self._entity_id)
+ speaker_state = current_state.state if current_state else "unknown"
+
+ # Hard failure: speaker is idle/unavailable/off — song won't play
+ if speaker_state in ("idle", "unavailable", "off", "unknown"):
+ _LOGGER.error(
+ "MA playback failed after %.1fs for %s (state: %s)",
+ PLAYBACK_TIMEOUT, uri, speaker_state,
+ )
+ return False
+
_LOGGER.warning(
"MA playback not confirmed after %.1fs for %s (state: %s). "
"Continuing anyway — MA may still be buffering. (#345)",
- PLAYBACK_TIMEOUT,
- uri,
- current_state.state if current_state else "unknown",
+ PLAYBACK_TIMEOUT, uri, speaker_state,
)
# Return True: don't skip the song — MA+YTMusic can take >8s to buffer.
# Returning False would trigger retries that cause race conditions (#345).