Skip to content

Commit

Permalink
Merge pull request #58 from Kareadita/feature/search
Browse files Browse the repository at this point in the history
Search
  • Loading branch information
majora2007 authored Feb 15, 2021
2 parents 3a8ec47 + 63fe1cb commit 90318e8
Show file tree
Hide file tree
Showing 19 changed files with 235 additions and 26 deletions.
27 changes: 26 additions & 1 deletion API/Controllers/LibraryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Interfaces;
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace API.Controllers
Expand All @@ -22,16 +26,18 @@ public class LibraryController : BaseApiController
private readonly IMapper _mapper;
private readonly ITaskScheduler _taskScheduler;
private readonly IUnitOfWork _unitOfWork;
private readonly DataContext _dataContext; // TODO: Remove, only for FTS prototyping

public LibraryController(IDirectoryService directoryService,
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
IUnitOfWork unitOfWork)
IUnitOfWork unitOfWork, DataContext dataContext)
{
_directoryService = directoryService;
_logger = logger;
_mapper = mapper;
_taskScheduler = taskScheduler;
_unitOfWork = unitOfWork;
_dataContext = dataContext;
}

/// <summary>
Expand Down Expand Up @@ -213,5 +219,24 @@ public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto
return Ok();

}

[HttpGet("search")]
public async Task<ActionResult<IEnumerable<SearchResultDto>>> Search(string queryString)
{
//NOTE: What about normalizing search query and only searching against normalizedname in Series?
// So One Punch would match One-Punch
// This also means less indexes we need.
queryString = queryString.Replace(@"%", "");

var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
// Get libraries user has access to
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();

if (!libraries.Any()) return BadRequest("User does not have access to any libraries");

var series = await _unitOfWork.SeriesRepository.SearchSeries(libraries.Select(l => l.Id).ToArray(), queryString);

return Ok(series);
}
}
}
32 changes: 25 additions & 7 deletions API/Controllers/ReaderController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,23 @@ public async Task<ActionResult<ImageDto>> GetImage(int chapterId, int page)
// Temp let's iterate the directory each call to get next image
var chapter = await _cacheService.Ensure(chapterId);

if (chapter == null) return BadRequest("There was an issue finding image file for reading.");
if (chapter == null) return BadRequest("There was an issue finding image file for reading");

// TODO: This code works, but might need bounds checking. UI can send bad data
// if (page >= chapter.Pages)
// {
// page = chapter.Pages - 1;
// } else if (page < 0)
// {
// page = 0;
// }

var (path, mangaFile) = await _cacheService.GetCachedPagePath(chapter, page);
if (string.IsNullOrEmpty(path)) return BadRequest($"No such image for page {page}");
var file = await _directoryService.ReadImageAsync(path);
file.Page = page;
file.MangaFileName = mangaFile.FilePath;
file.NeedsSplitting = file.Width > file.Height;

return Ok(file);
}
Expand All @@ -51,25 +61,33 @@ public async Task<ActionResult<int>> GetBookmark(int chapterId)
if (user.Progresses == null) return Ok(0);
var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.ChapterId == chapterId);

if (progress != null) return Ok(progress.PagesRead);

return Ok(0);
return Ok(progress?.PagesRead ?? 0);
}

[HttpPost("bookmark")]
public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
_logger.LogInformation($"Saving {user.UserName} progress for Chapter {bookmarkDto.ChapterId} to page {bookmarkDto.PageNum}");
_logger.LogInformation("Saving {UserName} progress for Chapter {ChapterId} to page {PageNum}", user.UserName, bookmarkDto.ChapterId, bookmarkDto.PageNum);

// TODO: Don't let user bookmark past total pages.
// Don't let user bookmark past total pages.
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId);
if (bookmarkDto.PageNum > chapter.Pages)
{
return BadRequest("Can't bookmark past max pages");
}

if (bookmarkDto.PageNum < 0)
{
return BadRequest("Can't bookmark less than 0");
}


user.Progresses ??= new List<AppUserProgress>();
var userProgress = user.Progresses.SingleOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == user.Id);

if (userProgress == null)
{

user.Progresses.Add(new AppUserProgress
{
PagesRead = bookmarkDto.PageNum,
Expand Down
3 changes: 2 additions & 1 deletion API/DTOs/ImageDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public class ImageDto
public int Height { get; init; }
public string Format { get; init; }
public byte[] Content { get; init; }
public int Chapter { get; set; }
//public int Chapter { get; set; }
public string MangaFileName { get; set; }
public bool NeedsSplitting { get; set; }
}
}
7 changes: 7 additions & 0 deletions API/DTOs/SearchQueryDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace API.DTOs
{
public class SearchQueryDto
{
public string QueryString { get; init; }
}
}
16 changes: 16 additions & 0 deletions API/DTOs/SearchResultDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace API.DTOs
{
public class SearchResultDto
{
public int SeriesId { get; init; }
public string Name { get; init; }
public string OriginalName { get; init; }
public string SortName { get; init; }
public byte[] CoverImage { get; init; } // This should be optional or a thumbImage (much smaller)


// Grouping information
public string LibraryName { get; set; }
public int LibraryId { get; set; }
}
}
9 changes: 9 additions & 0 deletions API/Data/LibraryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ public async Task<bool> DeleteLibrary(int libraryId)
return await _context.SaveChangesAsync() > 0;
}

public async Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId)
{
return await _context.Library
.Include(l => l.AppUsers)
.Where(l => l.AppUsers.Select(ap => ap.Id).Contains(userId))
.AsNoTracking()
.ToListAsync();
}

