First POC with working playback
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using MediaBrowser.Model.Plugins;
|
||||
|
||||
namespace Jellyfin.Plugin.JellyLMS.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin configuration for JellyLMS.
|
||||
/// </summary>
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||
/// </summary>
|
||||
public PluginConfiguration()
|
||||
{
|
||||
LmsServerUrl = "http://localhost:9000";
|
||||
LmsUsername = string.Empty;
|
||||
LmsPassword = string.Empty;
|
||||
JellyfinServerUrl = "http://localhost:8096";
|
||||
ConnectionTimeoutSeconds = 10;
|
||||
EnableAutoSync = true;
|
||||
DefaultPlayerMac = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LMS server URL (e.g., http://192.168.1.100:9000).
|
||||
/// </summary>
|
||||
public string LmsServerUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LMS username (if authentication is enabled).
|
||||
/// </summary>
|
||||
public string LmsUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LMS password (if authentication is enabled).
|
||||
/// </summary>
|
||||
public string LmsPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Jellyfin server URL that LMS will use to stream audio.
|
||||
/// This should be accessible from the LMS server.
|
||||
/// </summary>
|
||||
public string JellyfinServerUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection timeout in seconds.
|
||||
/// </summary>
|
||||
public int ConnectionTimeoutSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to automatically sync players
|
||||
/// when playing to multiple devices.
|
||||
/// </summary>
|
||||
public bool EnableAutoSync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default player MAC address to use when none is specified.
|
||||
/// </summary>
|
||||
public string DefaultPlayerMac { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Jellyfin API key for authenticating stream requests from LMS.
|
||||
/// </summary>
|
||||
public string JellyfinApiKey { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>JellyLMS</title>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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'
|
||||
};
|
||||
|
||||
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) {
|
||||
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;
|
||||
if (player.MacAddress === currentDefault) {
|
||||
option.selected = true;
|
||||
}
|
||||
defaultSelect.appendChild(option);
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
playersList.innerHTML = html;
|
||||
}).catch(function(err) {
|
||||
playersList.innerHTML = '<p style="color: red;">Error loading players. Check LMS connection.</p>';
|
||||
console.error('Error loading players:', 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();
|
||||
} 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();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user