using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Plugin.JellyLMS.Configuration; using Jellyfin.Plugin.JellyLMS.Models; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.JellyLMS.Services; /// /// HTTP client for LMS JSON-RPC API communication. /// public class LmsApiClient : ILmsApiClient, IDisposable { private readonly ILogger _logger; private readonly HttpClient _httpClient; private bool _disposed; /// /// Initializes a new instance of the class. /// /// The logger instance. public LmsApiClient(ILogger logger) { _logger = logger; _httpClient = new HttpClient(); } private PluginConfiguration Config => Plugin.Instance?.Configuration ?? new PluginConfiguration(); private string JsonRpcEndpoint => $"{Config.LmsServerUrl.TrimEnd('/')}/jsonrpc.js"; /// public async Task TestConnectionAsync() { var status = new LmsServerStatus(); try { var result = await SendCommandAsync("-", ["players", "0", "1"]).ConfigureAwait(false); status.IsConnected = result != null; status.PlayerCount = result?.Count ?? 0; status.Version = "Connected"; } catch (Exception ex) { status.IsConnected = false; status.LastError = ex.Message; _logger.LogError(ex, "Failed to connect to LMS at {Endpoint}", JsonRpcEndpoint); } return status; } /// public async Task> GetPlayersAsync() { var players = new List(); try { // First get player count var countResult = await SendCommandAsync("-", ["player", "count", "?"]).ConfigureAwait(false); var count = countResult?.Count ?? 0; if (count == 0) { return players; } // Then get player list var listResult = await SendCommandAsync("-", ["players", "0", count.ToString(CultureInfo.InvariantCulture)]).ConfigureAwait(false); if (listResult?.Players == null) { return players; } foreach (var p in listResult.Players) { var player = new LmsPlayer { Name = p.Name, MacAddress = p.PlayerId, IpAddress = p.Ip.Split(':')[0], // Remove port if present IsConnected = p.Connected == 1, IsPoweredOn = p.Power == 1, Model = p.ModelName }; // Get additional status for sync info var status = await GetPlayerStatusAsync(p.PlayerId).ConfigureAwait(false); if (status != null) { player.Volume = status.Volume; player.SyncMaster = status.SyncMaster; if (!string.IsNullOrEmpty(status.SyncSlaves)) { player.SyncSlaves = status.SyncSlaves.Split(',').ToList(); } } players.Add(player); } } catch (Exception ex) { _logger.LogError(ex, "Failed to get players from LMS"); } return players; } /// public async Task GetPlayerStatusAsync(string playerMac) { try { return await SendCommandAsync(playerMac, ["status", "-", "1", "tags:"]) .ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Failed to get status for player {Mac}", playerMac); return null; } } /// public async Task PlayUrlAsync(string playerMac, string url, string? title = null) { try { // First clear the playlist and add the URL await SendCommandAsync(playerMac, ["playlist", "clear"]).ConfigureAwait(false); await SendCommandAsync(playerMac, ["playlist", "add", url]).ConfigureAwait(false); // Set title if provided if (!string.IsNullOrEmpty(title)) { await SendCommandAsync(playerMac, ["playlist", "title", title]).ConfigureAwait(false); } // Start playback await SendCommandAsync(playerMac, ["play"]).ConfigureAwait(false); _logger.LogInformation("Started playback of {Url} on player {Mac}", url, playerMac); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to play URL on player {Mac}", playerMac); return false; } } /// public async Task PauseAsync(string playerMac) { try { await SendCommandAsync(playerMac, ["pause", "1"]).ConfigureAwait(false); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to pause player {Mac}", playerMac); return false; } } /// public async Task PlayAsync(string playerMac) { try { await SendCommandAsync(playerMac, ["play"]).ConfigureAwait(false); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to resume player {Mac}", playerMac); return false; } } /// public async Task StopAsync(string playerMac) { try { await SendCommandAsync(playerMac, ["stop"]).ConfigureAwait(false); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to stop player {Mac}", playerMac); return false; } } /// public async Task SetVolumeAsync(string playerMac, int volume) { try { volume = Math.Clamp(volume, 0, 100); await SendCommandAsync(playerMac, ["mixer", "volume", volume.ToString(CultureInfo.InvariantCulture)]) .ConfigureAwait(false); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to set volume on player {Mac}", playerMac); return false; } } /// public async Task SeekAsync(string playerMac, double positionSeconds) { try { _logger.LogInformation("LMS SeekAsync: player {Mac}, position {Seconds}s", playerMac, positionSeconds); await SendCommandAsync(playerMac, ["time", positionSeconds.ToString("F1", CultureInfo.InvariantCulture)]) .ConfigureAwait(false); _logger.LogInformation("LMS SeekAsync: command sent successfully"); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to seek on player {Mac}", playerMac); return false; } } /// public async Task PowerOnAsync(string playerMac) { try { await SendCommandAsync(playerMac, ["power", "1"]).ConfigureAwait(false); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to power on player {Mac}", playerMac); return false; } } /// public async Task PowerOffAsync(string playerMac) { try { await SendCommandAsync(playerMac, ["power", "0"]).ConfigureAwait(false); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to power off player {Mac}", playerMac); return false; } } /// public async Task SyncPlayerAsync(string masterMac, string slaveMac) { try { await SendCommandAsync(masterMac, ["sync", slaveMac]).ConfigureAwait(false); _logger.LogInformation("Synced player {Slave} to master {Master}", slaveMac, masterMac); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to sync player {Slave} to {Master}", slaveMac, masterMac); return false; } } /// public async Task UnsyncPlayerAsync(string playerMac) { try { await SendCommandAsync(playerMac, ["sync", "-"]).ConfigureAwait(false); _logger.LogInformation("Unsynced player {Mac}", playerMac); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to unsync player {Mac}", playerMac); return false; } } /// public async Task> GetSyncGroupsAsync() { var groups = new List(); var players = await GetPlayersAsync().ConfigureAwait(false); // Find all masters (players with slaves) var masters = players.Where(p => p.SyncSlaves.Count > 0).ToList(); foreach (var master in masters) { var group = new SyncGroup { MasterMac = master.MacAddress, MasterName = master.Name, SlaveMacs = master.SyncSlaves }; // Resolve slave names foreach (var slaveMac in master.SyncSlaves) { var slave = players.FirstOrDefault(p => p.MacAddress == slaveMac); if (slave != null) { group.SlaveNames.Add(slave.Name); } } groups.Add(group); } return groups; } private async Task SendCommandAsync(string playerMac, string[] command) { var request = new LmsJsonRpcRequest { Params = [playerMac, command] }; var response = await _httpClient.PostAsJsonAsync(JsonRpcEndpoint, request).ConfigureAwait(false); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var result = JsonSerializer.Deserialize>(content); if (!string.IsNullOrEmpty(result?.Error)) { throw new InvalidOperationException($"LMS API error: {result.Error}"); } return result != null ? result.Result : default; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Disposes managed resources. /// /// Whether to dispose managed resources. protected virtual void Dispose(bool disposing) { if (_disposed) { return; } if (disposing) { _httpClient.Dispose(); } _disposed = true; } }