using System; using System.Collections.Generic; using System.Linq; using System.Net.Mime; using System.Threading.Tasks; using Jellyfin.Plugin.WatchedTogether.Models; using Jellyfin.Plugin.WatchedTogether.Services; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.WatchedTogether.Controllers; /// /// Administrative endpoints backing the configuration page. /// [ApiController] [Authorize(Policy = Policies.RequiresElevation)] [Route("Plugins/WatchedTogether")] [Produces(MediaTypeNames.Application.Json)] public class WatchedTogetherController : ControllerBase { private readonly IProvisioningService _provisioningService; private readonly IUserManager _userManager; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The provisioning service. /// The user manager. /// The logger. public WatchedTogetherController( IProvisioningService provisioningService, IUserManager userManager, ILogger logger) { _provisioningService = provisioningService; _userManager = userManager; _logger = logger; } /// /// Gets every configured group, resolved against current user records. /// /// The configured groups. [HttpGet("Groups")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetGroups() { var config = Plugin.Instance?.Configuration; if (config is null) { return Ok(Array.Empty()); } var groups = config.Groups.Select(g => new GroupDto { SharedUserId = g.SharedUserId, SharedUsername = _userManager.GetUserById(g.SharedUserId)?.Username ?? "(deleted)", SyncUnwatched = g.SyncUnwatched, SyncPlayCount = g.SyncPlayCount, IsDisabled = g.IsDisabled, Members = g.MemberUserIds.Select(id => new MemberDto { UserId = id, Username = _userManager.GetUserById(id)?.Username ?? "(deleted)" }).ToList() }).ToList(); return Ok(groups); } /// /// Gets the users that may be selected as members - everyone who is not already a shared account. /// /// The eligible users. [HttpGet("EligibleUsers")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetEligibleUsers() { var sharedIds = Plugin.Instance?.Configuration.Groups .Select(g => g.SharedUserId) .ToHashSet() ?? []; var users = _userManager.Users .Where(u => !sharedIds.Contains(u.Id)) .Select(u => new MemberDto { UserId = u.Id, Username = u.Username }) .OrderBy(u => u.Username, StringComparer.OrdinalIgnoreCase) .ToList(); return Ok(users); } /// /// Creates a shared account and its group. /// /// The group to create. /// The created group. [HttpPost("Groups")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> CreateGroup([FromBody] CreateGroupRequest request) { ArgumentNullException.ThrowIfNull(request); try { var group = await _provisioningService.CreateGroupAsync( request.MemberUserIds, request.Name).ConfigureAwait(false); return Ok(new GroupDto { SharedUserId = group.SharedUserId, SharedUsername = _userManager.GetUserById(group.SharedUserId)?.Username ?? string.Empty, SyncUnwatched = group.SyncUnwatched, SyncPlayCount = group.SyncPlayCount, IsDisabled = group.IsDisabled, Members = group.MemberUserIds.Select(id => new MemberDto { UserId = id, Username = _userManager.GetUserById(id)?.Username ?? "(deleted)" }).ToList() }); } catch (ArgumentException ex) { _logger.LogWarning(ex, "Rejected group creation request"); return BadRequest(ex.Message); } } /// /// Updates an existing group's membership and options. /// /// The shared account identifying the group. /// The new membership and options. /// No content on success. [HttpPost("Groups/{sharedUserId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task UpdateGroup( [FromRoute] Guid sharedUserId, [FromBody] UpdateGroupRequest request) { ArgumentNullException.ThrowIfNull(request); try { await _provisioningService.UpdateGroupAsync( sharedUserId, request.MemberUserIds, request.SyncUnwatched, request.SyncPlayCount, request.IsDisabled).ConfigureAwait(false); return NoContent(); } catch (ArgumentException ex) { _logger.LogWarning(ex, "Rejected group update request"); return BadRequest(ex.Message); } } /// /// Deletes a group, optionally deleting its shared account too. /// /// The shared account identifying the group. /// Whether to delete the shared account as well. /// No content on success. [HttpDelete("Groups/{sharedUserId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task DeleteGroup( [FromRoute] Guid sharedUserId, [FromQuery] bool deleteSharedUser = false) { try { await _provisioningService.DeleteGroupAsync(sharedUserId, deleteSharedUser).ConfigureAwait(false); return NoContent(); } catch (ArgumentException ex) { _logger.LogWarning(ex, "Rejected group deletion request"); return BadRequest(ex.Message); } } }