475 lines
22 KiB
HTML
475 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<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">
|
|
<div data-role="content">
|
|
<div class="content-primary">
|
|
<h2>JellyLMS Configuration</h2>
|
|
<p>Configure the connection between Jellyfin and Logitech Media Server (LMS) for multi-room audio playback.</p>
|
|
|
|
<form id="JellyLmsConfigForm">
|
|
<div class="verticalSection">
|
|
<h3>LMS Server Settings</h3>
|
|
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="LmsServerUrl">LMS Server URL</label>
|
|
<input id="LmsServerUrl" name="LmsServerUrl" type="url" is="emby-input" />
|
|
<div class="fieldDescription">The URL of your LMS server (e.g., http://192.168.1.100:9000)</div>
|
|
</div>
|
|
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="LmsUsername">LMS Username (optional)</label>
|
|
<input id="LmsUsername" name="LmsUsername" type="text" is="emby-input" />
|
|
<div class="fieldDescription">Username if LMS authentication is enabled</div>
|
|
</div>
|
|
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="LmsPassword">LMS Password (optional)</label>
|
|
<input id="LmsPassword" name="LmsPassword" type="password" is="emby-input" />
|
|
<div class="fieldDescription">Password if LMS authentication is enabled</div>
|
|
</div>
|
|
|
|
<div>
|
|
<button is="emby-button" type="button" id="btnTestConnection" class="raised button-alt block emby-button">
|
|
<span>Test Connection</span>
|
|
</button>
|
|
<div id="connectionStatus" style="margin-top: 10px;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="verticalSection">
|
|
<h3>Jellyfin Server Settings</h3>
|
|
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="JellyfinServerUrl">Jellyfin Server URL</label>
|
|
<input id="JellyfinServerUrl" name="JellyfinServerUrl" type="url" is="emby-input" />
|
|
<div class="fieldDescription">The URL that LMS will use to stream audio from Jellyfin. This must be accessible from the LMS server (e.g., http://192.168.1.4:8096).</div>
|
|
</div>
|
|
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="JellyfinApiKey">Jellyfin API Key</label>
|
|
<input id="JellyfinApiKey" name="JellyfinApiKey" type="password" is="emby-input" />
|
|
<div class="fieldDescription">API key for LMS to authenticate with Jellyfin. Create one in Dashboard > API Keys.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="verticalSection">
|
|
<h3>Playback Settings</h3>
|
|
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="ConnectionTimeoutSeconds">Connection Timeout (seconds)</label>
|
|
<input id="ConnectionTimeoutSeconds" name="ConnectionTimeoutSeconds" type="number" is="emby-input" min="5" max="60" />
|
|
<div class="fieldDescription">Timeout for LMS API requests</div>
|
|
</div>
|
|
|
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
<label class="emby-checkbox-label">
|
|
<input id="EnableAutoSync" name="EnableAutoSync" type="checkbox" is="emby-checkbox" />
|
|
<span>Enable Auto-Sync</span>
|
|
</label>
|
|
<div class="fieldDescription checkboxFieldDescription">Automatically sync players when playing to multiple devices</div>
|
|
</div>
|
|
|
|
<div class="inputContainer" style="margin-top: 15px;">
|
|
<label class="inputLabel inputLabelUnfocused" for="DefaultPlayerMac">Default Player</label>
|
|
<select is="emby-select" id="DefaultPlayerMac" name="DefaultPlayerMac" class="emby-select-withcolor emby-select">
|
|
<option value="">None</option>
|
|
</select>
|
|
<div class="fieldDescription">Default player to use when none is specified</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="verticalSection">
|
|
<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 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>
|
|
|
|
<div>
|
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
|
<span>Save</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<script type="text/javascript">
|
|
var JellyLmsConfig = {
|
|
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 defaultSelect = document.querySelector('#DefaultPlayerMac');
|
|
var currentDefault = defaultSelect.value;
|
|
|
|
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>';
|
|
|
|
players.forEach(function(player) {
|
|
var option = document.createElement('option');
|
|
option.value = player.MacAddress;
|
|
option.text = player.Name;
|
|
if (player.MacAddress === currentDefault) {
|
|
option.selected = true;
|
|
}
|
|
defaultSelect.appendChild(option);
|
|
});
|
|
|
|
renderPlayerList();
|
|
}).catch(function(err) {
|
|
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>';
|
|
|
|
ApiClient.ajax({
|
|
url: ApiClient.getUrl('JellyLms/TestConnection'),
|
|
type: 'POST',
|
|
dataType: 'json'
|
|
}).then(function(result) {
|
|
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>';
|
|
}
|
|
}).catch(function(err) {
|
|
statusDiv.innerHTML = '<span style="color: red;">Connection failed. Check the server URL.</span>';
|
|
console.error('Connection test failed:', err);
|
|
});
|
|
}
|
|
|
|
document.querySelector('#JellyLmsConfigPage')
|
|
.addEventListener('pageshow', function() {
|
|
Dashboard.showLoadingMsg();
|
|
ApiClient.getPluginConfiguration(JellyLmsConfig.pluginUniqueId).then(function (config) {
|
|
document.querySelector('#LmsServerUrl').value = config.LmsServerUrl || 'http://localhost:9000';
|
|
document.querySelector('#LmsUsername').value = config.LmsUsername || '';
|
|
document.querySelector('#LmsPassword').value = config.LmsPassword || '';
|
|
document.querySelector('#JellyfinServerUrl').value = config.JellyfinServerUrl || 'http://localhost:8096';
|
|
document.querySelector('#JellyfinApiKey').value = config.JellyfinApiKey || '';
|
|
document.querySelector('#ConnectionTimeoutSeconds').value = config.ConnectionTimeoutSeconds || 10;
|
|
document.querySelector('#EnableAutoSync').checked = config.EnableAutoSync !== false;
|
|
document.querySelector('#DefaultPlayerMac').value = config.DefaultPlayerMac || '';
|
|
Dashboard.hideLoadingMsg();
|
|
|
|
loadPlayers();
|
|
loadSyncGroups();
|
|
});
|
|
});
|
|
|
|
document.querySelector('#btnTestConnection')
|
|
.addEventListener('click', function() {
|
|
ApiClient.getPluginConfiguration(JellyLmsConfig.pluginUniqueId).then(function (config) {
|
|
config.LmsServerUrl = document.querySelector('#LmsServerUrl').value;
|
|
config.LmsUsername = document.querySelector('#LmsUsername').value;
|
|
config.LmsPassword = document.querySelector('#LmsPassword').value;
|
|
ApiClient.updatePluginConfiguration(JellyLmsConfig.pluginUniqueId, config).then(function() {
|
|
testConnection();
|
|
});
|
|
});
|
|
});
|
|
|
|
document.querySelector('#btnRefreshPlayers')
|
|
.addEventListener('click', function() {
|
|
loadPlayers();
|
|
loadSyncGroups();
|
|
});
|
|
|
|
document.querySelector('#btnSyncSelected')
|
|
.addEventListener('click', function() {
|
|
syncSelected();
|
|
});
|
|
|
|
document.querySelector('#JellyLmsConfigForm')
|
|
.addEventListener('submit', function(e) {
|
|
Dashboard.showLoadingMsg();
|
|
ApiClient.getPluginConfiguration(JellyLmsConfig.pluginUniqueId).then(function (config) {
|
|
config.LmsServerUrl = document.querySelector('#LmsServerUrl').value;
|
|
config.LmsUsername = document.querySelector('#LmsUsername').value;
|
|
config.LmsPassword = document.querySelector('#LmsPassword').value;
|
|
config.JellyfinServerUrl = document.querySelector('#JellyfinServerUrl').value;
|
|
config.JellyfinApiKey = document.querySelector('#JellyfinApiKey').value;
|
|
config.ConnectionTimeoutSeconds = parseInt(document.querySelector('#ConnectionTimeoutSeconds').value) || 10;
|
|
config.EnableAutoSync = document.querySelector('#EnableAutoSync').checked;
|
|
config.DefaultPlayerMac = document.querySelector('#DefaultPlayerMac').value;
|
|
ApiClient.updatePluginConfiguration(JellyLmsConfig.pluginUniqueId, config).then(function (result) {
|
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
|
});
|
|
});
|
|
|
|
e.preventDefault();
|
|
return false;
|
|
});
|
|
</script>
|
|
</div>
|
|
</body>
|
|
</html>
|