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,
request.EnableAllFolders,
request.EnabledFolders).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);
}
}
}