new build system
inject controls in homepage
This commit is contained in:
@@ -3,8 +3,10 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Plugin.JellyLMS.Models;
|
||||
using Jellyfin.Plugin.JellyLMS.Services;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -29,6 +31,7 @@ public class JellyLmsController : ControllerBase
|
||||
private readonly ILmsApiClient _lmsClient;
|
||||
private readonly LmsPlayerManager _playerManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JellyLmsController"/> class.
|
||||
@@ -36,14 +39,54 @@ public class JellyLmsController : ControllerBase
|
||||
/// <param name="lmsClient">The LMS API client.</param>
|
||||
/// <param name="playerManager">The player manager.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
public JellyLmsController(
|
||||
ILmsApiClient lmsClient,
|
||||
LmsPlayerManager playerManager,
|
||||
ILibraryManager libraryManager)
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager)
|
||||
{
|
||||
_lmsClient = lmsClient;
|
||||
_playerManager = playerManager;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the current user is allowed to use the multi-room remote
|
||||
/// control features (administrators, or users granted the
|
||||
/// "Allow remote control of other users" permission).
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the user may use remote control endpoints.</returns>
|
||||
private bool HasRemoteControlAccess()
|
||||
{
|
||||
var username = User.Identity?.Name;
|
||||
if (string.IsNullOrEmpty(username))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserByName(username);
|
||||
if (user is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return HasPermission(user, PermissionKind.IsAdministrator)
|
||||
|| HasPermission(user, PermissionKind.EnableRemoteControlOfOtherUsers);
|
||||
}
|
||||
|
||||
private static bool HasPermission(Jellyfin.Database.Implementations.Entities.User user, PermissionKind kind)
|
||||
{
|
||||
foreach (var permission in user.Permissions)
|
||||
{
|
||||
if (permission.Kind == kind)
|
||||
{
|
||||
return permission.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,8 +108,14 @@ public class JellyLmsController : ControllerBase
|
||||
/// <returns>List of players.</returns>
|
||||
[HttpGet("Players")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<List<LmsPlayer>>> GetPlayers([FromQuery] bool refresh = false)
|
||||
{
|
||||
if (!HasRemoteControlAccess())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var players = await _playerManager.GetPlayersAsync(refresh).ConfigureAwait(false);
|
||||
return Ok(players);
|
||||
}
|
||||
@@ -78,9 +127,15 @@ public class JellyLmsController : ControllerBase
|
||||
/// <returns>The player details.</returns>
|
||||
[HttpGet("Players/{mac}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LmsPlayer>> GetPlayer(string mac)
|
||||
{
|
||||
if (!HasRemoteControlAccess())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var player = await _playerManager.GetPlayerAsync(mac).ConfigureAwait(false);
|
||||
if (player == null)
|
||||
{
|
||||
@@ -98,8 +153,14 @@ public class JellyLmsController : ControllerBase
|
||||
[HttpPost("Players/{mac}/PowerOn")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> PowerOn(string mac)
|
||||
{
|
||||
if (!HasRemoteControlAccess())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var success = await _lmsClient.PowerOnAsync(mac).ConfigureAwait(false);
|
||||
return success ? Ok() : BadRequest("Failed to power on player");
|
||||
}
|
||||
@@ -112,8 +173,14 @@ public class JellyLmsController : ControllerBase
|
||||
[HttpPost("Players/{mac}/PowerOff")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> PowerOff(string mac)
|
||||
{
|
||||
if (!HasRemoteControlAccess())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var success = await _lmsClient.PowerOffAsync(mac).ConfigureAwait(false);
|
||||
return success ? Ok() : BadRequest("Failed to power off player");
|
||||
}
|
||||
@@ -127,8 +194,14 @@ public class JellyLmsController : ControllerBase
|
||||
[HttpPost("Players/{mac}/Volume")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> SetVolume(string mac, [FromBody] VolumeRequest request)
|
||||
{
|
||||
if (!HasRemoteControlAccess())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var success = await _lmsClient.SetVolumeAsync(mac, request.Volume).ConfigureAwait(false);
|
||||
return success ? Ok() : BadRequest("Failed to set volume");
|
||||
}
|
||||
@@ -139,8 +212,14 @@ public class JellyLmsController : ControllerBase
|
||||
/// <returns>List of sync groups.</returns>
|
||||
[HttpGet("SyncGroups")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<List<SyncGroup>>> GetSyncGroups()
|
||||
{
|
||||
if (!HasRemoteControlAccess())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var groups = await _playerManager.GetSyncGroupsAsync().ConfigureAwait(false);
|
||||
return Ok(groups);
|
||||
}
|
||||
@@ -153,8 +232,14 @@ public class JellyLmsController : ControllerBase
|
||||
[HttpPost("SyncGroups")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> CreateSyncGroup([FromBody] CreateSyncGroupRequest request)
|
||||
{
|
||||
if (!HasRemoteControlAccess())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var success = await _playerManager.CreateSyncGroupAsync(request.MasterMac, request.SlaveMacs)
|
||||
.ConfigureAwait(false);
|
||||
return success ? Ok() : BadRequest("Failed to create sync group");
|
||||
@@ -168,8 +253,14 @@ public class JellyLmsController : ControllerBase
|
||||
[HttpDelete("SyncGroups/Players/{mac}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> UnsyncPlayer(string mac)
|
||||
{
|
||||
if (!HasRemoteControlAccess())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var success = await _playerManager.UnsyncPlayerAsync(mac).ConfigureAwait(false);
|
||||
return success ? Ok() : BadRequest("Failed to unsync player");
|
||||
}
|
||||
@@ -182,12 +273,67 @@ public class JellyLmsController : ControllerBase
|
||||
[HttpDelete("SyncGroups/{masterMac}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> DissolveSyncGroup(string masterMac)
|
||||
{
|
||||
if (!HasRemoteControlAccess())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var success = await _playerManager.DissolveSyncGroupAsync(masterMac).ConfigureAwait(false);
|
||||
return success ? Ok() : BadRequest("Failed to dissolve sync group");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the current user is allowed to use the multi-room remote control.
|
||||
/// Used by the remote control page and the injected web client button to decide
|
||||
/// whether to show themselves.
|
||||
/// </summary>
|
||||
/// <returns>200 OK if allowed, otherwise 403 Forbidden.</returns>
|
||||
[HttpGet("RemoteControl/Access")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult CheckRemoteControlAccess()
|
||||
{
|
||||
return HasRemoteControlAccess() ? Ok() : Forbid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serves the standalone multi-room remote control page. The page itself contains
|
||||
/// no sensitive data; it authenticates API calls using the Jellyfin access token
|
||||
/// stored by the web client, so it is reachable without a prior Jellyfin auth header.
|
||||
/// </summary>
|
||||
/// <returns>The remote control HTML page.</returns>
|
||||
[HttpGet("RemoteControl")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult GetRemoteControlPage()
|
||||
{
|
||||
return ServeEmbeddedResource("Jellyfin.Plugin.JellyLMS.Web.RemoteControl.html", "text/html");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serves the client script that is injected into the Jellyfin web client to add a
|
||||
/// floating button linking to the remote control page.
|
||||
/// </summary>
|
||||
/// <returns>The client script.</returns>
|
||||
[HttpGet("RemoteControl/ClientScript")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult GetRemoteControlClientScript()
|
||||
{
|
||||
return ServeEmbeddedResource("Jellyfin.Plugin.JellyLMS.Web.remote-button.js", "application/javascript");
|
||||
}
|
||||
|
||||
private FileStreamResult ServeEmbeddedResource(string resourceName, string contentType)
|
||||
{
|
||||
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException($"Embedded resource '{resourceName}' not found.");
|
||||
|
||||
return File(stream, contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers file paths used by Jellyfin's music libraries.
|
||||
/// Helps users configure path mappings for direct file access.
|
||||
|
||||
Reference in New Issue
Block a user