A bunch of bug fixes and some enhancements (#3871)
Co-authored-by: Joseph Milazzo <josephmajora@gmail.com>
This commit is contained in:
parent
62231d3c4e
commit
6fa1cf994e
24 changed files with 1464 additions and 91 deletions
|
|
@ -185,7 +185,7 @@ public class PersonController : BaseApiController
|
|||
[HttpGet("series-known-for")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetKnownSeries(int personId)
|
||||
{
|
||||
return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId));
|
||||
return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId, User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -206,6 +206,7 @@ public class PersonController : BaseApiController
|
|||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("merge")]
|
||||
[Authorize("RequireAdminRole")]
|
||||
public async Task<ActionResult<PersonDto>> MergePeople(PersonMergeDto dto)
|
||||
{
|
||||
var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ public sealed record ExternalSeriesDetailDto
|
|||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
public int AverageScore { get; set; }
|
||||
/// <remarks>AniList returns the total count of unique chapters, includes 1.1 for example</remarks>
|
||||
public int Chapters { get; set; }
|
||||
/// <remarks>AniList returns the total count of unique volumes, includes 1.1 for example</remarks>
|
||||
public int Volumes { get; set; }
|
||||
public IList<SeriesRelationship>? Relations { get; set; } = [];
|
||||
public IList<SeriesCharacter>? Characters { get; set; } = [];
|
||||
|
|
|
|||
|
|
@ -173,20 +173,30 @@ public class GenreRepository : IGenreRepository
|
|||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
var allLibrariesCount = await _context.Library.CountAsync();
|
||||
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
|
||||
var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync();
|
||||
|
||||
var query = _context.Genre
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.WhereIf(allLibrariesCount != userLibs.Count,
|
||||
genre => genre.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) ||
|
||||
genre.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId)))
|
||||
.Select(g => new BrowseGenreDto
|
||||
{
|
||||
Id = g.Id,
|
||||
Title = g.Title,
|
||||
SeriesCount = g.SeriesMetadatas
|
||||
.Select(sm => sm.Id)
|
||||
.Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId))
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.Distinct()
|
||||
.Count(),
|
||||
ChapterCount = g.Chapters
|
||||
.Select(ch => ch.Id)
|
||||
.Where(cp => allLibrariesCount == userLibs.Count || seriesIds.Contains(cp.Volume.SeriesId))
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.Distinct()
|
||||
.Count()
|
||||
.Count(),
|
||||
})
|
||||
.OrderBy(g => g.Title);
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public interface IPersonRepository
|
|||
Task<Person?> GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases);
|
||||
Task<bool> IsNameUnique(string name);
|
||||
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId, int userId);
|
||||
Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role);
|
||||
/// <summary>
|
||||
/// Returns all people with a matching name, or alias
|
||||
|
|
@ -179,20 +179,25 @@ public class PersonRepository : IPersonRepository
|
|||
public async Task<IEnumerable<PersonRole>> GetRolesForPersonByName(int personId, int userId)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||
|
||||
// Query roles from ChapterPeople
|
||||
var chapterRoles = await _context.Person
|
||||
.Where(p => p.Id == personId)
|
||||
.SelectMany(p => p.ChapterPeople)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.SelectMany(p => p.ChapterPeople.Select(cp => cp.Role))
|
||||
.RestrictByLibrary(userLibs)
|
||||
.Select(cp => cp.Role)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// Query roles from SeriesMetadataPeople
|
||||
var seriesRoles = await _context.Person
|
||||
.Where(p => p.Id == personId)
|
||||
.SelectMany(p => p.SeriesMetadataPeople)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.SelectMany(p => p.SeriesMetadataPeople.Select(smp => smp.Role))
|
||||
.RestrictByLibrary(userLibs)
|
||||
.Select(smp => smp.Role)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -204,44 +209,53 @@ public class PersonRepository : IPersonRepository
|
|||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
var query = CreateFilteredPersonQueryable(userId, filter, ageRating);
|
||||
var query = await CreateFilteredPersonQueryable(userId, filter, ageRating);
|
||||
|
||||
return await PagedList<BrowsePersonDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
private IQueryable<BrowsePersonDto> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating)
|
||||
private async Task<IQueryable<BrowsePersonDto>> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating)
|
||||
{
|
||||
var allLibrariesCount = await _context.Library.CountAsync();
|
||||
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
|
||||
var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync();
|
||||
|
||||
var query = _context.Person.AsNoTracking();
|
||||
|
||||
// Apply filtering based on statements
|
||||
query = BuildPersonFilterQuery(userId, filter, query);
|
||||
|
||||
// Apply age restriction
|
||||
query = query.RestrictAgainstAgeRestriction(ageRating);
|
||||
// Apply restrictions
|
||||
query = query.RestrictAgainstAgeRestriction(ageRating)
|
||||
.WhereIf(allLibrariesCount != userLibs.Count,
|
||||
person => person.ChapterPeople.Any(cp => seriesIds.Contains(cp.Chapter.Volume.SeriesId)) ||
|
||||
person.SeriesMetadataPeople.Any(smp => seriesIds.Contains(smp.SeriesMetadata.SeriesId)));
|
||||
|
||||
// Apply sorting and limiting
|
||||
var sortedQuery = query.SortBy(filter.SortOptions);
|
||||
|
||||
var limitedQuery = ApplyPersonLimit(sortedQuery, filter.LimitTo);
|
||||
|
||||
// Project to DTO
|
||||
var projectedQuery = limitedQuery.Select(p => new BrowsePersonDto
|
||||
return limitedQuery.Select(p => new BrowsePersonDto
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Description = p.Description,
|
||||
CoverImage = p.CoverImage,
|
||||
SeriesCount = p.SeriesMetadataPeople
|
||||
.Select(smp => smp.SeriesMetadata.SeriesId)
|
||||
.Select(smp => smp.SeriesMetadata)
|
||||
.Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId))
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.Distinct()
|
||||
.Count(),
|
||||
ChapterCount = p.ChapterPeople
|
||||
.Select(cp => cp.Chapter.Id)
|
||||
.Select(chp => chp.Chapter)
|
||||
.Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId))
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.Distinct()
|
||||
.Count()
|
||||
.Count(),
|
||||
});
|
||||
|
||||
return projectedQuery;
|
||||
}
|
||||
|
||||
private static IQueryable<Person> BuildPersonFilterQuery(int userId, BrowsePersonFilterDto filterDto, IQueryable<Person> query)
|
||||
|
|
@ -287,11 +301,13 @@ public class PersonRepository : IPersonRepository
|
|||
{
|
||||
var normalized = name.ToNormalized();
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||
|
||||
return await _context.Person
|
||||
.Where(p => p.NormalizedName == normalized)
|
||||
.Includes(includes)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.RestrictByLibrary(userLibs)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
|
@ -313,14 +329,18 @@ public class PersonRepository : IPersonRepository
|
|||
.AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name)));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId)
|
||||
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId, int userId)
|
||||
{
|
||||
List<PersonRole> notValidRoles = [PersonRole.Location, PersonRole.Team, PersonRole.Other, PersonRole.Publisher, PersonRole.Translator];
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
|
||||
return await _context.Person
|
||||
.Where(p => p.Id == personId)
|
||||
.SelectMany(p => p.SeriesMetadataPeople.Where(smp => !notValidRoles.Contains(smp.Role)))
|
||||
.SelectMany(p => p.SeriesMetadataPeople)
|
||||
.Select(smp => smp.SeriesMetadata)
|
||||
.Select(sm => sm.Series)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.Where(s => userLibs.Contains(s.LibraryId))
|
||||
.Distinct()
|
||||
.OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating)
|
||||
.Take(20)
|
||||
|
|
@ -331,11 +351,13 @@ public class PersonRepository : IPersonRepository
|
|||
public async Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||
|
||||
return await _context.ChapterPeople
|
||||
.Where(cp => cp.PersonId == personId && cp.Role == role)
|
||||
.Select(cp => cp.Chapter)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.RestrictByLibrary(userLibs)
|
||||
.OrderBy(ch => ch.SortOrder)
|
||||
.Take(20)
|
||||
.ProjectTo<StandaloneChapterDto>(_mapper.ConfigurationProvider)
|
||||
|
|
@ -386,27 +408,31 @@ public class PersonRepository : IPersonRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.Aliases)
|
||||
public async Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||
|
||||
return await _context.Person
|
||||
.Includes(includes)
|
||||
.OrderBy(p => p.Name)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.RestrictByLibrary(userLibs)
|
||||
.OrderBy(p => p.Name)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.Aliases)
|
||||
public async Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||
|
||||
return await _context.Person
|
||||
.Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters
|
||||
.Includes(includes)
|
||||
.OrderBy(p => p.Name)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.RestrictByLibrary(userLibs)
|
||||
.OrderBy(p => p.Name)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,18 +111,28 @@ public class TagRepository : ITagRepository
|
|||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
var allLibrariesCount = await _context.Library.CountAsync();
|
||||
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
|
||||
var seriesIds = _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id);
|
||||
|
||||
var query = _context.Tag
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.WhereIf(userLibs.Count != allLibrariesCount,
|
||||
tag => tag.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) ||
|
||||
tag.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId)))
|
||||
.Select(g => new BrowseTagDto
|
||||
{
|
||||
Id = g.Id,
|
||||
Title = g.Title,
|
||||
SeriesCount = g.SeriesMetadatas
|
||||
.Select(sm => sm.Id)
|
||||
.Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId))
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.Distinct()
|
||||
.Count(),
|
||||
ChapterCount = g.Chapters
|
||||
.Select(ch => ch.Id)
|
||||
.Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId))
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.Distinct()
|
||||
.Count()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.Data.Misc;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
|
||||
|
|
@ -55,4 +56,16 @@ public static class EnumerableExtensions
|
|||
|
||||
return q;
|
||||
}
|
||||
|
||||
public static IEnumerable<Chapter> RestrictAgainstAgeRestriction(this IEnumerable<Chapter> items, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return items;
|
||||
var q = items.Where(s => s.AgeRating <= restriction.AgeRating);
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(s => s.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,19 @@ public static class RestrictByAgeExtensions
|
|||
return q;
|
||||
}
|
||||
|
||||
public static IQueryable<SeriesMetadataPeople> RestrictAgainstAgeRestriction(this IQueryable<SeriesMetadataPeople> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
var q = queryable.Where(s => s.SeriesMetadata.AgeRating <= restriction.AgeRating);
|
||||
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(s => s.SeriesMetadata.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
|
||||
public static IQueryable<Chapter> RestrictAgainstAgeRestriction(this IQueryable<Chapter> queryable, AgeRestriction restriction)
|
||||
{
|
||||
|
|
@ -41,6 +54,19 @@ public static class RestrictByAgeExtensions
|
|||
return q;
|
||||
}
|
||||
|
||||
public static IQueryable<ChapterPeople> RestrictAgainstAgeRestriction(this IQueryable<ChapterPeople> queryable, AgeRestriction restriction)
|
||||
{
|
||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||
var q = queryable.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating <= restriction.AgeRating);
|
||||
|
||||
if (!restriction.IncludeUnknowns)
|
||||
{
|
||||
return q.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating != AgeRating.Unknown);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
|
||||
public static IQueryable<AppUserCollection> RestrictAgainstAgeRestriction(this IQueryable<AppUserCollection> queryable, AgeRestriction restriction)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Entities.Person;
|
||||
|
||||
namespace API.Extensions.QueryExtensions;
|
||||
|
||||
public static class RestrictByLibraryExtensions
|
||||
{
|
||||
|
||||
public static IQueryable<Person> RestrictByLibrary(this IQueryable<Person> query, IQueryable<int> userLibs)
|
||||
{
|
||||
return query.Where(p =>
|
||||
p.ChapterPeople.Any(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId)) ||
|
||||
p.SeriesMetadataPeople.Any(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId)));
|
||||
}
|
||||
|
||||
public static IQueryable<Chapter> RestrictByLibrary(this IQueryable<Chapter> query, IQueryable<int> userLibs)
|
||||
{
|
||||
return query.Where(cp => userLibs.Contains(cp.Volume.Series.LibraryId));
|
||||
}
|
||||
|
||||
public static IQueryable<SeriesMetadataPeople> RestrictByLibrary(this IQueryable<SeriesMetadataPeople> query, IQueryable<int> userLibs)
|
||||
{
|
||||
return query.Where(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId));
|
||||
}
|
||||
|
||||
public static IQueryable<ChapterPeople> RestrictByLibrary(this IQueryable<ChapterPeople> query, IQueryable<int> userLibs)
|
||||
{
|
||||
return query.Where(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId));
|
||||
}
|
||||
}
|
||||
|
|
@ -156,4 +156,24 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChapterBuilder WithTags(IList<Tag> tags)
|
||||
{
|
||||
_chapter.Tags ??= [];
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
_chapter.Tags.Add(tag);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChapterBuilder WithGenres(IList<Genre> genres)
|
||||
{
|
||||
_chapter.Genres ??= [];
|
||||
foreach (var genre in genres)
|
||||
{
|
||||
_chapter.Genres.Add(genre);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,4 +108,23 @@ public class SeriesMetadataBuilder : IEntityBuilder<SeriesMetadata>
|
|||
_seriesMetadata.TagsLocked = lockStatus;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SeriesMetadataBuilder WithTags(List<Tag> tags, bool lockStatus = false)
|
||||
{
|
||||
_seriesMetadata.Tags = tags;
|
||||
_seriesMetadata.TagsLocked = lockStatus;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SeriesMetadataBuilder WithMaxCount(int count)
|
||||
{
|
||||
_seriesMetadata.MaxCount = count;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SeriesMetadataBuilder WithTotalCount(int count)
|
||||
{
|
||||
_seriesMetadata.TotalCount = count;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1057,6 +1057,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
var status = DeterminePublicationStatus(series, chapters, externalMetadata);
|
||||
|
||||
series.Metadata.PublicationStatus = status;
|
||||
series.Metadata.PublicationStatusLocked = true;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -1188,32 +1189,39 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
|
||||
#region Rating
|
||||
|
||||
var averageCriticRating = metadata.CriticReviews.Average(r => r.Rating);
|
||||
var averageUserRating = metadata.UserReviews.Average(r => r.Rating);
|
||||
// C# can't make the implicit conversation here
|
||||
float? averageCriticRating = metadata.CriticReviews.Count > 0 ? metadata.CriticReviews.Average(r => r.Rating) : null;
|
||||
float? averageUserRating = metadata.UserReviews.Count > 0 ? metadata.UserReviews.Average(r => r.Rating) : null;
|
||||
|
||||
var existingRatings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapter.Id);
|
||||
_unitOfWork.ExternalSeriesMetadataRepository.Remove(existingRatings);
|
||||
|
||||
chapter.ExternalRatings =
|
||||
[
|
||||
new ExternalRating
|
||||
chapter.ExternalRatings = [];
|
||||
|
||||
if (averageUserRating != null)
|
||||
{
|
||||
chapter.ExternalRatings.Add(new ExternalRating
|
||||
{
|
||||
AverageScore = (int) averageUserRating,
|
||||
Provider = ScrobbleProvider.Cbr,
|
||||
Authority = RatingAuthority.User,
|
||||
ProviderUrl = metadata.IssueUrl,
|
||||
},
|
||||
new ExternalRating
|
||||
|
||||
});
|
||||
chapter.AverageExternalRating = averageUserRating.Value;
|
||||
}
|
||||
|
||||
if (averageCriticRating != null)
|
||||
{
|
||||
chapter.ExternalRatings.Add(new ExternalRating
|
||||
{
|
||||
AverageScore = (int) averageCriticRating,
|
||||
Provider = ScrobbleProvider.Cbr,
|
||||
Authority = RatingAuthority.Critic,
|
||||
ProviderUrl = metadata.IssueUrl,
|
||||
|
||||
},
|
||||
];
|
||||
|
||||
chapter.AverageExternalRating = averageUserRating;
|
||||
});
|
||||
}
|
||||
|
||||
madeModification = averageUserRating > 0f || averageCriticRating > 0f || madeModification;
|
||||
|
||||
|
|
@ -1563,16 +1571,16 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
var maxVolume = (int)(nonSpecialVolumes.Count != 0 ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0);
|
||||
var maxChapter = (int)chapters.Max(c => c.MaxNumber);
|
||||
|
||||
if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1)
|
||||
if (series.Format is MangaFormat.Epub or MangaFormat.Pdf && chapters.Count == 1)
|
||||
{
|
||||
series.Metadata.MaxCount = 1;
|
||||
}
|
||||
else if (series.Metadata.TotalCount <= 1 && chapters.Count == 1 && chapters[0].IsSpecial)
|
||||
else if (series.Metadata.TotalCount <= 1 && chapters is [{ IsSpecial: true }])
|
||||
{
|
||||
series.Metadata.MaxCount = series.Metadata.TotalCount;
|
||||
}
|
||||
else if ((maxChapter == Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) &&
|
||||
maxVolume <= series.Metadata.TotalCount)
|
||||
maxVolume <= series.Metadata.TotalCount && maxVolume != Parser.DefaultChapterNumber)
|
||||
{
|
||||
series.Metadata.MaxCount = maxVolume;
|
||||
}
|
||||
|
|
@ -1593,8 +1601,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
{
|
||||
status = PublicationStatus.Ended;
|
||||
|
||||
// Check if all volumes/chapters match the total count
|
||||
if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0)
|
||||
if (IsSeriesCompleted(series, chapters, externalMetadata, maxVolume))
|
||||
{
|
||||
status = PublicationStatus.Completed;
|
||||
}
|
||||
|
|
@ -1610,6 +1617,68 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
return PublicationStatus.OnGoing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the series should be marked as completed, checks loosey with chapter and series numbers.
|
||||
/// Respects Specials to reach the required amount.
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="chapters"></param>
|
||||
/// <param name="externalMetadata"></param>
|
||||
/// <param name="maxVolumes"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>Updates MaxCount and TotalCount if a loosey check is used to set as completed</remarks>
|
||||
public static bool IsSeriesCompleted(Series series, List<Chapter> chapters, ExternalSeriesDetailDto externalMetadata, int maxVolumes)
|
||||
{
|
||||
// A series is completed if exactly the amount is found
|
||||
if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If volumes are collected, check if we reach the required volumes by including specials, and decimal volumes
|
||||
//
|
||||
// TODO BUG: If the series has specials, that are not included in the external count. But you do own them
|
||||
// This may mark the series as completed pre-maturely
|
||||
// Note: I've currently opted to keep this an equals to prevent the above bug from happening
|
||||
// We *could* change this to >= in the future in case this is reported by users
|
||||
// If we do; test IsSeriesCompleted_Volumes_TooManySpecials needs to be updated
|
||||
if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == series.Volumes.Count)
|
||||
{
|
||||
series.Metadata.MaxCount = series.Volumes.Count;
|
||||
series.Metadata.TotalCount = series.Volumes.Count;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Note: If Kavita has specials, we should be lenient and ignore for the volume check
|
||||
var volumeModifier = series.Volumes.Any(v => v.Name == Parser.SpecialVolume) ? 1 : 0;
|
||||
var modifiedMinVolumeCount = series.Volumes.Count - volumeModifier;
|
||||
if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == modifiedMinVolumeCount)
|
||||
{
|
||||
series.Metadata.MaxCount = modifiedMinVolumeCount;
|
||||
series.Metadata.TotalCount = modifiedMinVolumeCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no volumes are collected, the series is completed if we reach or exceed the external chapters
|
||||
if (maxVolumes == Parser.DefaultChapterNumber && series.Metadata.MaxCount >= externalMetadata.Chapters)
|
||||
{
|
||||
series.Metadata.TotalCount = series.Metadata.MaxCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no volumes are collected, the series is complete if we reach or exceed the external chapters while including
|
||||
// prologues, and extra chapters
|
||||
if (maxVolumes == Parser.DefaultChapterNumber && chapters.Count >= externalMetadata.Chapters)
|
||||
{
|
||||
series.Metadata.TotalCount = chapters.Count;
|
||||
series.Metadata.MaxCount = chapters.Count;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Dictionary<MetadataFieldType, List<string>> ApplyFieldMappings(IEnumerable<string> values, MetadataFieldType sourceType, List<MetadataFieldMappingDto> mappings)
|
||||
{
|
||||
var result = new Dictionary<MetadataFieldType, List<string>>();
|
||||
|
|
|
|||
|
|
@ -215,9 +215,9 @@ public class TaskScheduler : ITaskScheduler
|
|||
RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false),
|
||||
LicenseService.Cron, RecurringJobOptions);
|
||||
|
||||
// KavitaPlus Scrobbling (every hour)
|
||||
// KavitaPlus Scrobbling (every hour) - randomise minutes to spread requests out for K+
|
||||
RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, () => _scrobblingService.ProcessUpdatesSinceLastSync(),
|
||||
"0 */1 * * *", RecurringJobOptions);
|
||||
Cron.Hourly(Rnd.Next(0, 60)), RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, () => _scrobblingService.ClearProcessedEvents(),
|
||||
Cron.Daily, RecurringJobOptions);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue