using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Plugin.JellyLMS.Models;
using Jellyfin.Plugin.JellyLMS.Services;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.JellyLMS.Api;
///
/// REST API controller for JellyLMS operations.
///
[ApiController]
[Route("JellyLms")]
[Authorize]
[Produces(MediaTypeNames.Application.Json)]
public class JellyLmsController : ControllerBase
{
private readonly ILmsApiClient _lmsClient;
private readonly LmsPlayerManager _playerManager;
private readonly LmsSessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
///
/// Initializes a new instance of the class.
///
/// The LMS API client.
/// The player manager.
/// The session manager.
/// The library manager.
public JellyLmsController(
ILmsApiClient lmsClient,
LmsPlayerManager playerManager,
LmsSessionManager sessionManager,
ILibraryManager libraryManager)
{
_lmsClient = lmsClient;
_playerManager = playerManager;
_sessionManager = sessionManager;
_libraryManager = libraryManager;
}
///
/// Tests the connection to the LMS server.
///
/// The connection status.
[HttpPost("TestConnection")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task> TestConnection()
{
var status = await _lmsClient.TestConnectionAsync().ConfigureAwait(false);
return Ok(status);
}
///
/// Gets all LMS players.
///
/// Force refresh from LMS.
/// List of players.
[HttpGet("Players")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task>> GetPlayers([FromQuery] bool refresh = false)
{
var players = await _playerManager.GetPlayersAsync(refresh).ConfigureAwait(false);
return Ok(players);
}
///
/// Gets a specific player by MAC address.
///
/// The player's MAC address.
/// The player details.
[HttpGet("Players/{mac}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task> GetPlayer(string mac)
{
var player = await _playerManager.GetPlayerAsync(mac).ConfigureAwait(false);
if (player == null)
{
return NotFound();
}
return Ok(player);
}
///
/// Powers on a player.
///
/// The player's MAC address.
/// Success status.
[HttpPost("Players/{mac}/PowerOn")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task PowerOn(string mac)
{
var success = await _lmsClient.PowerOnAsync(mac).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to power on player");
}
///
/// Powers off a player.
///
/// The player's MAC address.
/// Success status.
[HttpPost("Players/{mac}/PowerOff")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task PowerOff(string mac)
{
var success = await _lmsClient.PowerOffAsync(mac).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to power off player");
}
///
/// Sets the volume on a player.
///
/// The player's MAC address.
/// The volume request.
/// Success status.
[HttpPost("Players/{mac}/Volume")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task SetVolume(string mac, [FromBody] VolumeRequest request)
{
var success = await _lmsClient.SetVolumeAsync(mac, request.Volume).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to set volume");
}
///
/// Gets all sync groups.
///
/// List of sync groups.
[HttpGet("SyncGroups")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task>> GetSyncGroups()
{
var groups = await _playerManager.GetSyncGroupsAsync().ConfigureAwait(false);
return Ok(groups);
}
///
/// Creates a sync group.
///
/// The sync request.
/// Success status.
[HttpPost("SyncGroups")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task CreateSyncGroup([FromBody] CreateSyncGroupRequest request)
{
var success = await _playerManager.CreateSyncGroupAsync(request.MasterMac, request.SlaveMacs)
.ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to create sync group");
}
///
/// Removes a player from its sync group.
///
/// The player's MAC address.
/// Success status.
[HttpDelete("SyncGroups/Players/{mac}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task UnsyncPlayer(string mac)
{
var success = await _playerManager.UnsyncPlayerAsync(mac).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to unsync player");
}
///
/// Dissolves an entire sync group.
///
/// The master player's MAC address.
/// Success status.
[HttpDelete("SyncGroups/{masterMac}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task DissolveSyncGroup(string masterMac)
{
var success = await _playerManager.DissolveSyncGroupAsync(masterMac).ConfigureAwait(false);
return success ? Ok() : BadRequest("Failed to dissolve sync group");
}
///
/// Gets all active playback sessions.
///
/// List of active sessions.
[HttpGet("Sessions")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetSessions()
{
return Ok(_sessionManager.GetActiveSessions());
}
///
/// Starts playback of a Jellyfin item on LMS players.
///
/// The playback request.
/// The created session.
[HttpPost("Sessions/Play")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task> 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);
}
///
/// Pauses a playback session.
///
/// The session ID.
/// Success status.
[HttpPost("Sessions/{sessionId}/Pause")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task PauseSession(string sessionId)
{
var success = await _sessionManager.PauseSessionAsync(sessionId).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
///
/// Resumes a paused playback session.
///
/// The session ID.
/// Success status.
[HttpPost("Sessions/{sessionId}/Resume")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task ResumeSession(string sessionId)
{
var success = await _sessionManager.ResumeSessionAsync(sessionId).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
///
/// Stops a playback session.
///
/// The session ID.
/// Success status.
[HttpPost("Sessions/{sessionId}/Stop")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task StopSession(string sessionId)
{
var success = await _sessionManager.StopSessionAsync(sessionId).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
///
/// Seeks to a position in the playback session.
///
/// The session ID.
/// The seek request.
/// Success status.
[HttpPost("Sessions/{sessionId}/Seek")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task SeekSession(string sessionId, [FromBody] SeekRequest request)
{
var success = await _sessionManager.SeekAsync(sessionId, request.PositionTicks).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
///
/// Sets the volume for all players in a session.
///
/// The session ID.
/// The volume request.
/// Success status.
[HttpPost("Sessions/{sessionId}/Volume")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task SetSessionVolume(string sessionId, [FromBody] VolumeRequest request)
{
var success = await _sessionManager.SetVolumeAsync(sessionId, request.Volume).ConfigureAwait(false);
return success ? Ok() : NotFound();
}
///
/// Discovers file paths used by Jellyfin's music libraries.
/// Helps users configure path mappings for direct file access.
///
/// Sample file paths from each music library.
[HttpGet("DiscoverPaths")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult DiscoverPaths()
{
var response = new DiscoveredPathsResponse();
// Get sample audio files from the library
var query = new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Audio],
Limit = 50,
Recursive = true
};
var items = _libraryManager.GetItemsResult(query).Items;
// Extract unique path prefixes
var pathPrefixes = new HashSet();
foreach (var item in items)
{
if (string.IsNullOrEmpty(item.Path))
{
continue;
}
// Add sample paths
if (response.SamplePaths.Count < 5)
{
response.SamplePaths.Add(item.Path);
}
// Try to find common path prefixes
var path = item.Path.Replace('\\', '/');
var parts = path.Split('/');
// Build prefix from first few directory levels
if (parts.Length > 2)
{
// Try different prefix lengths to find common ones
for (var i = 2; i <= Math.Min(4, parts.Length - 1); i++)
{
var prefix = string.Join('/', parts.Take(i));
if (!string.IsNullOrEmpty(prefix) && !prefix.Contains('.', StringComparison.Ordinal))
{
pathPrefixes.Add(prefix);
}
}
}
}
// Sort prefixes by length (shorter = more general)
response.DetectedPrefixes = pathPrefixes
.OrderBy(p => p.Length)
.Take(10)
.ToList();
return Ok(response);
}
}
///
/// Response containing discovered file paths from Jellyfin libraries.
///
public class DiscoveredPathsResponse
{
///
/// Gets or sets sample file paths from the music library.
///
public List SamplePaths { get; set; } = [];
///
/// Gets or sets detected common path prefixes.
///
public List DetectedPrefixes { get; set; } = [];
}
///
/// Request to set volume.
///
public class VolumeRequest
{
///
/// Gets or sets the volume level (0-100).
///
[Range(0, 100)]
public int Volume { get; set; }
}
///
/// Request to create a sync group.
///
public class CreateSyncGroupRequest
{
///
/// Gets or sets the master player MAC address.
///
[Required]
public string MasterMac { get; set; } = string.Empty;
///
/// Gets or sets the slave player MAC addresses.
///
[Required]
public List SlaveMacs { get; set; } = [];
}
///
/// Request to start playback.
///
public class StartPlaybackRequest
{
///
/// Gets or sets the Jellyfin item ID.
///
[Required]
public Guid ItemId { get; set; }
///
/// Gets or sets the LMS player MAC addresses.
///
[Required]
public List PlayerMacs { get; set; } = [];
///
/// Gets or sets the optional user ID.
///
public Guid? UserId { get; set; }
}
///
/// Request to seek to a position.
///
public class SeekRequest
{
///
/// Gets or sets the position in ticks.
///
public long PositionTicks { get; set; }
}