Release Shakeout Day 1 (#1591)

* Fixed an issue where reading list were not able to update their summary due to a duplicate title check.

* Misc code smell cleanup

* Updated .net dependencies and removed unneeded ones

* Fixed an issue where removing a series from want to read list page wouldn't update the page correctly

* Fixed age restriction not applied to Recommended page

* Ensure that Genres and Tags are age restricted gated

* Persons are now age gated as well

* When you choose a cover, the new cover will properly be selected and will focus on it, in the cases there are many other covers available.

* Fixed caching profiles

* Added in a special hook when deleting a library to clear all series Relations before we delete
This commit is contained in:
Joe Milazzo 2022-10-18 16:53:17 -07:00 committed by GitHub
parent 03bd2e9103
commit b802e1e1b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 404 additions and 153 deletions

View file

@ -47,7 +47,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.4" />
<PackageReference Include="ExCSS" Version="4.1.0" />
<PackageReference Include="Flurl" Version="3.0.6" />
@ -59,20 +59,19 @@
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
<PackageReference Include="NetVips" Version="2.2.0" />
<PackageReference Include="NetVips.Native" Version="8.13.1" />
<PackageReference Include="Ng.UserAgentService" Version="1.1.0" />
<PackageReference Include="NReco.Logging.File" Version="1.1.5" />
<PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
@ -85,13 +84,13 @@
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.45.0.54064">
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.47.0.55603">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.23.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" />
<PackageReference Include="System.IO.Abstractions" Version="17.2.3" />
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
</ItemGroup>

View file

@ -11,6 +11,7 @@ using API.DTOs.Search;
using API.DTOs.System;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
using API.Extensions;
using API.Services;
using API.Services.Tasks.Scanner;
@ -251,6 +252,14 @@ public class LibraryController : BaseApiController
return BadRequest(
"You cannot delete a library while a scan is in progress. Please wait for scan to continue then try to delete");
}
// Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library
foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id))
{
s.Relations = new List<SeriesRelation>();
_unitOfWork.SeriesRepository.Update(s);
}
_unitOfWork.LibraryRepository.Delete(library);
await _unitOfWork.CommitAsync();

View file

@ -8,6 +8,7 @@ using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Metadata;
using API.Entities.Enums;
using API.Extensions;
using Kavita.Common.Extensions;
using Microsoft.AspNetCore.Mvc;
@ -31,15 +32,18 @@ public class MetadataController : BaseApiController
[HttpGet("genres")]
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids));
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, userId));
}
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync());
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(userId));
}
/// <summary>
/// Fetches people from the instance
/// </summary>
@ -48,12 +52,13 @@ public class MetadataController : BaseApiController
[HttpGet("people")]
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids));
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, userId));
}
return Ok(await _unitOfWork.PersonRepository.GetAllPeople());
return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(userId));
}
/// <summary>
@ -64,12 +69,13 @@ public class MetadataController : BaseApiController
[HttpGet("tags")]
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids));
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, userId));
}
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync());
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(userId));
}
/// <summary>

View file

@ -21,7 +21,6 @@ public class ReadingListController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
private readonly IReadingListService _readingListService;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
{
@ -219,22 +218,22 @@ public class ReadingListController : BaseApiController
dto.Title = dto.Title.Trim();
if (!string.IsNullOrEmpty(dto.Title))
{
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
if (hasExisting)
{
return BadRequest("A list of this name already exists");
}
readingList.Title = dto.Title;
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title);
}
if (!string.IsNullOrEmpty(dto.Title))
{
readingList.Summary = dto.Summary;
if (!readingList.Title.Equals(dto.Title))
{
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
if (hasExisting)
{
return BadRequest("A list of this name already exists");
}
readingList.Title = dto.Title;
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title);
}
}
readingList.Promoted = dto.Promoted;
readingList.CoverImageLocked = dto.CoverImageLocked;
if (!dto.CoverImageLocked)

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
using API.Extensions;

View file

