using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs; using API.DTOs.Person; using API.Entities.Enums; using API.Extensions; using API.Helpers; using API.Services; using API.Services.Tasks.Metadata; using API.SignalR; using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Nager.ArticleNumber; namespace API.Controllers; #nullable enable public class PersonController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly ILocalizationService _localizationService; private readonly IMapper _mapper; private readonly ICoverDbService _coverDbService; private readonly IImageService _imageService; private readonly IEventHub _eventHub; private readonly IPersonService _personService; public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper, ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub, IPersonService personService) { _unitOfWork = unitOfWork; _localizationService = localizationService; _mapper = mapper; _coverDbService = coverDbService; _imageService = imageService; _eventHub = eventHub; _personService = personService; } [HttpGet] public async Task> GetPersonByName(string name) { return Ok(await _unitOfWork.PersonRepository.GetPersonDtoByName(name, User.GetUserId())); } /// /// Find a person by name or alias against a query string /// /// /// [HttpGet("search")] public async Task>> SearchPeople([FromQuery] string queryString) { return Ok(await _unitOfWork.PersonRepository.SearchPeople(queryString)); } /// /// Returns all roles for a Person /// /// /// [HttpGet("roles")] public async Task>> GetRolesForPersonByName(int personId) { return Ok(await _unitOfWork.PersonRepository.GetRolesForPersonByName(personId, User.GetUserId())); } /// /// Returns a list of authors and artists for browsing /// /// /// [HttpPost("all")] public async Task>> GetAuthorsForBrowse([FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; var list = await _unitOfWork.PersonRepository.GetAllWritersAndSeriesCount(User.GetUserId(), userParams); Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); return Ok(list); } /// /// Updates the Person /// /// /// [Authorize("RequireAdminRole")] [HttpPost("update")] public async Task> UpdatePerson(UpdatePersonDto dto) { // This needs to get all people and update them equally var person = await _unitOfWork.PersonRepository.GetPersonById(dto.Id, PersonIncludes.Aliases); if (person == null) return BadRequest(_localizationService.Translate(User.GetUserId(), "person-doesnt-exist")); if (string.IsNullOrEmpty(dto.Name)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-required")); // Validate the name is unique if (dto.Name != person.Name && !(await _unitOfWork.PersonRepository.IsNameUnique(dto.Name))) { return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-unique")); } var success = await _personService.UpdatePersonAliasesAsync(person, dto.Aliases); if (!success) return BadRequest(await _localizationService.Translate(User.GetUserId(), "aliases-have-overlap")); person.Name = dto.Name?.Trim(); person.Description = dto.Description ?? string.Empty; person.CoverImageLocked = dto.CoverImageLocked; if (dto.MalId is > 0) { person.MalId = (long) dto.MalId; } if (dto.AniListId is > 0) { person.AniListId = (int) dto.AniListId; } if (!string.IsNullOrEmpty(dto.HardcoverId?.Trim())) { person.HardcoverId = dto.HardcoverId.Trim(); } var asin = dto.Asin?.Trim(); if (!string.IsNullOrEmpty(asin) && (ArticleNumberHelper.IsValidIsbn10(asin) || ArticleNumberHelper.IsValidIsbn13(asin))) { person.Asin = asin; } _unitOfWork.PersonRepository.Update(person); await _unitOfWork.CommitAsync(); return Ok(_mapper.Map(person)); } /// /// Attempts to download the cover from CoversDB (Note: Not yet release in Kavita) /// /// /// [HttpPost("fetch-cover")] public async Task> DownloadCoverImage([FromQuery] int personId) { var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var person = await _unitOfWork.PersonRepository.GetPersonById(personId); if (person == null) return BadRequest(_localizationService.Translate(User.GetUserId(), "person-doesnt-exist")); var personImage = await _coverDbService.DownloadPersonImageAsync(person, settings.EncodeMediaAs); if (string.IsNullOrEmpty(personImage)) { return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-image-doesnt-exist")); } person.CoverImage = personImage; _imageService.UpdateColorScape(person); _unitOfWork.PersonRepository.Update(person); await _unitOfWork.CommitAsync(); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(person.Id, "person"), false); return Ok(personImage); } /// /// Returns the top 20 series that the "person" is known for. This will use Average Rating when applicable (Kavita+ field), else it's a random sort /// /// /// [HttpGet("series-known-for")] public async Task>> GetKnownSeries(int personId) { return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId)); } /// /// Returns all individual chapters by role. Limited to 20 results. /// /// /// /// [HttpGet("chapters-by-role")] public async Task>> GetChaptersByRole(int personId, PersonRole role) { return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, User.GetUserId(), role)); } /// /// Merges Persons into one, this action is irreversible /// /// /// [HttpPost("merge")] public async Task> MergePeople(PersonMergeDto dto) { var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All); if (dst == null) return BadRequest(); var src = await _unitOfWork.PersonRepository.GetPersonById(dto.SrcId, PersonIncludes.All); if (src == null) return BadRequest(); await _personService.MergePeopleAsync(src, dst); await _eventHub.SendMessageAsync(MessageFactory.PersonMerged, MessageFactory.PersonMergedMessage(dst, src)); return Ok(_mapper.Map(dst)); } /// /// Ensure the alias is valid to be added. For example, the alias cannot be on another person or be the same as the current person name/alias. /// /// /// /// [HttpGet("valid-alias")] public async Task> IsValidAlias(int personId, string alias) { var person = await _unitOfWork.PersonRepository.GetPersonById(personId, PersonIncludes.Aliases); if (person == null) return NotFound(); var existingAlias = await _unitOfWork.PersonRepository.AnyAliasExist(alias); return Ok(!existingAlias && person.NormalizedName != alias.ToNormalized()); } }