All checks were successful
Build Plugin / build (push) Successful in 2m21s
444 lines
15 KiB
C#
444 lines
15 KiB
C#
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;
|
|
|
|
/// <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;
|
|
private readonly ILibraryManager _libraryManager;
|
|
|
|
/// <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>
|
|
/// <param name="libraryManager">The library manager.</param>
|
|
public JellyLmsController(
|
|
ILmsApiClient lmsClient,
|
|
LmsPlayerManager playerManager,
|
|
LmsSessionManager sessionManager,
|
|
ILibraryManager libraryManager)
|
|
{
|
|
_lmsClient = lmsClient;
|
|
_playerManager = playerManager;
|
|
_sessionManager = sessionManager;
|
|
_libraryManager = libraryManager;
|
|
}
|
|
|
|
/// <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>
|
|
/// Discovers file paths used by Jellyfin's music libraries.
|
|
/// Helps users configure path mappings for direct file access.
|
|
/// </summary>
|
|
/// <returns>Sample file paths from each music library.</returns>
|
|
[HttpGet("DiscoverPaths")]
|
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
public ActionResult<DiscoveredPathsResponse> 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<string>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response containing discovered file paths from Jellyfin libraries.
|
|
/// </summary>
|
|
public class DiscoveredPathsResponse
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets sample file paths from the music library.
|
|
/// </summary>
|
|
public List<string> SamplePaths { get; set; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets or sets detected common path prefixes.
|
|
/// </summary>
|
|
public List<string> DetectedPrefixes { get; set; } = [];
|
|
}
|
|
|
|
/// <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; }
|
|
}
|