jellyLMS/Jellyfin.Plugin.JellyLMS/Api/JellyLmsController.cs
Duncan Tourolle 2f5a182afd
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
📝 Create/Update Release Draft & Release Bump PR / call (push) Failing after 0s
🔬 Run CodeQL / call (push) Failing after 0s
🧪 Test Plugin / call (push) Failing after 0s
First POC with working playback
2025-12-13 23:54:33 +01:00

353 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Plugin.JellyLMS.Models;
using Jellyfin.Plugin.JellyLMS.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.JellyLMS.Api;
/// <summary>
/// REST API controller for JellyLMS operations.
/// </summary>
[ApiController]
[Route("JellyLms")]
[Authorize]
[Produces(MediaTypeNames.Application.Json)]
public class JellyLmsController : ControllerBase
{
private readonly ILmsApiClient _lmsClient;
private readonly LmsPlayerManager _playerManager;
private readonly LmsSessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="JellyLmsController"/> class.
/// </summary>
/// <param name="lmsClient">The LMS API client.</param>
/// <param name="playerManager">The player manager.</param>
/// <param name="sessionManager">The session manager.</param>
public JellyLmsController(
ILmsApiClient lmsClient,
LmsPlayerManager playerManager,
LmsSessionManager sessionManager)
{
_lmsClient = lmsClient;
_playerManager = playerManager;
_sessionManager = sessionManager;
}
/// <summary>
/// Tests the connection to the LMS server.
/// </summary>
/// <returns>The connection status.</returns>
[HttpPost("TestConnection")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<LmsServerStatus>> TestConnection()
{
var status = await _lmsClient.TestConnectionAsync().ConfigureAwait(false);
return Ok(status);
}
/// <summary>
/// Gets all LMS players.
/// </summary>
/// <param name="refresh">Force refresh from LMS.</param>
/// <returns>List of players.</returns>
[HttpGet("Players")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<List<LmsPlayer>>> GetPlayers([FromQuery] bool refresh = false)
{
var players = await _playerManager.GetPlayersAsync(refresh).ConfigureAwait(false);
return Ok(players);
}
/// <summary>
/// Gets a specific player by MAC address.
/// </summary>
/// <param name="mac">The player's MAC address.</param>
/// <returns>The player details.</returns>
[HttpGet("Players/{mac}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<LmsPlayer>> GetPlayer(string mac)
{
var player = await _playerManager.GetPlayerAsync(mac).ConfigureAwait(false);
if (player == null)
{
return NotFound();
}
return Ok(player);
}
/// <summary>
/// Powers on a player.
/// </summary>
/// <param name="mac">The player's MAC address.</param>
/// <returns>Success status.</returns>
[HttpPost("Players/{mac}/PowerOn")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> PowerOn(string mac)
{
var success = await _lmsClient.PowerOnAsync(mac).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to power on player");
}
/// <summary>
/// Powers off a player.
/// </summary>
/// <param name="mac">The player's MAC address.</param>
/// <returns>Success status.</returns>
[HttpPost("Players/{mac}/PowerOff")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> PowerOff(string mac)
{
var success = await _lmsClient.PowerOffAsync(mac).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to power off player");
}
/// <summary>
/// Sets the volume on a player.
/// </summary>
/// <param name="mac">The player's MAC address.</param>
/// <param name="request">The volume request.</param>
/// <returns>Success status.</returns>
[HttpPost("Players/{mac}/Volume")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> SetVolume(string mac, [FromBody] VolumeRequest request)
{
var success = await _lmsClient.SetVolumeAsync(mac, request.Volume).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to set volume");
}
/// <summary>
/// Gets all sync groups.
/// </summary>
/// <returns>List of sync groups.</returns>
[HttpGet("SyncGroups")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<List<SyncGroup>>> GetSyncGroups()
{
var groups = await _playerManager.GetSyncGroupsAsync().ConfigureAwait(false);
return Ok(groups);
}
/// <summary>
/// Creates a sync group.
/// </summary>
/// <param name="request">The sync request.</param>
/// <returns>Success status.</returns>
[HttpPost("SyncGroups")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> CreateSyncGroup([FromBody] CreateSyncGroupRequest request)
{
var success = await _playerManager.CreateSyncGroupAsync(request.MasterMac, request.SlaveMacs)
.ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to create sync group");
}
/// <summary>
/// Removes a player from its sync group.
/// </summary>
/// <param name="mac">The player's MAC address.</param>
/// <returns>Success status.</returns>
[HttpDelete("SyncGroups/Players/{mac}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> UnsyncPlayer(string mac)
{
var success = await _playerManager.UnsyncPlayerAsync(mac).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to unsync player");
}
/// <summary>
/// Dissolves an entire sync group.
/// </summary>
/// <param name="masterMac">The master player's MAC address.</param>
/// <returns>Success status.</returns>
[HttpDelete("SyncGroups/{masterMac}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> DissolveSyncGroup(string masterMac)
{
var success = await _playerManager.DissolveSyncGroupAsync(masterMac).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to dissolve sync group");
}
/// <summary>
/// Gets all active playback sessions.
/// </summary>
/// <returns>List of active sessions.</returns>
[HttpGet("Sessions")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<List<LmsPlaybackSession>> GetSessions()
{
return Ok(_sessionManager.GetActiveSessions());
}
/// <summary>
/// Starts playback of a Jellyfin item on LMS players.
/// </summary>
/// <param name="request">The playback request.</param>
/// <returns>The created session.</returns>
[HttpPost("Sessions/Play")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<LmsPlaybackSession>> StartPlayback([FromBody] StartPlaybackRequest request)
{
var session = await _sessionManager.StartPlaybackAsync(request.ItemId, request.PlayerMacs, request.UserId)
.ConfigureAwait(false);
if (session == null)
{
return BadRequest("Failed to start playback");
}
return Ok(session);
}
/// <summary>
/// Pauses a playback session.
/// </summary>
/// <param name="sessionId">The session ID.</param>
/// <returns>Success status.</returns>
[HttpPost("Sessions/{sessionId}/Pause")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PauseSession(string sessionId)
{
var success = await _sessionManager.PauseSessionAsync(sessionId).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
/// <summary>
/// Resumes a paused playback session.
/// </summary>
/// <param name="sessionId">The session ID.</param>
/// <returns>Success status.</returns>
[HttpPost("Sessions/{sessionId}/Resume")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> ResumeSession(string sessionId)
{
var success = await _sessionManager.ResumeSessionAsync(sessionId).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
/// <summary>
/// Stops a playback session.
/// </summary>
/// <param name="sessionId">The session ID.</param>
/// <returns>Success status.</returns>
[HttpPost("Sessions/{sessionId}/Stop")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> StopSession(string sessionId)
{
var success = await _sessionManager.StopSessionAsync(sessionId).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
/// <summary>
/// Seeks to a position in the playback session.
/// </summary>
/// <param name="sessionId">The session ID.</param>
/// <param name="request">The seek request.</param>
/// <returns>Success status.</returns>
[HttpPost("Sessions/{sessionId}/Seek")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> SeekSession(string sessionId, [FromBody] SeekRequest request)
{
var success = await _sessionManager.SeekAsync(sessionId, request.PositionTicks).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
/// <summary>
/// Sets the volume for all players in a session.
/// </summary>
/// <param name="sessionId">The session ID.</param>
/// <param name="request">The volume request.</param>
/// <returns>Success status.</returns>
[HttpPost("Sessions/{sessionId}/Volume")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> SetSessionVolume(string sessionId, [FromBody] VolumeRequest request)
{
var success = await _sessionManager.SetVolumeAsync(sessionId, request.Volume).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
}
/// <summary>
/// Request to set volume.
/// </summary>
public class VolumeRequest
{
/// <summary>
/// Gets or sets the volume level (0-100).
/// </summary>
[Range(0, 100)]
public int Volume { get; set; }
}
/// <summary>
/// Request to create a sync group.
/// </summary>
public class CreateSyncGroupRequest
{
/// <summary>
/// Gets or sets the master player MAC address.
/// </summary>
[Required]
public string MasterMac { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the slave player MAC addresses.
/// </summary>
[Required]
public List<string> SlaveMacs { get; set; } = [];
}
/// <summary>
/// Request to start playback.
/// </summary>
public class StartPlaybackRequest
{
/// <summary>
/// Gets or sets the Jellyfin item ID.
/// </summary>
[Required]
public Guid ItemId { get; set; }
/// <summary>
/// Gets or sets the LMS player MAC addresses.
/// </summary>
[Required]
public List<string> PlayerMacs { get; set; } = [];
/// <summary>
/// Gets or sets the optional user ID.
/// </summary>
public Guid? UserId { get; set; }
}
/// <summary>
/// Request to seek to a position.
/// </summary>
public class SeekRequest
{
/// <summary>
/// Gets or sets the position in ticks.
/// </summary>
public long PositionTicks { get; set; }
}