diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c78cd5..fff5e2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,14 @@ All notable changes to the KoalaSync browser extension and relay server.
---
-## [v2.1.1] — 2026-06-04
+## [v2.1.2] — 2026-06-06
+
+### Fixed
+- **Episode guard regex**: Fixed `isDifferentEpisode()` not detecting episode changes when the MediaSession title uses `Sxx:Exx` format (colon separator, as used by Jellyfin/Emby). The regex character class `[\s\-\.]` was replaced with `[^a-zA-Z0-9]` to match **any** non-alphanumeric separator between season and episode numbers, preventing play/pause/seek commands from a different episode leaking through and incorrectly manipulating a peer's playback.
+- **Per-device storage isolation**: Migrated `username`, `roomId`, `password`, `serverUrl`, and `useCustomServer` from `chrome.storage.sync` (synced across Google account) to `chrome.storage.local` (per-device). This prevents the extension from automatically joining the same room with the same name on multiple devices. Existing user data is migrated silently on first run; all preferences (`filterNoise`, `autoSyncNextEpisode`, etc.) remain synced.
### Changed
-- Added progressive enhancement using `appearance: base-select` to support styling of country flag emojis in expanded language selectors on newer Chromium browsers.
+- Added one-time migration fallback in `getSettings()` and popup `init()` to copy existing user settings from `storage.sync` to `storage.local` on first launch after the update.
---
diff --git a/README.md b/README.md
index 16923e5..c12ceb3 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
New v2.1.1 Release! — See what's changed
+New v2.1.2 Release! — See what's changed
KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback on almost any website with a video element—YouTube, Twitch, Netflix, Emby, Jellyfin, and beyond. Built with a focus on Data Sovereignty and Performance.
diff --git a/extension/background.js b/extension/background.js index 46a4c33..1e372c1 100644 --- a/extension/background.js +++ b/extension/background.js @@ -230,35 +230,38 @@ async function getPeerId() { } async function getSettings() { - return new Promise((resolve, reject) => { - chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username'], (data) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - return; - } - let username = data.username; - if (!username) { - username = generateUsername(); - chrome.storage.sync.set({ username }, () => { - resolve({ - serverUrl: data.serverUrl || '', - useCustomServer: data.useCustomServer || false, - roomId: data.roomId || '', - password: data.password || '', - username: username - }); - }); - } else { - resolve({ - serverUrl: data.serverUrl || '', - useCustomServer: data.useCustomServer || false, - roomId: data.roomId || '', - password: data.password || '', - username: username - }); - } + // Try local (per-device) first, fall back to sync for migration + let data = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']); + let migrated = false; + if (!data.username) { + const syncData = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']); + if (syncData.username || syncData.roomId) { + data = syncData; + migrated = true; + } + } + let username = data.username; + if (!username) { + username = generateUsername(); + } + if (migrated) { + await chrome.storage.local.set({ + serverUrl: data.serverUrl || '', + useCustomServer: data.useCustomServer || false, + roomId: data.roomId || '', + password: data.password || '', + username }); - }); + } else if (!data.username) { + await chrome.storage.local.set({ username }); + } + return { + serverUrl: data.serverUrl || '', + useCustomServer: data.useCustomServer || false, + roomId: data.roomId || '', + password: data.password || '', + username + }; } function addLog(message, type = 'info') { @@ -382,7 +385,7 @@ async function leaveRoomAfterIdleGrace(reason) { lastContentHeartbeatAt: null, episodeLobby: null }).catch(() => {}); - await chrome.storage.sync.set({ roomId: '', password: '' }).catch(() => {}); + await chrome.storage.local.set({ roomId: '', password: '' }).catch(() => {}); addLog(reason, 'info'); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {}); updateBadgeStatus(); @@ -1396,7 +1399,7 @@ async function handleAsyncMessage(message, sender, sendResponse) { } else if (message.type === 'WEB_JOIN_REQUEST') { const { roomId: rawRoomId, password, useCustomServer, serverUrl } = message; const roomId = typeof rawRoomId === 'string' ? rawRoomId.replace(/[^a-zA-Z0-9\-]/g, '') : ''; - chrome.storage.sync.set({ + chrome.storage.local.set({ roomId, password, useCustomServer: !!useCustomServer, diff --git a/extension/content.js b/extension/content.js index 1d0824d..8ef655c 100644 --- a/extension/content.js +++ b/extension/content.js @@ -126,8 +126,8 @@ // Returns null if no episode pattern found. function extractEpisodeId(title) { if (!title || typeof title !== 'string') return null; - // S01E01 patterns (with optional spaces, dashes, dots between S and E) - const se = title.match(/S(?:eason\s*)?(\d+)[\s\-\.]*E(?:pisode\s*)?(\d+)/i); + // S01E01 patterns (with any non-alphanumeric separator between season and E) + const se = title.match(/S(?:eason\s*)?(\d+)[^a-zA-Z0-9]*E(?:pisode\s*)?(\d+)/i); if (se) return `S${String(se[1]).padStart(2, '0')}E${String(se[2]).padStart(2, '0')}`; // "Episode X", "Folge X", "Ep. X", "#X" const ep = title.match(/(?:Episode|Folge|Ep\.?|#)\s*(\d+)/i); diff --git a/extension/popup.js b/extension/popup.js index 020fd0d..014452f 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -108,10 +108,28 @@ function setRoomRefreshCooldown() { // --- Initialization --- async function init() { - // Load Settings - const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale']); + // Load per-device settings (local) + synced preferences (sync) + const [localData, syncData] = await Promise.all([ + chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']), + chrome.storage.sync.get(['filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale']) + ]); - let activeLang = data.locale; + // Migrate from sync → local for existing users + const hadLocalData = !!(localData.username || localData.roomId); + let syncHadData = false; + if (!hadLocalData) { + const oldSync = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']); + syncHadData = !!(oldSync.username || oldSync.roomId); + if (syncHadData) { + localData.serverUrl = oldSync.serverUrl; + localData.useCustomServer = oldSync.useCustomServer; + localData.roomId = oldSync.roomId; + localData.password = oldSync.password; + localData.username = oldSync.username; + } + } + + let activeLang = syncData.locale; if (!activeLang) { activeLang = getSystemLanguage(); chrome.storage.sync.set({ locale: activeLang }); @@ -122,21 +140,31 @@ async function init() { if (elements.langSelector) elements.langSelector.value = activeLang; - let username = data.username; + let username = localData.username; if (!username) { username = generateUsername(); - chrome.storage.sync.set({ username }); + await chrome.storage.local.set({ username }); + } + if (syncHadData) { + // Persist migrated room data to local (one-time migration) + await chrome.storage.local.set({ + serverUrl: localData.serverUrl || '', + useCustomServer: localData.useCustomServer || false, + roomId: localData.roomId || '', + password: localData.password || '', + username + }); } - elements.serverUrl.value = data.serverUrl || ''; - elements.roomId.value = data.roomId || ''; - elements.password.value = data.password || ''; + elements.serverUrl.value = localData.serverUrl || ''; + elements.roomId.value = localData.roomId || ''; + elements.password.value = localData.password || ''; elements.username.value = username; - if (elements.filterNoise) elements.filterNoise.checked = data.filterNoise !== false; - if (elements.autoSyncNextEpisode) elements.autoSyncNextEpisode.checked = data.autoSyncNextEpisode !== false; - if (elements.forceSyncMode) elements.forceSyncMode.value = data.forceSyncMode || 'jump-to-others'; - if (elements.browserNotifications) elements.browserNotifications.checked = data.browserNotifications === true; - if (elements.autoCopyInvite) elements.autoCopyInvite.checked = data.autoCopyInvite !== false; + if (elements.filterNoise) elements.filterNoise.checked = syncData.filterNoise !== false; + if (elements.autoSyncNextEpisode) elements.autoSyncNextEpisode.checked = syncData.autoSyncNextEpisode !== false; + if (elements.forceSyncMode) elements.forceSyncMode.value = syncData.forceSyncMode || 'jump-to-others'; + if (elements.browserNotifications) elements.browserNotifications.checked = syncData.browserNotifications === true; + if (elements.autoCopyInvite) elements.autoCopyInvite.checked = syncData.autoCopyInvite !== false; // Set Version Info const versionTxt = `v${chrome.runtime.getManifest().version}`; @@ -145,14 +173,14 @@ async function init() { const popupVerEl = document.getElementById('popupVersion'); if (popupVerEl) popupVerEl.textContent = versionTxt; - if (data.useCustomServer) { + if (localData.useCustomServer) { setServerMode(true); } else { setServerMode(false); } - toggleUIState(!!data.roomId); - updateUI(data.roomId, data.password, data.useCustomServer, data.serverUrl); + toggleUIState(!!localData.roomId); + updateUI(localData.roomId, localData.password, localData.useCustomServer, localData.serverUrl); refreshLogs(); refreshHistory(); @@ -881,7 +909,7 @@ function checkInviteLink() { if (serverUrl || useCustomServer) { elements.serverUrl.value = serverUrl; setServerMode(useCustomServer); - chrome.storage.sync.set({ serverUrl, useCustomServer }); + chrome.storage.local.set({ serverUrl, useCustomServer }); } elements.joinBtn.style.boxShadow = '0 0 15px var(--accent)'; @@ -898,9 +926,9 @@ function setServerMode(custom) { elements.serverOfficial.classList.toggle('active', !custom); elements.serverCustom.classList.toggle('active', custom); elements.serverUrl.style.display = custom ? 'block' : 'none'; - chrome.storage.sync.get(['useCustomServer', 'serverUrl'], (data) => { + chrome.storage.local.get(['useCustomServer', 'serverUrl'], (data) => { if (data.useCustomServer !== custom) { - chrome.storage.sync.set({ useCustomServer: custom }); + chrome.storage.local.set({ useCustomServer: custom }); if (!custom || data.serverUrl) { chrome.runtime.sendMessage({ type: 'RETRY_CONNECT' }); } @@ -936,11 +964,11 @@ elements.forceSyncMode.addEventListener('change', () => { }); elements.serverUrl.addEventListener('input', () => { - chrome.storage.sync.set({ serverUrl: elements.serverUrl.value }); + chrome.storage.local.set({ serverUrl: elements.serverUrl.value }); }); elements.username.addEventListener('change', () => { - chrome.storage.sync.set({ username: elements.username.value }); + chrome.storage.local.set({ username: elements.username.value }); }); if (elements.langSelector) { @@ -961,14 +989,14 @@ if (elements.langSelector) { lastKnownPeers = res.peers || []; if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers); - const data = await chrome.storage.sync.get(['roomId', 'password', 'useCustomServer', 'serverUrl']); + const data = await chrome.storage.local.get(['roomId', 'password', 'useCustomServer', 'serverUrl']); updateUI(data.roomId, data.password, data.useCustomServer, data.serverUrl); await populateTabs(res.peers, res.targetTabId); if (res.episodeLobby) updateLobbyUI(res.episodeLobby, res.peers); } else { applyConnectionStatus('disconnected'); - const data = await chrome.storage.sync.get(['roomId', 'password', 'useCustomServer', 'serverUrl']); + const data = await chrome.storage.local.get(['roomId', 'password', 'useCustomServer', 'serverUrl']); updateUI(data.roomId, data.password, data.useCustomServer, data.serverUrl); await populateTabs(); } @@ -984,7 +1012,7 @@ elements.serverUrl.addEventListener('change', () => { if (url && !url.includes('://')) { url = 'ws://' + url; elements.serverUrl.value = url; - chrome.storage.sync.set({ serverUrl: url }); + chrome.storage.local.set({ serverUrl: url }); } if (elements.serverCustom.classList.contains('active') && url) { chrome.runtime.sendMessage({ type: 'RETRY_CONNECT' }); @@ -1098,7 +1126,7 @@ elements.joinBtn.addEventListener('click', async () => { window.justCreatedRoom = true; } - await chrome.storage.sync.set({ serverUrl, roomId, password, useCustomServer: useCustom }); + await chrome.storage.local.set({ serverUrl, roomId, password, useCustomServer: useCustom }); elements.roomId.value = roomId; // Tell background to connect @@ -1111,7 +1139,7 @@ elements.joinBtn.addEventListener('click', async () => { elements.leaveBtn.addEventListener('click', async () => { clearConnectionErrorTimer(); chrome.runtime.sendMessage({ type: 'LEAVE_ROOM' }); - await chrome.storage.sync.set({ roomId: '', password: '' }); + await chrome.storage.local.set({ roomId: '', password: '' }); elements.roomId.value = ''; elements.password.value = ''; lastKnownPeers = []; @@ -1501,7 +1529,7 @@ chrome.runtime.onMessage.addListener((msg) => { if (msg.success) { // Final confirmation of join from background - chrome.storage.sync.get(['roomId', 'password', 'useCustomServer', 'serverUrl'], (data) => { + chrome.storage.local.get(['roomId', 'password', 'useCustomServer', 'serverUrl'], (data) => { updateUI(data.roomId, data.password, data.useCustomServer, data.serverUrl); }); } else { diff --git a/package.json b/package.json index 6951526..2f2df48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "koalasync", - "version": "2.1.1", + "version": "2.1.2", "description": "KoalaSync Build Scripts", "private": true, "scripts": {