mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Merge pull request #67 from Kareadita/feature/pagination
ComicInfo.xml Summaries
This commit is contained in:
commit
30352403cf
@ -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);
|
||||
|
@ -82,15 +82,44 @@ namespace API.Controllers
|
||||
userRating.Rating = updateSeriesRatingDto.UserRating;
|
||||
userRating.Review = updateSeriesRatingDto.UserReview;
|
||||
userRating.SeriesId = updateSeriesRatingDto.SeriesId;
|
||||
|
||||
_unitOfWork.UserRepository.AddRatingTracking(userRating);
|
||||
user.Ratings ??= new List<AppUserRating>();
|
||||
user.Ratings.Add(userRating);
|
||||
|
||||
|
||||
if (userRating.Id == 0)
|
||||
{
|
||||
user.Ratings ??= new List<AppUserRating>();
|
||||
user.Ratings.Add(userRating);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (!await _unitOfWork.Complete()) return BadRequest("There was a critical error.");
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
||||
{
|
||||
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id);
|
||||
|
||||
if (series == null) return BadRequest("Series does not exist");
|
||||
|
||||
// TODO: Support changing series properties once "locking" is implemented.
|
||||
// series.Name = updateSeries.Name;
|
||||
// series.OriginalName = updateSeries.OriginalName;
|
||||
// series.SortName = updateSeries.SortName;
|
||||
series.Summary = updateSeries.Summary;
|
||||
//series.CoverImage = updateSeries.CoverImage;
|
||||
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
|
||||
if (await _unitOfWork.Complete())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("There was an error with updating the series");
|
||||
}
|
||||
}
|
||||
}
|
14
API/DTOs/UpdateSeriesDto.cs
Normal file
14
API/DTOs/UpdateSeriesDto.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace API.DTOs
|
||||
{
|
||||
public class UpdateSeriesDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Name { get; init; }
|
||||
public string OriginalName { get; init; }
|
||||
public string SortName { get; init; }
|
||||
public string Summary { get; init; }
|
||||
public byte[] CoverImage { get; init; }
|
||||
public int UserRating { get; set; }
|
||||
public string UserReview { get; set; }
|
||||
}
|
||||
}
|
@ -67,9 +67,6 @@ namespace API.Data
|
||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
.OrderBy(s => s.SortName)
|
||||
@ -77,10 +74,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<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
@ -222,7 +215,7 @@ namespace API.Data
|
||||
return chapterIds.ToArray();
|
||||
}
|
||||
|
||||
private async Task AddSeriesModifiers(int userId, List<SeriesDto> series)
|
||||
public async Task AddSeriesModifiers(int userId, List<SeriesDto> series)
|
||||
{
|
||||
var userProgress = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId && series.Select(s => s.Id).Contains(p.SeriesId))
|
||||
|
73
API/Extensions/LeftJoinExtensions.cs
Normal file
73
API/Extensions/LeftJoinExtensions.cs
Normal file
@ -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<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
|
||||
this IQueryable<TOuter> outer,
|
||||
IQueryable<TInner> inner,
|
||||
Expression<Func<TOuter, TKey>> outerKeySelector,
|
||||
Expression<Func<TInner, TKey>> innerKeySelector,
|
||||
Expression<Func<TOuter, TInner, TResult>> 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<TOuter, TInner>));
|
||||
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<TOuter, TInner>), typeof (TInner), typeof (TResult));
|
||||
|
||||
var groupJoinResultSelector = (Expression<Func<TOuter, IEnumerable<TInner>, LeftJoinIntermediate<TOuter, TInner>>>)
|
||||
((oneOuter, manyInners) => new LeftJoinIntermediate<TOuter, TInner> {OneOuter = oneOuter, ManyInners = manyInners});
|
||||
|
||||
MethodCallExpression exprGroupJoin = Expression.Call(groupJoin, outer.Expression, inner.Expression, outerKeySelector, innerKeySelector, groupJoinResultSelector);
|
||||
|
||||
var selectManyCollectionSelector = (Expression<Func<LeftJoinIntermediate<TOuter, TInner>, IEnumerable<TInner>>>)
|
||||
(t => t.ManyInners.DefaultIfEmpty());
|
||||
|
||||
ParameterExpression paramUser = resultSelector.Parameters.First();
|
||||
|
||||
ParameterExpression paramNew = Expression.Parameter(typeof (LeftJoinIntermediate<TOuter, TInner>), "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<TResult>(exprSelectMany);
|
||||
}
|
||||
|
||||
private class LeftJoinIntermediate<TOuter, TInner>
|
||||
{
|
||||
public TOuter OneOuter { get; set; }
|
||||
public IEnumerable<TInner> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -39,5 +39,12 @@ namespace API.Interfaces
|
||||
Task<Volume> GetVolumeByIdAsync(int volumeId);
|
||||
Task<Series> GetSeriesByIdAsync(int seriesId);
|
||||
Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds);
|
||||
/// <summary>
|
||||
/// Used to add Progress/Rating information to series list.
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="series"></param>
|
||||
/// <returns></returns>
|
||||
Task AddSeriesModifiers(int userId, List<SeriesDto> series);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -10,8 +10,10 @@ namespace API.Parser
|
||||
{
|
||||
public static readonly string MangaFileExtensions = @"\.cbz|\.zip"; // |\.rar|\.cbr
|
||||
public static readonly string ImageFileExtensions = @"\.png|\.jpeg|\.jpg|\.gif";
|
||||
private static readonly string XmlRegexExtensions = @"\.xml";
|
||||
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex MangaFileRegex = new Regex(MangaFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
//?: is a non-capturing group in C#, else anything in () will be a group
|
||||
private static readonly Regex[] MangaVolumeRegex = new[]
|
||||
@ -406,6 +408,12 @@ namespace API.Parser
|
||||
return ImageRegex.IsMatch(fileInfo.Extension);
|
||||
}
|
||||
|
||||
public static bool IsXml(string filePath)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
return XmlRegex.IsMatch(fileInfo.Extension);
|
||||
}
|
||||
|
||||
public static float MinimumNumberFromRange(string range)
|
||||
{
|
||||
var tokens = range.Split("-");
|
||||
@ -416,5 +424,7 @@ namespace API.Parser
|
||||
{
|
||||
return name.ToLower().Replace("-", "").Replace(" ", "");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -3,6 +3,9 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.Serialization;
|
||||
using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
@ -77,7 +80,7 @@ namespace API.Services
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth);
|
||||
return thumbnail.WriteToBuffer(".jpg");
|
||||
return thumbnail.WriteToBuffer(".jpg"); // TODO: Validate this code works with .png files
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -144,6 +147,38 @@ namespace API.Services
|
||||
|
||||
}
|
||||
|
||||
public string GetSummaryInfo(string archivePath)
|
||||
{
|
||||
var summary = "";
|
||||
if (!IsValidArchive(archivePath)) return summary;
|
||||
|
||||
using var archive = ZipFile.OpenRead(archivePath);
|
||||
if (!archive.HasFiles()) return summary;
|
||||
|
||||
var info = archive.Entries.SingleOrDefault(x => Path.GetFileNameWithoutExtension(x.Name).ToLower() == "comicinfo" && Parser.Parser.IsXml(x.FullName));
|
||||
if (info == null) return summary;
|
||||
|
||||
// Parse XML file
|
||||
try
|
||||
{
|
||||
using var stream = info.Open();
|
||||
var serializer = new XmlSerializer(typeof(ComicInfo));
|
||||
ComicInfo comicInfo =
|
||||
(ComicInfo)serializer.Deserialize(stream);
|
||||
|
||||
if (comicInfo != null)
|
||||
{
|
||||
return comicInfo.Summary;
|
||||
}
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue parsing ComicInfo.xml from {ArchivePath}", archivePath);
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
15
API/Services/ComicInfo.cs
Normal file
15
API/Services/ComicInfo.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace API.Services
|
||||
{
|
||||
public class ComicInfo
|
||||
{
|
||||
public string Summary;
|
||||
public string Title;
|
||||
public string Series;
|
||||
public string Notes;
|
||||
public string Publisher;
|
||||
public string Genre;
|
||||
public int PageCount;
|
||||
public string LanguageISO;
|
||||
public string Web;
|
||||
}
|
||||
}
|
@ -65,10 +65,14 @@ namespace API.Services
|
||||
}
|
||||
series.CoverImage = firstCover?.CoverImage;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(series.Summary) && !forceUpdate) return;
|
||||
|
||||
if (string.IsNullOrEmpty(series.Summary) || forceUpdate)
|
||||
var firstVolume = series.Volumes.FirstOrDefault(v => v.Chapters.Any() && v.Number == 1);
|
||||
var firstChapter = firstVolume?.Chapters.FirstOrDefault(c => c.Files.Any());
|
||||
if (firstChapter != null)
|
||||
{
|
||||
series.Summary = "";
|
||||
series.Summary = _archiveService.GetSummaryInfo(firstChapter.Files.FirstOrDefault()?.FilePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft": "Error",
|
||||
"Microsoft": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Error",
|
||||
"Hangfire": "Information"
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user