using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs.Account; using API.DTOs.Scrobbling; using API.Entities.Scrobble; using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; using Hangfire; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace API.Controllers; #nullable enable public class ScrobblingController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly IScrobblingService _scrobblingService; private readonly ILogger _logger; private readonly ILocalizationService _localizationService; public ScrobblingController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger logger, ILocalizationService localizationService) { _unitOfWork = unitOfWork; _scrobblingService = scrobblingService; _logger = logger; _localizationService = localizationService; } [HttpGet("anilist-token")] public async Task GetAniListToken() { // Validate the license var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(); return Ok(user.AniListAccessToken); } [HttpPost("update-anilist-token")] public async Task UpdateAniListToken(AniListUpdateDto dto) { // Validate the license var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(); var isNewToken = string.IsNullOrEmpty(user.AniListAccessToken); user.AniListAccessToken = dto.Token; _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); if (isNewToken) { BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistory(user.Id)); } return Ok(); } [HttpGet("token-expired")] public async Task> HasTokenExpired(ScrobbleProvider provider) { return Ok(await _scrobblingService.HasTokenExpired(User.GetUserId(), provider)); } /// /// Returns all scrobbling errors for the instance /// /// Requires admin /// [Authorize(Policy = "RequireAdminRole")] [HttpGet("scrobble-errors")] public async Task>> GetScrobbleErrors() { return Ok(await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()); } /// /// Clears the scrobbling errors table /// /// [Authorize(Policy = "RequireAdminRole")] [HttpPost("clear-errors")] public async Task ClearScrobbleErrors() { await _unitOfWork.ScrobbleRepository.ClearScrobbleErrors(); return Ok(); } /// /// Returns the scrobbling history for the user /// /// User must have a valid license /// [HttpPost("scrobble-events")] public async Task>> GetScrobblingEvents([FromQuery] UserParams pagination, [FromBody] ScrobbleEventFilter filter) { pagination ??= UserParams.Default; var events = await _unitOfWork.ScrobbleRepository.GetUserEvents(User.GetUserId(), filter, pagination); Response.AddPaginationHeader(events.CurrentPage, events.PageSize, events.TotalCount, events.TotalPages); return Ok(events); } /// /// Returns all scrobble holds for the current user /// /// [HttpGet("holds")] public async Task>> GetScrobbleHolds() { return Ok(await _unitOfWork.UserRepository.GetHolds(User.GetUserId())); } /// /// If there is an active hold on the series /// /// /// [HttpGet("has-hold")] public async Task> HasHold(int seriesId) { return Ok(await _unitOfWork.UserRepository.HasHoldOnSeries(User.GetUserId(), seriesId)); } /// /// Does the library the series is in allow scrobbling? /// /// /// [HttpGet("library-allows-scrobbling")] public async Task> LibraryAllowsScrobbling(int seriesId) { return Ok(await _unitOfWork.LibraryRepository.GetAllowsScrobblingBySeriesId(seriesId)); } /// /// Adds a hold against the Series for user's scrobbling /// /// /// [HttpPost("add-hold")] public async Task AddHold(int seriesId) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds); if (user == null) return Unauthorized(); if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId)) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); var seriesHold = new ScrobbleHoldBuilder().WithSeriesId(seriesId).Build(); user.ScrobbleHolds.Add(seriesHold); _unitOfWork.UserRepository.Update(user); try { _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); return Ok(); } catch (DbUpdateConcurrencyException ex) { foreach (var entry in ex.Entries) { // Reload the entity from the database await entry.ReloadAsync(); } // Retry the update _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); return Ok(); } catch (Exception ex) { // Handle other exceptions or log the error _logger.LogError(ex, "An error occurred while adding the hold"); return StatusCode(StatusCodes.Status500InternalServerError, await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); } } /// /// Adds a hold against the Series for user's scrobbling /// /// /// [HttpDelete("remove-hold")] public async Task RemoveHold(int seriesId) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds); if (user == null) return Unauthorized(); user.ScrobbleHolds = user.ScrobbleHolds.Where(h => h.SeriesId != seriesId).ToList(); _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); return Ok(); } }