@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data.Misc;
using API.DTOs.Metadata;
using API.Entities;
using API.Extensions;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
@ -15,9 +17,9 @@ public interface IGenreRepository
void Remove(Genre genre);
Task<Genre> FindByNameAsync(string genreName);
Task<IList<Genre>> GetAllGenresAsync();
Task<IList<GenreTagDto>> GetAllGenreDtosAsync();
Task<IList<GenreTagDto>> GetAllGenreDtosAsync(int userId);
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds);
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds, int userId);
Task<int> GetCountAsync();
}
@ -63,10 +65,18 @@ public class GenreRepository : IGenreRepository
await _context.SaveChangesAsync();
}
public async Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds)
/// <summary>
/// Returns a set of Genre tags for a set of library Ids. UserId will restrict returned Genres based on user's age restriction.
/// </summary>
/// <param name="libraryIds"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds, int userId)
{
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.RestrictAgainstAgeRestriction(userRating)
.SelectMany(s => s.Metadata.Genres)
.AsSplitQuery()
.Distinct()
@ -75,6 +85,7 @@ public class GenreRepository : IGenreRepository
.ToListAsync();
}
public async Task<int> GetCountAsync()
{
return await _context.Genre.CountAsync();
@ -85,9 +96,11 @@ public class GenreRepository : IGenreRepository
return await _context.Genre.ToListAsync();
}
public async Task<IList<GenreTagDto>> GetAllGenreDtosAsync()
public async Task<IList<GenreTagDto>> GetAllGenreDtosAsync(int userId)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.Genre
.RestrictAgainstAgeRestriction(ageRating)
.AsNoTracking()
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();

View file

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
using API.Extensions;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
@ -14,8 +15,9 @@ public interface IPersonRepository
void Attach(Person person);
void Remove(Person person);
Task<IList<Person>> GetAllPeople();
Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId);
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds);
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId);
Task<int> GetCountAsync();
}
@ -40,14 +42,6 @@ public class PersonRepository : IPersonRepository
_context.Person.Remove(person);
}
public async Task<Person> FindByNameAsync(string name)
{
var normalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name);
return await _context.Person
.Where(p => normalizedName.Equals(p.NormalizedName))
.SingleOrDefaultAsync();
}
public async Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false)
{
var peopleWithNoConnections = await _context.Person
@ -62,10 +56,12 @@ public class PersonRepository : IPersonRepository
await _context.SaveChangesAsync();
}
public async Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds)
public async Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.RestrictAgainstAgeRestriction(ageRating)
.SelectMany(s => s.Metadata.People)
.Distinct()
.OrderBy(p => p.Name)
@ -87,4 +83,14 @@ public class PersonRepository : IPersonRepository
.OrderBy(p => p.Name)
.ToListAsync();
}
public async Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.Person
.OrderBy(p => p.Name)
.RestrictAgainstAgeRestriction(ageRating)
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
}

View file

