update readme
Build Plugin / build (push) Successful in 2m5s
Release Plugin / build-and-release (push) Failing after 8s

improve scrubbing
improve syncing gui
This commit is contained in:
2025-12-14 16:16:33 +01:00
parent a8e0dcaf37
commit c967b062a2
11 changed files with 383 additions and 561 deletions
@@ -3,6 +3,64 @@
<head>
<meta charset="utf-8">
<title>JellyLMS</title>
<style>
.sync-group {
border: 2px solid #00a4dc;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
background: rgba(0, 164, 220, 0.05);
}
.sync-group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.sync-group-players {
color: #ccc;
}
.player-sync-row {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #333;
}
.player-sync-row:last-child {
border-bottom: none;
}
.player-sync-checkbox {
margin-right: 12px;
}
.player-sync-name {
flex: 1;
font-weight: 500;
}
.player-sync-status {
display: flex;
align-items: center;
gap: 6px;
color: #888;
font-size: 0.9em;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-dot.on { background: #52b54b; }
.status-dot.standby { background: #f9a825; }
.status-dot.off { background: #f44336; }
.sync-actions {
margin-top: 15px;
display: flex;
gap: 10px;
align-items: center;
}
#syncStatus {
margin-left: 10px;
}
</style>
</head>
<body>
<div id="JellyLmsConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
@@ -73,18 +131,6 @@
</label>
<div class="fieldDescription checkboxFieldDescription">Automatically sync players when playing to multiple devices</div>
</div>
</div>
<div class="verticalSection">
<h3>LMS Players</h3>
<div id="playersList">
<p>Click "Refresh Players" to discover LMS players.</p>
</div>
<div>
<button is="emby-button" type="button" id="btnRefreshPlayers" class="raised button-alt block emby-button">
<span>Refresh Players</span>
</button>
</div>
<div class="inputContainer" style="margin-top: 15px;">
<label class="inputLabel inputLabelUnfocused" for="DefaultPlayerMac">Default Player</label>
@@ -96,15 +142,25 @@
</div>
<div class="verticalSection">
<h3>Player Sync Management</h3>
<p>Manage synchronized playback groups for multi-room audio.</p>
<div>
<a is="emby-button" href="configurationpage?name=JellyLMS%20Sync" class="raised button-alt block emby-button" style="display: inline-block; text-decoration: none;">
<span>Open Sync Manager</span>
</a>
<h3>Player Sync</h3>
<p class="fieldDescription">Select players to sync together for multi-room audio. Synced players play in perfect sync.</p>
<div id="currentSyncGroups">
<!-- Existing sync groups shown here -->
</div>
<div class="fieldDescription" style="margin-top: 10px;">
Users can access this page directly at: <code>/web/configurationpage?name=JellyLMS%20Sync</code>
<div id="playerSyncList">
<p>Loading players...</p>
</div>
<div class="sync-actions">
<button is="emby-button" type="button" id="btnSyncSelected" class="raised button-submit emby-button" disabled>
<span>Sync Selected</span>
</button>
<button is="emby-button" type="button" id="btnRefreshPlayers" class="raised button-alt emby-button">
<span>Refresh</span>
</button>
<span id="syncStatus"></span>
</div>
</div>
@@ -118,45 +174,34 @@
</div>
<script type="text/javascript">
var JellyLmsConfig = {
pluginUniqueId: 'a5b8c9d0-1e2f-3a4b-5c6d-7e8f9a0b1c2d'
pluginUniqueId: 'a5b8c9d0-1e2f-3a4b-5c6d-7e8f9a0b1c2d',
players: [],
syncGroups: []
};
function getStatusClass(player) {
if (!player.IsConnected) return 'off';
return player.IsPoweredOn ? 'on' : 'standby';
}
function getStatusText(player) {
if (!player.IsConnected) return 'Disconnected';
return player.IsPoweredOn ? 'On' : 'Standby';
}
function loadPlayers() {
var playersList = document.querySelector('#playersList');
var defaultSelect = document.querySelector('#DefaultPlayerMac');
var currentDefault = defaultSelect.value;
playersList.innerHTML = '<p>Loading players...</p>';
ApiClient.ajax({
url: ApiClient.getUrl('JellyLms/Players', { refresh: true }),
type: 'GET',
dataType: 'json'
}).then(function(players) {
JellyLmsConfig.players = players || [];
defaultSelect.innerHTML = '<option value="">None</option>';
if (!players || players.length === 0) {
playersList.innerHTML = '<p>No players found. Make sure LMS is running and has connected players.</p>';
return;
}
var html = '<table class="tblGenres detailTable" style="width: 100%;">';
html += '<thead><tr><th>Name</th><th>Model</th><th>Status</th><th>Volume</th><th>Synced</th></tr></thead>';
html += '<tbody>';
players.forEach(function(player) {
var status = player.IsConnected ? (player.IsPoweredOn ? 'On' : 'Standby') : 'Disconnected';
var statusColor = player.IsConnected ? (player.IsPoweredOn ? 'green' : 'orange') : 'red';
var syncStatus = player.IsSynced ? 'Yes' : 'No';
html += '<tr>';
html += '<td>' + player.Name + '</td>';
html += '<td>' + player.Model + '</td>';
html += '<td style="color: ' + statusColor + ';">' + status + '</td>';
html += '<td>' + player.Volume + '%</td>';
html += '<td>' + syncStatus + '</td>';
html += '</tr>';
var option = document.createElement('option');
option.value = player.MacAddress;
option.text = player.Name;
@@ -166,14 +211,179 @@
defaultSelect.appendChild(option);
});
html += '</tbody></table>';
playersList.innerHTML = html;
renderPlayerList();
}).catch(function(err) {
playersList.innerHTML = '<p style="color: red;">Error loading players. Check LMS connection.</p>';
document.querySelector('#playerSyncList').innerHTML = '<p style="color: red;">Error loading players. Check LMS connection.</p>';
console.error('Error loading players:', err);
});
}
function loadSyncGroups() {
ApiClient.ajax({
url: ApiClient.getUrl('JellyLms/SyncGroups'),
type: 'GET',
dataType: 'json'
}).then(function(groups) {
JellyLmsConfig.syncGroups = groups || [];
renderSyncGroups();
renderPlayerList();
}).catch(function(err) {
console.error('Error loading sync groups:', err);
});
}
function renderSyncGroups() {
var container = document.querySelector('#currentSyncGroups');
var groups = JellyLmsConfig.syncGroups;
if (!groups || groups.length === 0) {
container.innerHTML = '';
return;
}
var html = '';
groups.forEach(function(group) {
var playerNames = [];
var masterPlayer = JellyLmsConfig.players.find(function(p) { return p.MacAddress === group.MasterMac; });
if (masterPlayer) playerNames.push(masterPlayer.Name);
group.SlaveMacs.forEach(function(mac) {
var player = JellyLmsConfig.players.find(function(p) { return p.MacAddress === mac; });
if (player) playerNames.push(player.Name);
});
html += '<div class="sync-group">';
html += '<div class="sync-group-header">';
html += '<span class="sync-group-players">' + playerNames.join(' + ') + '</span>';
html += '<button is="emby-button" type="button" class="raised button-alt emby-button btnUnsyncGroup" data-master="' + group.MasterMac + '">';
html += '<span>Unsync</span>';
html += '</button>';
html += '</div>';
html += '</div>';
});
container.innerHTML = html;
container.querySelectorAll('.btnUnsyncGroup').forEach(function(btn) {
btn.addEventListener('click', function() {
unsyncGroup(this.getAttribute('data-master'));
});
});
}
function renderPlayerList() {
var container = document.querySelector('#playerSyncList');
var players = JellyLmsConfig.players;
if (!players || players.length === 0) {
container.innerHTML = '<p>No players found. Make sure LMS is running.</p>';
updateSyncButton();
return;
}
// Get MACs of already synced players
var syncedMacs = new Set();
JellyLmsConfig.syncGroups.forEach(function(group) {
syncedMacs.add(group.MasterMac);
group.SlaveMacs.forEach(function(mac) { syncedMacs.add(mac); });
});
// Only show unsynced players
var unsyncedPlayers = players.filter(function(p) {
return !syncedMacs.has(p.MacAddress);
});
if (unsyncedPlayers.length === 0) {
container.innerHTML = '<p>All players are synced.</p>';
updateSyncButton();
return;
}
var html = '';
unsyncedPlayers.forEach(function(player) {
var statusClass = getStatusClass(player);
html += '<div class="player-sync-row">';
html += '<input type="checkbox" class="player-sync-checkbox" data-mac="' + player.MacAddress + '" id="sync-' + player.MacAddress + '">';
html += '<label class="player-sync-name" for="sync-' + player.MacAddress + '">' + player.Name + '</label>';
html += '<div class="player-sync-status">';
html += '<span class="status-dot ' + statusClass + '"></span>';
html += '<span>' + getStatusText(player) + '</span>';
html += '</div>';
html += '</div>';
});
container.innerHTML = html;
// Add change listeners
container.querySelectorAll('.player-sync-checkbox').forEach(function(cb) {
cb.addEventListener('change', updateSyncButton);
});
updateSyncButton();
}
function getSelectedMacs() {
var checkboxes = document.querySelectorAll('.player-sync-checkbox:checked');
var macs = [];
checkboxes.forEach(function(cb) {
macs.push(cb.getAttribute('data-mac'));
});
return macs;
}
function updateSyncButton() {
var btn = document.querySelector('#btnSyncSelected');
var selected = getSelectedMacs();
btn.disabled = selected.length < 2;
}
function syncSelected() {
var macs = getSelectedMacs();
if (macs.length < 2) return;
var statusDiv = document.querySelector('#syncStatus');
statusDiv.innerHTML = '<span style="color: orange;">Syncing...</span>';
// First MAC becomes master (arbitrary, user doesn't need to know)
var masterMac = macs[0];
var slaveMacs = macs.slice(1);
ApiClient.ajax({
url: ApiClient.getUrl('JellyLms/SyncGroups'),
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
MasterMac: masterMac,
SlaveMacs: slaveMacs
})
}).then(function() {
statusDiv.innerHTML = '<span style="color: green;">Synced!</span>';
setTimeout(function() {
statusDiv.innerHTML = '';
loadSyncGroups();
}, 1500);
}).catch(function(err) {
statusDiv.innerHTML = '<span style="color: red;">Failed to sync.</span>';
console.error('Error syncing:', err);
});
}
function unsyncGroup(masterMac) {
var statusDiv = document.querySelector('#syncStatus');
statusDiv.innerHTML = '<span style="color: orange;">Unsyncing...</span>';
ApiClient.ajax({
url: ApiClient.getUrl('JellyLms/SyncGroups/' + encodeURIComponent(masterMac)),
type: 'DELETE'
}).then(function() {
statusDiv.innerHTML = '';
loadSyncGroups();
}).catch(function(err) {
statusDiv.innerHTML = '<span style="color: red;">Failed to unsync.</span>';
console.error('Error unsyncing:', err);
});
}
function testConnection() {
var statusDiv = document.querySelector('#connectionStatus');
statusDiv.innerHTML = '<span style="color: orange;">Testing connection...</span>';
@@ -186,6 +396,7 @@
if (result.IsConnected) {
statusDiv.innerHTML = '<span style="color: green;">Connected! Found ' + result.PlayerCount + ' player(s).</span>';
loadPlayers();
loadSyncGroups();
} else {
statusDiv.innerHTML = '<span style="color: red;">Connection failed: ' + (result.LastError || 'Unknown error') + '</span>';
}
@@ -208,6 +419,9 @@
document.querySelector('#EnableAutoSync').checked = config.EnableAutoSync !== false;
document.querySelector('#DefaultPlayerMac').value = config.DefaultPlayerMac || '';
Dashboard.hideLoadingMsg();
loadPlayers();
loadSyncGroups();
});
});
@@ -226,6 +440,12 @@
document.querySelector('#btnRefreshPlayers')
.addEventListener('click', function() {
loadPlayers();
loadSyncGroups();
});
document.querySelector('#btnSyncSelected')
.addEventListener('click', function() {
syncSelected();
});
document.querySelector('#JellyLmsConfigForm')