First POC with working playback
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for LMS JSON-RPC API communication.
|
||||
/// </summary>
|
||||
public class LmsApiClient : ILmsApiClient, IDisposable
|
||||
{
|
||||
private readonly ILogger<LmsApiClient> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LmsApiClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public LmsApiClient(ILogger<LmsApiClient> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
private PluginConfiguration Config => Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||
|
||||
private string JsonRpcEndpoint => $"{Config.LmsServerUrl.TrimEnd('/')}/jsonrpc.js";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<LmsServerStatus> TestConnectionAsync()
|
||||
{
|
||||
var status = new LmsServerStatus();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await SendCommandAsync<PlayersListResult>("-", ["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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<LmsPlayer>> GetPlayersAsync()
|
||||
{
|
||||
var players = new List<LmsPlayer>();
|
||||
|
||||
try
|
||||
{
|
||||
// First get player count
|
||||
var countResult = await SendCommandAsync<PlayerCountResult>("-", ["player", "count", "?"]).ConfigureAwait(false);
|
||||
var count = countResult?.Count ?? 0;
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return players;
|
||||
}
|
||||
|
||||
// Then get player list
|
||||
var listResult = await SendCommandAsync<PlayersListResult>("-", ["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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PlayerStatusResult?> GetPlayerStatusAsync(string playerMac)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await SendCommandAsync<PlayerStatusResult>(playerMac, ["status", "-", "1", "tags:"])
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get status for player {Mac}", playerMac);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> PlayUrlAsync(string playerMac, string url, string? title = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// First clear the playlist and add the URL
|
||||
await SendCommandAsync<object>(playerMac, ["playlist", "clear"]).ConfigureAwait(false);
|
||||
await SendCommandAsync<object>(playerMac, ["playlist", "add", url]).ConfigureAwait(false);
|
||||
|
||||
// Set title if provided
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
await SendCommandAsync<object>(playerMac, ["playlist", "title", title]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Start playback
|
||||
await SendCommandAsync<object>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> PauseAsync(string playerMac)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync<object>(playerMac, ["pause", "1"]).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to pause player {Mac}", playerMac);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> PlayAsync(string playerMac)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync<object>(playerMac, ["play"]).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to resume player {Mac}", playerMac);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> StopAsync(string playerMac)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync<object>(playerMac, ["stop"]).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to stop player {Mac}", playerMac);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SetVolumeAsync(string playerMac, int volume)
|
||||
{
|
||||
try
|
||||
{
|
||||
volume = Math.Clamp(volume, 0, 100);
|
||||
await SendCommandAsync<object>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SeekAsync(string playerMac, double positionSeconds)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync<object>(playerMac, ["time", positionSeconds.ToString("F1", CultureInfo.InvariantCulture)])
|
||||
.ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to seek on player {Mac}", playerMac);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> PowerOnAsync(string playerMac)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync<object>(playerMac, ["power", "1"]).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to power on player {Mac}", playerMac);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> PowerOffAsync(string playerMac)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync<object>(playerMac, ["power", "0"]).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to power off player {Mac}", playerMac);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SyncPlayerAsync(string masterMac, string slaveMac)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync<object>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UnsyncPlayerAsync(string playerMac)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync<object>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<SyncGroup>> GetSyncGroupsAsync()
|
||||
{
|
||||
var groups = new List<SyncGroup>();
|
||||
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<T?> SendCommandAsync<T>(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<LmsJsonRpcResponse<T>>(content);
|
||||
|
||||
if (!string.IsNullOrEmpty(result?.Error))
|
||||
{
|
||||
throw new InvalidOperationException($"LMS API error: {result.Error}");
|
||||
}
|
||||
|
||||
return result != null ? result.Result : default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Whether to dispose managed resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user