@ -291,7 +291,7 @@ public class SeriesRepository : ISeriesRepository
const int maxRecords = 15;
var result = new SearchResultGroupDto();
var searchQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(searchQuery);
var userRating = await GetUserAgeRestriction(userId);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var seriesIds = _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
@ -723,7 +723,7 @@ public class SeriesRepository : ISeriesRepository
private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter)
{
var userLibraries = await GetUserLibraries(libraryId, userId);
var userRating = await GetUserAgeRestriction(userId);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries,
out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter,
@ -1027,59 +1027,46 @@ public class SeriesRepository : ISeriesRepository
public async Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30)
{
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
var index = 0;
var userRating = await GetUserAgeRestriction(userId);
var index = 0;
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var items = (await GetRecentlyAddedChaptersQuery(userId));
if (userRating.AgeRating != AgeRating.NotApplicable)
var items = (await GetRecentlyAddedChaptersQuery(userId));
if (userRating.AgeRating != AgeRating.NotApplicable)
{
items = items.RestrictAgainstAgeRestriction(userRating);
}
foreach (var item in items)
{
if (seriesMap.Keys.Count == pageSize) break;
if (seriesMap.ContainsKey(item.SeriesName))
{
items = items.RestrictAgainstAgeRestriction(userRating);
seriesMap[item.SeriesName].Count += 1;
}
foreach (var item in items)
else
{
if (seriesMap.Keys.Count == pageSize) break;
if (seriesMap.ContainsKey(item.SeriesName))
seriesMap[item.SeriesName] = new GroupedSeriesDto()
{
seriesMap[item.SeriesName].Count += 1;
}
else
{
seriesMap[item.SeriesName] = new GroupedSeriesDto()
{
LibraryId = item.LibraryId,
LibraryType = item.LibraryType,
SeriesId = item.SeriesId,
SeriesName = item.SeriesName,
Created = item.Created,
Id = index,
Format = item.Format,
Count = 1,
};
index += 1;
}
LibraryId = item.LibraryId,
LibraryType = item.LibraryType,
SeriesId = item.SeriesId,
SeriesName = item.SeriesName,
Created = item.Created,
Id = index,
Format = item.Format,
Count = 1,
};
index += 1;
}
}
return seriesMap.Values.AsEnumerable();
}
private async Task<AgeRestriction> GetUserAgeRestriction(int userId)
{
return await _context.AppUser
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u =>
new AgeRestriction(){
AgeRating = u.AgeRestriction,
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
})
.SingleAsync();
return seriesMap.Values.AsEnumerable();
}
public async Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind)
{
var libraryIds = GetLibraryIdsForUser(userId);
var userRating = await GetUserAgeRestriction(userId);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var usersSeriesIds = _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
@ -1108,9 +1095,14 @@ public class SeriesRepository : ISeriesRepository
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
// Because this can be called from an API, we need to provide an additional check if the genre has anything the
// user with age restrictions can access
var query = _context.Series
.Where(s => s.Metadata.Genres.Select(g => g.Id).Contains(genreId))
.Where(s => usersSeriesIds.Contains(s.Id))
.RestrictAgainstAgeRestriction(userRating)
.AsSplitQuery()
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
@ -1147,7 +1139,7 @@ public class SeriesRepository : ISeriesRepository
public async Task<SeriesDto> GetSeriesForMangaFile(int mangaFileId, int userId)
{
var libraryIds = GetLibraryIdsForUser(userId);
var userRating = await GetUserAgeRestriction(userId);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.MangaFile
.Where(m => m.Id == mangaFileId)
@ -1164,7 +1156,7 @@ public class SeriesRepository : ISeriesRepository
public async Task<SeriesDto> GetSeriesForChapter(int chapterId, int userId)
{
var libraryIds = GetLibraryIdsForUser(userId);
var userRating = await GetUserAgeRestriction(userId);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.Chapter
.Where(m => m.Id == chapterId)
.AsSplitQuery()
@ -1321,9 +1313,11 @@ public class SeriesRepository : ISeriesRepository
.Where(s => usersSeriesIds.Contains(s.SeriesId) && s.Rating > 4)
.Select(p => p.SeriesId)
.Distinct();
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var query = _context.Series
.Where(s => distinctSeriesIdsWithHighRating.Contains(s.Id))
.RestrictAgainstAgeRestriction(userRating)
.AsSplitQuery()
.OrderByDescending(s => _context.AppUserRating.Where(r => r.SeriesId == s.Id).Select(r => r.Rating).Average())
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
@ -1340,6 +1334,7 @@ public class SeriesRepository : ISeriesRepository
.Where(s => usersSeriesIds.Contains(s.SeriesId))
.Select(p => p.SeriesId)
.Distinct();
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var query = _context.Series
@ -1349,6 +1344,7 @@ public class SeriesRepository : ISeriesRepository
&& !distinctSeriesIdsWithProgress.Contains(s.Id) &&
usersSeriesIds.Contains(s.Id))
.Where(s => s.Metadata.PublicationStatus != PublicationStatus.OnGoing)
.RestrictAgainstAgeRestriction(userRating)
.AsSplitQuery()
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
@ -1365,6 +1361,8 @@ public class SeriesRepository : ISeriesRepository
.Select(p => p.SeriesId)
.Distinct();
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var query = _context.Series
.Where(s => (
@ -1373,6 +1371,7 @@ public class SeriesRepository : ISeriesRepository
&& !distinctSeriesIdsWithProgress.Contains(s.Id) &&
usersSeriesIds.Contains(s.Id))
.Where(s => s.Metadata.PublicationStatus == PublicationStatus.OnGoing)
.RestrictAgainstAgeRestriction(userRating)
.AsSplitQuery()
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
@ -1406,7 +1405,7 @@ public class SeriesRepository : ISeriesRepository
{
var libraryIds = GetLibraryIdsForUser(userId);
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
var userRating = await GetUserAgeRestriction(userId);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
return new RelatedSeriesDto()
{

View file

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using API.DTOs.Metadata;
using API.Entities;
using API.Extensions;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
@ -13,11 +14,10 @@ public interface ITagRepository
{
void Attach(Tag tag);
void Remove(Tag tag);
Task<Tag> FindByNameAsync(string tagName);
Task<IList<Tag>> GetAllTagsAsync();
Task<IList<TagDto>> GetAllTagDtosAsync();
Task<IList<TagDto>> GetAllTagDtosAsync(int userId);
Task RemoveAllTagNoLongerAssociated(bool removeExternal = false);
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds);
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds, int userId);
}
public class TagRepository : ITagRepository
@ -41,13 +41,6 @@ public class TagRepository : ITagRepository
_context.Tag.Remove(tag);
}
public async Task<Tag> FindByNameAsync(string tagName)
{
var normalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(tagName);
return await _context.Tag
.FirstOrDefaultAsync(g => g.NormalizedTitle.Equals(normalizedName));
}
public async Task RemoveAllTagNoLongerAssociated(bool removeExternal = false)
{
var tagsWithNoConnections = await _context.Tag
@ -62,10 +55,12 @@ public class TagRepository : ITagRepository
await _context.SaveChangesAsync();
}
public async Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds)
public async Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds, int userId)
{
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.RestrictAgainstAgeRestriction(userRating)
.SelectMany(s => s.Metadata.Tags)
.AsSplitQuery()
.Distinct()
@ -80,10 +75,12 @@ public class TagRepository : ITagRepository
return await _context.Tag.ToListAsync();
}
public async Task<IList<TagDto>> GetAllTagDtosAsync()
public async Task<IList<TagDto>> GetAllTagDtosAsync(int userId)
{
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.Tag
.AsNoTracking()
.RestrictAgainstAgeRestriction(userRating)
.OrderBy(t => t.Title)
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
.ToListAsync();

View file

@ -1,7 +1,9 @@
using System.Linq;
using System.Threading.Tasks;
using API.Data.Misc;
using API.Entities;
using API.Entities.Enums;
using Microsoft.EntityFrameworkCore;
namespace API.Extensions;
@ -33,6 +35,48 @@ public static class QueryableExtensions
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
}
public static IQueryable<Genre> RestrictAgainstAgeRestriction(this IQueryable<Genre> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
if (restriction.IncludeUnknowns)
{
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating));
}
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
}
public static IQueryable<Tag> RestrictAgainstAgeRestriction(this IQueryable<Tag> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
if (restriction.IncludeUnknowns)
{
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating));
}
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
}
public static IQueryable<Person> RestrictAgainstAgeRestriction(this IQueryable<Person> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
if (restriction.IncludeUnknowns)
{
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating));
}
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
}
public static IQueryable<ReadingList> RestrictAgainstAgeRestriction(this IQueryable<ReadingList> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
@ -45,4 +89,25 @@ public static class QueryableExtensions
return q;
}
public static Task<AgeRestriction> GetUserAgeRestriction(this DbSet<AppUser> queryable, int userId)
{
if (userId < 1)
{
return Task.FromResult(new AgeRestriction()
{
AgeRating = AgeRating.NotApplicable,
IncludeUnknowns = true
});
}
return queryable
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u =>
new AgeRestriction(){
AgeRating = u.AgeRestriction,
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
})
.SingleAsync();
}
}

