(function () { "use strict"; // ------------------------------------------------------------------------- // Plain JS, no build step on purpose (see docs/architecture.md). Talks // directly to this Emby server: standard auth endpoint for sign-in, then // this plugin's own /WatchParty/* routes for everything else. // // TODO(verify) - see docs/architecture.md#assumptions-to-verify: // - X-Emby-Authorization header field names below. // - /Users/AuthenticateByName's `Pw` field name. // ------------------------------------------------------------------------- const state = { serverUrl: localStorage.getItem("wp_serverUrl") || "", accessToken: localStorage.getItem("wp_accessToken") || "", userId: localStorage.getItem("wp_userId") || "", partyId: localStorage.getItem("wp_partyId") || "", pollTimer: null }; const els = { loginPanel: document.getElementById("loginPanel"), lobbyPanel: document.getElementById("lobbyPanel"), partyPanel: document.getElementById("partyPanel"), serverUrl: document.getElementById("serverUrl"), username: document.getElementById("username"), password: document.getElementById("password"), loginBtn: document.getElementById("loginBtn"), loginError: document.getElementById("loginError"), createBtn: document.getElementById("createBtn"), joinCode: document.getElementById("joinCode"), joinBtn: document.getElementById("joinBtn"), lobbyError: document.getElementById("lobbyError"), partyCode: document.getElementById("partyCode"), memberList: document.getElementById("memberList"), resyncBtn: document.getElementById("resyncBtn"), leaveBtn: document.getElementById("leaveBtn") }; function deviceId() { let id = localStorage.getItem("wp_deviceId"); if (!id) { id = "watchparty-web-" + Math.random().toString(36).slice(2); localStorage.setItem("wp_deviceId", id); } return id; } function authHeader() { return 'MediaBrowser Client="WatchParty", Device="Browser", DeviceId="' + deviceId() + '", Version="0.1.0"'; } async function embyFetch(path, options) { options = options || {}; options.headers = Object.assign({}, options.headers, { "X-Emby-Authorization": authHeader() }); if (state.accessToken) { options.headers["X-Emby-Token"] = state.accessToken; } const res = await fetch(state.serverUrl.replace(/\/+$/, "") + path, options); if (!res.ok) { throw new Error("Emby request failed: " + res.status); } const text = await res.text(); return text ? JSON.parse(text) : null; } async function login() { els.loginError.textContent = ""; state.serverUrl = els.serverUrl.value.trim(); try { const result = await embyFetch("/Users/AuthenticateByName", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ Username: els.username.value, Pw: els.password.value }) }); state.accessToken = result.AccessToken; state.userId = result.User.Id; localStorage.setItem("wp_serverUrl", state.serverUrl); localStorage.setItem("wp_accessToken", state.accessToken); localStorage.setItem("wp_userId", state.userId); showLobbyOrParty(); } catch (err) { els.loginError.textContent = "Couldn't sign in - check the server URL and credentials."; } } async function createParty() { els.lobbyError.textContent = ""; try { const party = await embyFetch("/WatchParty/Parties", { method: "POST" }); enterParty(party); } catch (err) { els.lobbyError.textContent = "Couldn't start a party - are you currently playing something in Emby?"; } } async function joinParty() { els.lobbyError.textContent = ""; const code = els.joinCode.value.trim().toUpperCase(); try { const party = await embyFetch("/WatchParty/Parties/" + encodeURIComponent(code) + "/Join", { method: "POST" }); enterParty(party); } catch (err) { els.lobbyError.textContent = "Couldn't join - check the code, and make sure you're playing the same title."; } } function enterParty(party) { state.partyId = party.PartyId; localStorage.setItem("wp_partyId", state.partyId); renderParty(party); show(els.partyPanel); startPolling(); } function renderParty(party) { els.partyCode.textContent = party.JoinCode; els.memberList.innerHTML = ""; const reference = party.Members.find((m) => m.IsReference); party.Members.forEach((m) => { const li = document.createElement("li"); const dot = document.createElement("span"); dot.className = "dot"; if (m.IsPaused) { dot.classList.add("paused"); } else if (reference && Math.abs(m.PositionTicks - reference.PositionTicks) > 50000000) { // 50,000,000 ticks = 5s - just for this indicator dot; the server enforces the real threshold. dot.classList.add("drift"); } li.appendChild(dot); const label = document.createElement("span"); label.textContent = m.UserName + (m.IsReference ? " (reference)" : ""); li.appendChild(label); els.memberList.appendChild(li); }); } async function refreshParty() { try { const party = await embyFetch("/WatchParty/Parties/" + state.partyId, {}); renderParty(party); } catch (err) { // Party probably ended - drop back to the lobby. stopPolling(); state.partyId = ""; localStorage.removeItem("wp_partyId"); show(els.lobbyPanel); } } function startPolling() { stopPolling(); state.pollTimer = setInterval(refreshParty, 4000); } function stopPolling() { if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; } } async function resync() { await embyFetch("/WatchParty/Parties/" + state.partyId + "/Resync", { method: "POST" }); } async function leaveParty() { await embyFetch("/WatchParty/Parties/" + state.partyId + "/Leave", { method: "POST" }); stopPolling(); state.partyId = ""; localStorage.removeItem("wp_partyId"); show(els.lobbyPanel); } function show(panel) { [els.loginPanel, els.lobbyPanel, els.partyPanel].forEach((p) => p.classList.add("hidden")); panel.classList.remove("hidden"); } function showLobbyOrParty() { show(state.partyId ? els.partyPanel : els.lobbyPanel); } els.loginBtn.addEventListener("click", login); els.createBtn.addEventListener("click", createParty); els.joinBtn.addEventListener("click", joinParty); els.resyncBtn.addEventListener("click", resync); els.leaveBtn.addEventListener("click", leaveParty); // Resume a session if we already have a token stored. if (state.accessToken && state.serverUrl) { els.serverUrl.value = state.serverUrl; if (state.partyId) { show(els.partyPanel); refreshParty().then(startPolling); } else { show(els.lobbyPanel); } } else { show(els.loginPanel); } })();