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; } }