View file

@ -63,14 +63,4 @@ public static class GenreHelper
metadataGenres.Add(genre);
}
}
public static void AddGenreIfNotExists(BlockingCollection<Genre> metadataGenres, Genre genre)
{
var existingGenre = metadataGenres.FirstOrDefault(p =>
p.NormalizedTitle == Services.Tasks.Scanner.Parser.Parser.Normalize(genre.Title));
if (existingGenre == null)
{
metadataGenres.Add(genre);
}
}
}

View file

@ -8,6 +8,7 @@ using API.Data;
using API.Data.Metadata;
using API.Data.Repositories;
using API.Data.Scanner;
using API.DTOs.Metadata;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;

View file

@ -68,6 +68,7 @@ public class ParseScannedFiles
/// This will Scan all files in a folder path. For each folder within the folderPath, FolderAction will be invoked for all files contained
/// </summary>
/// <param name="scanDirectoryByDirectory">Scan directory by directory and for each, call folderAction</param>
/// <param name="seriesPaths">A dictionary mapping a normalized path to a list of <see cref="SeriesModified"/> to help scanner skip I/O</param>
/// <param name="folderPath">A library folder or series folder</param>
/// <param name="folderAction">A callback async Task to be called once all files for each folder path are found</param>
/// <param name="forceCheck">If we should bypass any folder last write time checks on the scan and force I/O</param>
@ -215,6 +216,7 @@ public class ParseScannedFiles
/// Using a normalized name from the passed ParserInfo, this checks against all found series so far and if an existing one exists with
/// same normalized name, it merges into the existing one. This is important as some manga may have a slight difference with punctuation or capitalization.
/// </summary>
/// <param name="scannedSeries"></param>
/// <param name="info"></param>
/// <returns>Series Name to group this info into</returns>
private string MergeName(ConcurrentDictionary<ParsedSeries, List<ParserInfo>> scannedSeries, ParserInfo info)

