using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Plugin.WatchedTogether.Compat;
using Jellyfin.Plugin.WatchedTogether.Configuration;
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 IRestrictionService _restrictionService;
private readonly IUserManager _userManager;
private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
/// The provisioning service.
/// The content restriction service.
/// The user manager.
/// The logger.
public WatchedTogetherController(
IProvisioningService provisioningService,
IRestrictionService restrictionService,
IUserManager userManager,
ILogger logger)
{
_provisioningService = provisioningService;
_restrictionService = restrictionService;
_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());
}
return Ok(config.Groups.Select(ToDto).ToList());
}
///
/// 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.GetAllUsers()
.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(ToDto(group));
}
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,
request.InheritParentalRating,
request.ParentalRatingCap).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);
}
}
///
/// Resolves a stored group against current user records, including the restrictions its
/// shared account actually carries right now.
///
/// The stored group.
/// The group as the dashboard shows it.
private GroupDto ToDto(SharedGroup group)
{
var sharedUser = _userManager.GetUserById(group.SharedUserId);
var range = _restrictionService.GetRatingRange(group.MemberUserIds);
return new GroupDto
{
SharedUserId = group.SharedUserId,
SharedUsername = sharedUser?.Username ?? "(deleted)",
SyncUnwatched = group.SyncUnwatched,
SyncPlayCount = group.SyncPlayCount,
IsDisabled = group.IsDisabled,
InheritParentalRating = group.InheritParentalRating,
ParentalRatingCap = group.ParentalRatingCap,
StrictestMemberRating = range.Strictest,
LoosestMemberRating = range.Loosest,
Restrictions = sharedUser is null ? null : ToDto(ContentRestrictions.FromUser(sharedUser)),
Members = group.MemberUserIds.Select(id =>
{
var member = _userManager.GetUserById(id);
return new MemberDto
{
UserId = id,
Username = member?.Username ?? "(deleted)",
MaxParentalRating = member is null ? 0 : member.MaxParentalRatingScore
};
}).ToList()
};
}
private static RestrictionsDto ToDto(ContentRestrictions restrictions)
=> new()
{
MaxParentalRating = restrictions.MaxParentalRatingScore,
MaxParentalSubRating = restrictions.MaxParentalRatingSubScore,
BlockUnratedItems = restrictions.BlockUnratedItems.Select(u => u.ToString()).OrderBy(u => u, StringComparer.Ordinal).ToList(),
BlockedTags = restrictions.BlockedTags.OrderBy(t => t, StringComparer.OrdinalIgnoreCase).ToList(),
AllowedTags = (restrictions.AllowedTags ?? Enumerable.Empty()).OrderBy(t => t, StringComparer.OrdinalIgnoreCase).ToList(),
AllowsNothing = restrictions.HasAllowedTags && restrictions.AllowedTags!.Count == 0
};
}