public async Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync()
{
return await _context.Library
Expand Down
25 changes: 23 additions & 2 deletions API/Data/SeriesRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace API.Data
{
public class SeriesRepository : ISeriesRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
private readonly ILogger _logger;

public SeriesRepository(DataContext context, IMapper mapper)
public SeriesRepository(DataContext context, IMapper mapper, ILogger logger)
{
_context = context;
_mapper = mapper;
_logger = logger;
}

public void Add(Series series)
Expand Down Expand Up @@ -74,7 +77,25 @@ public async Task<IEnumerable<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libr
await AddSeriesModifiers(userId, series);


Console.WriteLine("Processed GetSeriesDtoForLibraryIdAsync in {0} milliseconds", sw.ElapsedMilliseconds);
_logger.LogDebug("Processed GetSeriesDtoForLibraryIdAsync in {ElapsedMilliseconds} milliseconds", sw.ElapsedMilliseconds);
return series;
}

public async Task<IEnumerable<SearchResultDto>> SearchSeries(int[] libraryIds, string searchQuery)
{
var sw = Stopwatch.StartNew();
var series = await _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|| EF.Functions.Like(s.OriginalName, $"%{searchQuery}%"))
.Include(s => s.Library) // NOTE: Is there a way to do this faster?
.OrderBy(s => s.SortName)
.AsNoTracking()
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
.ToListAsync();


_logger.LogDebug("Processed SearchSeries in {ElapsedMilliseconds} milliseconds", sw.ElapsedMilliseconds);
return series;
}

Expand Down
7 changes: 5 additions & 2 deletions API/Data/UnitOfWork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using API.Interfaces;
using AutoMapper;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;

namespace API.Data
{
Expand All @@ -11,15 +12,17 @@ public class UnitOfWork : IUnitOfWork
private readonly DataContext _context;
private readonly IMapper _mapper;
private readonly UserManager<AppUser> _userManager;
private readonly ILogger<UnitOfWork> _seriesLogger;

public UnitOfWork(DataContext context, IMapper mapper, UserManager<AppUser> userManager)
public UnitOfWork(DataContext context, IMapper mapper, UserManager<AppUser> userManager, ILogger<UnitOfWork> seriesLogger)
{
_context = context;
_mapper = mapper;
_userManager = userManager;
_seriesLogger = seriesLogger;
}

public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper);
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper, _seriesLogger);
public IUserRepository UserRepository => new UserRepository(_context, _userManager);
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);

Expand Down
1 change: 1 addition & 0 deletions API/Entities/Series.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class Series : IEntityDate
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public byte[] CoverImage { get; set; }
// NOTE: Do I want to store a thumbImage for search results?
/// <summary>
/// Sum of all Volume page counts
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions API/Extensions/HttpExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Text.Json;
using API.Helpers;
using Microsoft.AspNetCore.Http;

namespace API.Extensions
{
public static class HttpExtensions
{
public static void AddPaginationHeader(this HttpResponse response, int currentPage,
int itemsPerPage, int totalItems, int totalPages)
{
var paginationHeader = new PaginationHeader(currentPage, itemsPerPage, totalItems, totalPages);
response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader));
response.Headers.Add("Access-Control-Expose-Headers", "Pagination");
}
}
}
7 changes: 7 additions & 0 deletions API/Helpers/AutoMapperProfiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ public AutoMapperProfiles()
CreateMap<Series, SeriesDto>();

CreateMap<AppUserPreferences, UserPreferencesDto>();

CreateMap<Series, SearchResultDto>()
.ForMember(dest => dest.SeriesId,
opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.LibraryName,
opt => opt.MapFrom(src => src.Library.Name));


CreateMap<Library, LibraryDto>()
.ForMember(dest => dest.Folders,
Expand Down
32 changes: 32 additions & 0 deletions API/Helpers/PagedList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace API.Helpers
{
public class PagedList<T> : List<T>
{
public PagedList(IEnumerable<T> items, int count, int pageNumber, int pageSize)
{
CurrentPage = pageNumber;
TotalPages = (int) Math.Ceiling(count / (double) pageSize);
PageSize = pageSize;
TotalCount = count;
AddRange(items);
}

public int CurrentPage { get; set; }
public int TotalPages { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }

public static async Task<PagedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedList<T>(items, count, pageNumber, pageSize);
}
}
}
18 changes: 18 additions & 0 deletions API/Helpers/PaginationHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace API.Helpers
{
public class PaginationHeader
{
public PaginationHeader(int currentPage, int itemsPerPage, int totalItems, int totalPages)
{
CurrentPage = currentPage;
ItemsPerPage = itemsPerPage;
TotalItems = totalItems;
TotalPages = totalPages;
}

public int CurrentPage { get; set; }
public int ItemsPerPage { get; set; }
public int TotalItems { get; set; }
public int TotalPages { get; set; }
}
}
15 changes: 15 additions & 0 deletions API/Helpers/UserParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace API.Helpers
{
public class UserParams
{
private const int MaxPageSize = 50;
public int PageNumber { get; set; } = 1;
private int _pageSize = 10;

public int PageSize
{
get => _pageSize;
set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value;
}
}
}
1 change: 1 addition & 0 deletions API/Interfaces/ILibraryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ public interface ILibraryRepository
Task<IEnumerable<LibraryDto>> GetLibraryDtosForUsernameAsync(string userName);
Task<IEnumerable<Library>> GetLibrariesAsync();
Task<bool> DeleteLibrary(int libraryId);
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
}
}
Loading

0 comments on commit 90318e8

Please sign in to comment.