View file

@ -749,12 +749,12 @@ public static class Parser
foreach (var regex in MangaChapterRegex)
{
var matches = regex.Matches(filename);
foreach (Match match in matches)
foreach (var groups in matches.Select(match => match.Groups))
{
if (!match.Groups["Chapter"].Success || match.Groups["Chapter"] == Match.Empty) continue;
if (!groups["Chapter"].Success || groups["Chapter"] == Match.Empty) continue;
var value = match.Groups["Chapter"].Value;
var hasPart = match.Groups["Part"].Success;
var value = groups["Chapter"].Value;
var hasPart = groups["Part"].Success;
return FormatValue(value, hasPart);
}
@ -778,11 +778,11 @@ public static class Parser
foreach (var regex in ComicChapterRegex)
{
var matches = regex.Matches(filename);
foreach (Match match in matches)
foreach (var groups in matches.Select(match => match.Groups))
{
if (!match.Groups["Chapter"].Success || match.Groups["Chapter"] == Match.Empty) continue;
var value = match.Groups["Chapter"].Value;
var hasPart = match.Groups["Part"].Success;
if (!groups["Chapter"].Success || groups["Chapter"] == Match.Empty) continue;
var value = groups["Chapter"].Value;
var hasPart = groups["Part"].Success;
return FormatValue(value, hasPart);
}

View file

@ -428,7 +428,7 @@ public class ScannerService : IScannerService
/// <param name="forceUpdate">Defaults to false</param>
[Queue(TaskScheduler.ScanQueue)]
[DisableConcurrentExecution(60 * 60 * 60)]
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
{
var sw = Stopwatch.StartNew();

View file

@ -75,7 +75,7 @@ public class TokenService : ITokenService
var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.NameId)?.Value;
var user = await _userManager.FindByNameAsync(username);
if (user == null) return null; // This forces a logout
var isValid = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "RefreshToken", request.RefreshToken);
await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "RefreshToken", request.RefreshToken);
await _userManager.UpdateSecurityStampAsync(user);

View file

@ -90,7 +90,6 @@ public class PresenceTracker : IPresenceTracker
public Task<string[]> GetOnlineAdmins()
{
// TODO: This might end in stale data, we want to get the online users, query against DB to check if they are admins then return
string[] onlineUsers;
lock (OnlineUsers)
{

View file

@ -74,21 +74,21 @@ public class Startup
new CacheProfile()
{
Duration = 60 * 10,
Location = ResponseCacheLocation.Any,
Location = ResponseCacheLocation.None,
NoStore = false
});
options.CacheProfiles.Add("5Minute",
new CacheProfile()
{
Duration = 60 * 5,
Location = ResponseCacheLocation.Any,
Location = ResponseCacheLocation.None,
});
// Instant is a very quick cache, because we can't bust based on the query params, but rather body
options.CacheProfiles.Add("Instant",
new CacheProfile()
{
Duration = 30,
Location = ResponseCacheLocation.Any,
Location = ResponseCacheLocation.None,
});
});
services.Configure<ForwardedHeadersOptions>(options =>
@ -300,13 +300,6 @@ public class Startup
app.Use(async (context, next) =>
{
// Note: I removed this as I caught Chrome caching api responses when it shouldn't have
// context.Response.GetTypedHeaders().CacheControl =
// new CacheControlHeaderValue()
// {
// Public = false,
// MaxAge = TimeSpan.FromSeconds(10),
// };
context.Response.Headers[HeaderNames.Vary] =
new[] { "Accept-Encoding" };