#nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using API.Data; using API.DTOs.Metadata.Browse.Requests; using API.DTOs.Reader; using API.Entities; using API.Extensions; using API.Helpers; using API.Services; using API.SignalR; using HtmlAgilityPack; using Kavita.Common; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace API.Controllers; public class AnnotationController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly ILocalizationService _localizationService; private readonly IEventHub _eventHub; private readonly IAnnotationService _annotationService; public AnnotationController(IUnitOfWork unitOfWork, ILogger logger, ILocalizationService localizationService, IEventHub eventHub, IAnnotationService annotationService) { _unitOfWork = unitOfWork; _logger = logger; _localizationService = localizationService; _eventHub = eventHub; _annotationService = annotationService; } /// /// Returns a list of annotations for browsing /// /// /// /// [HttpPost("all-filtered")] public async Task>> GetAnnotationsForBrowse(BrowseAnnotationFilterDto filter, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; var list = await _unitOfWork.AnnotationRepository.GetAnnotationDtos(User.GetUserId(), filter, userParams); Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); return Ok(list); } /// /// Returns the annotations for the given chapter /// /// /// [HttpGet("all")] public async Task>> GetAnnotations(int chapterId) { return Ok(await _unitOfWork.UserRepository.GetAnnotations(User.GetUserId(), chapterId)); } /// /// Returns all annotations by Series /// /// /// [HttpGet("all-for-series")] public async Task> GetAnnotationsBySeries(int seriesId) { return Ok(await _unitOfWork.UserRepository.GetAnnotationDtosBySeries(User.GetUserId(), seriesId)); } /// /// Returns the Annotation by Id. User must have access to annotation. /// /// /// [HttpGet("{annotationId}")] public async Task> GetAnnotation(int annotationId) { return Ok(await _unitOfWork.UserRepository.GetAnnotationDtoById(User.GetUserId(), annotationId)); } /// /// Create a new Annotation for the user against a Chapter /// /// /// [HttpPost("create")] public async Task> CreateAnnotation(AnnotationDto dto) { try { return Ok(await _annotationService.CreateAnnotation(User.GetUserId(), dto)); } catch (KavitaException ex) { return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); } } /// /// Update the modifiable fields (Spoiler, highlight slot, and comment) for an annotation /// /// /// [HttpPost("update")] public async Task> UpdateAnnotation(AnnotationDto dto) { try { return Ok(await _annotationService.UpdateAnnotation(User.GetUserId(), dto)); } catch (KavitaException ex) { return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); } } /// /// Delete the annotation for the user /// /// /// [HttpDelete] public async Task DeleteAnnotation(int annotationId) { var annotation = await _unitOfWork.AnnotationRepository.GetAnnotation(annotationId); if (annotation == null || annotation.AppUserId != User.GetUserId()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "annotation-delete")); _unitOfWork.AnnotationRepository.Remove(annotation); await _unitOfWork.CommitAsync(); return Ok(); } /// /// Removes annotations in bulk. Requires every annotation to be owned by the authenticated user /// /// /// [HttpPost("bulk-delete")] public async Task DeleteAnnotationsBulk(IList annotationIds) { var userId = User.GetUserId(); var annotations = await _unitOfWork.AnnotationRepository.GetAnnotations(annotationIds); if (annotations.Any(a => a.AppUserId != userId)) { return BadRequest(); } _unitOfWork.AnnotationRepository.Remove(annotations); await _unitOfWork.CommitAsync(); return Ok(); } /// /// Exports annotations for the given users /// /// [HttpPost("export-filter")] public async Task ExportAnnotationsFilter(BrowseAnnotationFilterDto filter, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; var list = await _unitOfWork.AnnotationRepository.GetAnnotationDtos(User.GetUserId(), filter, userParams); var annotations = list.Select(a => a.Id).ToList(); var json = await _annotationService.ExportAnnotations(User.GetUserId(), annotations); if (string.IsNullOrEmpty(json)) return BadRequest(); var bytes = Encoding.UTF8.GetBytes(json); var fileName = System.Web.HttpUtility.UrlEncode($"annotations_export_{User.GetUserId()}_{DateTime.UtcNow:yyyyMMdd_HHmmss}_filtered"); return File(bytes, "application/json", fileName + ".json"); } /// /// Exports Annotations for the User /// /// Export annotations with the given ids /// [HttpPost("export")] public async Task ExportAnnotations(IList? annotations = null) { var json = await _annotationService.ExportAnnotations(User.GetUserId(), annotations); if (string.IsNullOrEmpty(json)) return BadRequest(); var bytes = Encoding.UTF8.GetBytes(json); var fileName = System.Web.HttpUtility.UrlEncode($"annotations_export_{User.GetUserId()}_{DateTime.UtcNow:yyyyMMdd_HHmmss}"); if (annotations != null) { fileName += "_user_selection"; } return File(bytes, "application/json", fileName + ".json"); } }