mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
v2.1.2
- Fixed episode guard regex to support Sxx:Exx title format (Jellyfin/Emby) and any non-alphanumeric separator between season and episode numbers - Migrated username, roomId, password, serverUrl, useCustomServer from chrome.storage.sync to chrome.storage.local for per-device isolation, preventing automatic cross-device room joins with duplicate names - Added one-time migration from sync to local for existing users
This commit is contained in:
+6
-2
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white" alt="Chrome Extension"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="CHANGELOG.md"><b>New v2.1.1 Release!</b> — See what's changed</a></p>
|
||||
<p align="center"><a href="CHANGELOG.md"><b>New v2.1.2 Release!</b> — See what's changed</a></p>
|
||||
|
||||
<p align="center"><i>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 <b>Data Sovereignty</b> and <b>Performance</b>.</i></p>
|
||||
|
||||
|
||||
+33
-30
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
+55
-27
@@ -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 {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"description": "KoalaSync Build Scripts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user