From 922c0153d3b3e8a1d661038cb4cf91f906202a64 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 17 Feb 2021 15:28:57 -0600 Subject: [PATCH] Added series progress/rating information back in. Left attempts at doing via a JOIN or raw SQL. --- API/Controllers/LibraryController.cs | 3 + API/DTOs/SeriesDto.cs | 9 ++ API/Data/SeriesRepository.cs | 173 ++++++++++++++++++++- API/Extensions/LeftJoinExtensions.cs | 73 +++++++++ API/Interfaces/ISeriesRepository.cs | 7 + API/Interfaces/Services/IArchiveService.cs | 2 + API/Services/ArchiveService.cs | 5 + API/Services/MetadataService.cs | 2 +- API/appsettings.Development.json | 2 +- 9 files changed, 269 insertions(+), 7 deletions(-) create mode 100644 API/Extensions/LeftJoinExtensions.cs diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 34bb2e94e..e3f17ec3e 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -174,6 +174,9 @@ namespace API.Controllers var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, userParams); + // Apply progress/rating information (I can't work out how to do this in initial query) + await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series); + Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index bf9fc8a45..8359f60b9 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -21,5 +21,14 @@ /// Review from logged in user. Calculated at API-time. /// public string UserReview { get; set; } + + /// + /// DO NOT Use. Used as a hack for + /// + public int LibraryId { get; set; } + /// + /// DO NOT Use. Used as a hack for + /// + public int AppUserId { get; set; } } } \ No newline at end of file diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index c501fdc61..738e4eb22 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -69,7 +69,174 @@ namespace API.Data var sw = Stopwatch.StartNew(); + // + // // var query = QueryableExtensions.LeftJoin(_context.Series + // // .Join(_context.AppUserProgresses, series => series.Id, progress => progress.SeriesId, + // // (series, progress) => + // // new { + // // series.LibraryId, + // // series.Id, + // // series.Created, + // // series.Name, + // // series.Pages, + // // series.Summary, + // // series.CoverImage, + // // series.OriginalName, + // // series.SortName, + // // progress.PagesRead + // // }), _context.AppUserRating, series => series.Id, rating => rating.SeriesId, + // // (series, rating) => + // // new { + // // series.LibraryId, + // // series.Id, + // // series.Created, + // // series.Name, + // // series.Pages, + // // series.Summary, + // // series.CoverImage, + // // series.OriginalName, + // // series.SortName, + // // series.PagesRead, + // // rating.Review, + // // rating.Rating, + // // rating.AppUserId + // // }) + // // .Where(s => s.LibraryId == libraryId && s.AppUserId == userId) + // // .OrderBy(s => s.SortName) + // // .Select(arg => new SeriesDto() + // // { + // // PagesRead = arg.PagesRead, + // // UserRating = arg.Rating, + // // UserReview = arg.Review, + // // Summary = arg.Summary, + // // Name = arg.Name, + // // OriginalName = arg.OriginalName, + // // SortName = arg.SortName, + // // Id = arg.Id, + // // Pages = arg.Pages, + // // CoverImage = arg.CoverImage + // // }) + // // //.ProjectTo(_mapper.ConfigurationProvider) + // // .AsNoTracking(); + // + // + // // var query2 = _context.Series + // // .Join(_context.AppUserProgresses, series => series.Id, progress => progress.SeriesId, + // // (series, progress) => new { series, progress} + // // /*new + // // { + // // series.LibraryId, + // // series.Id, + // // series.Created, + // // series.Name, + // // series.Pages, + // // series.Summary, + // // series.CoverImage, + // // series.OriginalName, + // // series.SortName, + // // progress.PagesRead + // // }*/) + // // .LeftJoin(_context.AppUserRating, series => series.series.Id, rating => rating.SeriesId, + // // (series, rating) => new {series.series, series.progress, rating} + // // /* + // // * new + // // { + // // series.LibraryId, + // // series.Id, + // // series.Name, + // // series.Pages, + // // series.Summary, + // // series.CoverImage, + // // series.OriginalName, + // // series.SortName, + // // series.PagesRead, + // // rating.Review, + // // rating.Rating, + // // rating.AppUserId + // // } + // // */) + // // // .Select(arg => new SeriesDto() + // // // { + // // // // Where is failing because this select changes what we select + // // // PagesRead = arg.PagesRead, + // // // UserRating = arg.Rating, + // // // UserReview = arg.Review, + // // // Summary = arg.Summary, + // // // Name = arg.Name, + // // // OriginalName = arg.OriginalName, + // // // SortName = arg.SortName, + // // // Id = arg.Id, + // // // Pages = arg.Pages, + // // // CoverImage = arg.CoverImage, + // // // }) + // .Select(arg => new SeriesDto() + // { + // // Where is failing because this select changes what we select + // LibraryId = arg.series.LibraryId, + // PagesRead = arg.progress.PagesRead, + // UserRating = arg.rating.Rating, + // UserReview = arg.rating.Review, + // Summary = arg.series.Summary, + // Name = arg.series.Name, + // OriginalName = arg.series.OriginalName, + // SortName = arg.series.SortName, + // Id = arg.series.Id, + // Pages = arg.series.Pages, + // CoverImage = arg.series.CoverImage, + // }) + // // .Where(s => s.LibraryId == libraryId && s.AppUserId == userId) + // // .OrderBy(s => s.SortName) + // // .AsNoTracking(); + // + // var query = _context.Series + // .FromSqlRaw(@"select S.*, Rating AS UserRating, Review AS UserReview from Series AS S + // INNER JOIN AppUserProgresses AS AP ON AP.SeriesId = S.Id AND AP.AppUserId = {0} + // LEFT OUTER JOIN AppUserRating AUR on AUR.SeriesId = S.Id AND AUR.AppUserId = {0} + // WHERE (S.LibraryId = {1}) AND (AP.AppUserId = {0}) + // ORDER BY S.SortName", userId, libraryId) + // .Select(series => new SeriesDto() + // { + // // Where is failing because this select changes what we select + // PagesRead = series.PagesRead, + // UserRating = series.UserRating, + // UserReview = arg.rating.Review, + // Summary = arg.series.Summary, + // Name = arg.series.Name, + // OriginalName = arg.series.OriginalName, + // SortName = arg.series.SortName, + // Id = arg.series.Id, + // Pages = arg.series.Pages, + // CoverImage = arg.series.CoverImage, + // }) + // .AsNoTracking(); + + + /* + * select S.*, Rating AS UserRating, Review AS UserReview from Series AS S + INNER JOIN AppUserProgresses AS AP ON AP.SeriesId = S.Id AND AP.AppUserId = 1 + LEFT OUTER JOIN AppUserRating AUR on AUR.SeriesId = S.Id AND AUR.AppUserId = 1; + */ + + + //await AddSeriesModifiers(userId, series); + // var userProgress = await _context.AppUserProgresses + // .Where(p => p.AppUserId == userId && series.Select(s => s.Id).Contains(p.SeriesId) ) + // .ToListAsync(); + // + // var userRatings = await _context.AppUserRating + // .Where(r => r.AppUserId == userId && series.Select(s => s.Id).Contains(r.SeriesId)) + // .ToListAsync(); + // + // foreach (var s in series) + // { + // s.PagesRead = userProgress.Where(p => p.SeriesId == s.Id).Sum(p => p.PagesRead); + // var rating = userRatings.SingleOrDefault(r => r.SeriesId == s.Id); + // if (rating == null) continue; + // s.UserRating = rating.Rating; + // s.UserReview = rating.Review; + // } + var query = _context.Series .Where(s => s.LibraryId == libraryId) .OrderBy(s => s.SortName) @@ -77,10 +244,6 @@ namespace API.Data .AsNoTracking(); - // TODO: Refactor this into JOINs - //await AddSeriesModifiers(userId, series); - - _logger.LogDebug("Processed GetSeriesDtoForLibraryIdAsync in {ElapsedMilliseconds} milliseconds", sw.ElapsedMilliseconds); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } @@ -222,7 +385,7 @@ namespace API.Data return chapterIds.ToArray(); } - private async Task AddSeriesModifiers(int userId, List series) + public async Task AddSeriesModifiers(int userId, List series) { var userProgress = await _context.AppUserProgresses .Where(p => p.AppUserId == userId && series.Select(s => s.Id).Contains(p.SeriesId)) diff --git a/API/Extensions/LeftJoinExtensions.cs b/API/Extensions/LeftJoinExtensions.cs new file mode 100644 index 000000000..c4ea979a4 --- /dev/null +++ b/API/Extensions/LeftJoinExtensions.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace API.Extensions +{ + public static class LeftJoinExtensions +{ + public static IQueryable LeftJoin( + this IQueryable outer, + IQueryable inner, + Expression> outerKeySelector, + Expression> innerKeySelector, + Expression> resultSelector) + { + MethodInfo groupJoin = typeof (Queryable).GetMethods() + .Single(m => m.ToString() == "System.Linq.IQueryable`1[TResult] GroupJoin[TOuter,TInner,TKey,TResult](System.Linq.IQueryable`1[TOuter], System.Collections.Generic.IEnumerable`1[TInner], System.Linq.Expressions.Expression`1[System.Func`2[TOuter,TKey]], System.Linq.Expressions.Expression`1[System.Func`2[TInner,TKey]], System.Linq.Expressions.Expression`1[System.Func`3[TOuter,System.Collections.Generic.IEnumerable`1[TInner],TResult]])") + .MakeGenericMethod(typeof (TOuter), typeof (TInner), typeof (TKey), typeof (LeftJoinIntermediate)); + MethodInfo selectMany = typeof (Queryable).GetMethods() + .Single(m => m.ToString() == "System.Linq.IQueryable`1[TResult] SelectMany[TSource,TCollection,TResult](System.Linq.IQueryable`1[TSource], System.Linq.Expressions.Expression`1[System.Func`2[TSource,System.Collections.Generic.IEnumerable`1[TCollection]]], System.Linq.Expressions.Expression`1[System.Func`3[TSource,TCollection,TResult]])") + .MakeGenericMethod(typeof (LeftJoinIntermediate), typeof (TInner), typeof (TResult)); + + var groupJoinResultSelector = (Expression, LeftJoinIntermediate>>) + ((oneOuter, manyInners) => new LeftJoinIntermediate {OneOuter = oneOuter, ManyInners = manyInners}); + + MethodCallExpression exprGroupJoin = Expression.Call(groupJoin, outer.Expression, inner.Expression, outerKeySelector, innerKeySelector, groupJoinResultSelector); + + var selectManyCollectionSelector = (Expression, IEnumerable>>) + (t => t.ManyInners.DefaultIfEmpty()); + + ParameterExpression paramUser = resultSelector.Parameters.First(); + + ParameterExpression paramNew = Expression.Parameter(typeof (LeftJoinIntermediate), "t"); + MemberExpression propExpr = Expression.Property(paramNew, "OneOuter"); + + LambdaExpression selectManyResultSelector = Expression.Lambda(new Replacer(paramUser, propExpr).Visit(resultSelector.Body), paramNew, resultSelector.Parameters.Skip(1).First()); + + MethodCallExpression exprSelectMany = Expression.Call(selectMany, exprGroupJoin, selectManyCollectionSelector, selectManyResultSelector); + + return outer.Provider.CreateQuery(exprSelectMany); + } + + private class LeftJoinIntermediate + { + public TOuter OneOuter { get; set; } + public IEnumerable ManyInners { get; set; } + } + + private class Replacer : ExpressionVisitor + { + private readonly ParameterExpression _oldParam; + private readonly Expression _replacement; + + public Replacer(ParameterExpression oldParam, Expression replacement) + { + _oldParam = oldParam; + _replacement = replacement; + } + + public override Expression Visit(Expression exp) + { + if (exp == _oldParam) + { + return _replacement; + } + + return base.Visit(exp); + } + } +} +} \ No newline at end of file diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index 3fc58824a..6a9845975 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -39,5 +39,12 @@ namespace API.Interfaces Task GetVolumeByIdAsync(int volumeId); Task GetSeriesByIdAsync(int seriesId); Task GetChapterIdsForSeriesAsync(int[] seriesIds); + /// + /// Used to add Progress/Rating information to series list. + /// + /// + /// + /// + Task AddSeriesModifiers(int userId, List series); } } \ No newline at end of file diff --git a/API/Interfaces/Services/IArchiveService.cs b/API/Interfaces/Services/IArchiveService.cs index b150dd761..73452859f 100644 --- a/API/Interfaces/Services/IArchiveService.cs +++ b/API/Interfaces/Services/IArchiveService.cs @@ -1,4 +1,5 @@ using System.IO.Compression; +using API.Entities; namespace API.Interfaces.Services { @@ -9,5 +10,6 @@ namespace API.Interfaces.Services int GetNumberOfPagesFromArchive(string archivePath); byte[] GetCoverImage(string filepath, bool createThumbnail = false); bool IsValidArchive(string archivePath); + string GetSummaryInfo(string archivePath); } } \ No newline at end of file diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index fe3d93e1b..716c0d91b 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -144,6 +144,11 @@ namespace API.Services } + public string GetSummaryInfo(string archivePath) + { + throw new NotImplementedException(); + } + /// /// Extracts an archive to a temp cache directory. Returns path to new directory. If temp cache directory already exists, /// will return that without performing an extraction. Returns empty string if there are any invalidations which would diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index e8411e212..023c1decc 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -68,7 +68,7 @@ namespace API.Services if (string.IsNullOrEmpty(series.Summary) || forceUpdate) { - series.Summary = ""; + series.Summary = _archiveService.GetSummaryInfo(series.Volumes.First().Chapters.First().Files.First().FilePath); } } diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json index d2bf464c4..67e0049ba 100644 --- a/API/appsettings.Development.json +++ b/API/appsettings.Development.json @@ -6,7 +6,7 @@ "Logging": { "LogLevel": { "Default": "Debug", - "Microsoft": "Error", + "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Error", "Hangfire": "Information" },