Release Bugfixes (#2470)

This commit is contained in:
Joe Milazzo 2023-12-03 11:54:57 -06:00 committed by GitHub
parent d796d06fd1
commit bdcd3965fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 194 additions and 25 deletions

View File

@ -514,6 +514,72 @@ public class CleanupServiceTests : AbstractDbTest
}
#endregion
#region EnsureChapterProgressIsCapped
[Fact]
public async Task EnsureChapterProgressIsCapped_ShouldNormalizeProgress()
{
await ResetDb();
var s = new SeriesBuilder("Test CleanupWantToRead_ShouldRemoveFullyReadSeries")
.WithMetadata(new SeriesMetadataBuilder().WithPublicationStatus(PublicationStatus.Completed).Build())
.Build();
s.Library = new LibraryBuilder("Test LIb").Build();
var c = new ChapterBuilder("1").WithPages(2).Build();
c.UserProgress = new List<AppUserProgress>();
s.Volumes = new List<Volume>()
{
new VolumeBuilder("0").WithChapter(c).Build()
};
_context.Series.Add(s);
var user = new AppUser()
{
UserName = "EnsureChapterProgressIsCapped",
Progresses = new List<AppUserProgress>()
};
_context.AppUser.Add(user);
await _unitOfWork.CommitAsync();
await _readerService.MarkChaptersAsRead(user, s.Id, new List<Chapter>() {c});
await _unitOfWork.CommitAsync();
var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id);
await _unitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter);
Assert.NotNull(chapter);
Assert.Equal(2, chapter.PagesRead);
// Update chapter to have 1 page
c.Pages = 1;
_unitOfWork.ChapterRepository.Update(c);
await _unitOfWork.CommitAsync();
chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id);
await _unitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter);
Assert.NotNull(chapter);
Assert.Equal(2, chapter.PagesRead);
Assert.Equal(1, chapter.Pages);
var cleanupService = new CleanupService(Substitute.For<ILogger<CleanupService>>(), _unitOfWork,
Substitute.For<IEventHub>(),
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
await cleanupService.EnsureChapterProgressIsCapped();
chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id);
await _unitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter);
Assert.NotNull(chapter);
Assert.Equal(1, chapter.PagesRead);
_context.AppUser.Remove(user);
await _unitOfWork.CommitAsync();
}
#endregion
// #region CleanupBookmarks
//
// [Fact]

View File

@ -0,0 +1,14 @@
using API.Services.Plus;
using Xunit;
namespace API.Tests.Services;
public class ScrobblingServiceTests
{
[Theory]
[InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)]
public void CanParseWeblink(string link, long expectedId)
{
Assert.Equal(ScrobblingService.ExtractId<long>(link, ScrobblingService.AniListWeblinkWebsite), expectedId);
}
}

View File

@ -425,7 +425,7 @@ public class LibraryController : BaseApiController
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto dto)
{
var userId = User.GetUserId();
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders | LibraryIncludes.FileTypes);
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns);
if (library == null) return BadRequest(await _localizationService.Translate(userId, "library-doesnt-exist"));
var newName = dto.Name.Trim();
@ -453,8 +453,8 @@ public class LibraryController : BaseApiController
.ToList();
library.LibraryExcludePatterns = dto.ExcludePatterns
.Select(t => new LibraryExcludePattern() {Pattern = t, LibraryId = library.Id})
.Distinct()
.Select(t => new LibraryExcludePattern() {Pattern = t, LibraryId = library.Id})
.ToList();
// Override Scrobbling for Comic libraries since there are no providers to scrobble to

View File

@ -270,6 +270,8 @@ public class ServerController : BaseApiController
await provider.FlushAsync();
provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings);
await provider.FlushAsync();
provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries);
await provider.FlushAsync();
return Ok();
}

View File

@ -34,6 +34,7 @@ public interface IAppUserProgressRepository
Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId);
Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId);
Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId);
Task UpdateAllProgressThatAreMoreThanChapterPages();
}
#nullable disable
public class AppUserProgressRepository : IAppUserProgressRepository
@ -198,6 +199,36 @@ public class AppUserProgressRepository : IAppUserProgressRepository
return list.Count == 0 ? null : list.DefaultIfEmpty().Min();
}
public async Task UpdateAllProgressThatAreMoreThanChapterPages()
{
var updates = _context.AppUserProgresses
.Join(_context.Chapter,
progress => progress.ChapterId,
chapter => chapter.Id,
(progress, chapter) => new
{
Progress = progress,
Chapter = chapter
})
.Where(joinResult => joinResult.Progress.PagesRead > joinResult.Chapter.Pages)
.Select(result => new
{
ProgressId = result.Progress.Id,
NewPagesRead = Math.Min(result.Progress.PagesRead, result.Chapter.Pages)
})
.AsEnumerable();
foreach (var update in updates)
{
_context.AppUserProgresses
.Where(p => p.Id == update.ProgressId)
.ToList() // Execute the query to ensure exclusive lock
.ForEach(p => p.PagesRead = update.NewPagesRead);
}
await _context.SaveChangesAsync();
}
#nullable enable
public async Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId)
{

View File

@ -175,13 +175,13 @@ public class ScrobblingService : IScrobblingService
private async Task<string> GetTokenForProvider(int userId, ScrobbleProvider provider)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return null;
if (user == null) return string.Empty;
return provider switch
{
ScrobbleProvider.AniList => user.AniListAccessToken,
_ => string.Empty
};
} ?? string.Empty;
}
public async Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody)
@ -192,11 +192,9 @@ public class ScrobblingService : IScrobblingService
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
_logger.LogInformation("Processing Scrobbling review event for {UserId} on {SeriesName}", userId, series.Name);
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
if (await CheckIfCannotScrobble(userId, seriesId, series)) return;
if (string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 ||
reviewTitle.Length > 120 ||
reviewTitle.Length < 20))
if (IsAniListReviewValid(reviewTitle, reviewBody))
{
_logger.LogDebug(
"Rejecting Scrobble event for {Series}. Review is not long enough to meet requirements", series.Name);
@ -232,6 +230,13 @@ public class ScrobblingService : IScrobblingService
_logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {UserId} ", series.Name, userId);
}
private static bool IsAniListReviewValid(string reviewTitle, string reviewBody)
{
return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 ||
reviewTitle.Length > 120 ||
reviewTitle.Length < 20);
}
public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating)
{
if (!await _licenseService.HasActiveLicense()) return;
@ -240,7 +245,7 @@ public class ScrobblingService : IScrobblingService
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
_logger.LogInformation("Processing Scrobbling rating event for {UserId} on {SeriesName}", userId, series.Name);
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
if (await CheckIfCannotScrobble(userId, seriesId, series)) return;
var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id,
ScrobbleEventType.ScoreUpdated);
@ -279,7 +284,7 @@ public class ScrobblingService : IScrobblingService
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
_logger.LogInformation("Processing Scrobbling reading event for {UserId} on {SeriesName}", userId, series.Name);
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
if (await CheckIfCannotScrobble(userId, seriesId, series)) return;
var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id,
ScrobbleEventType.ChapterRead);
@ -334,11 +339,11 @@ public class ScrobblingService : IScrobblingService
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
_logger.LogInformation("Processing Scrobbling want-to-read event for {UserId} on {SeriesName}", userId, series.Name);
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
if (await CheckIfCannotScrobble(userId, seriesId, series)) return;
var existing = await _unitOfWork.ScrobbleRepository.Exists(userId, series.Id,
onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead);
if (existing) return;
if (existing) return; // BUG: If I take a series and add to remove from want to read, then add to want to read, Kavita rejects the second as a duplicate, when it's not
var evt = new ScrobbleEvent()
{
@ -355,7 +360,7 @@ public class ScrobblingService : IScrobblingService
_logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {UserId} ", series.Name, userId);
}
private async Task<bool> CheckIfCanScrobble(int userId, int seriesId, Series series)
private async Task<bool> CheckIfCannotScrobble(int userId, int seriesId, Series series)
{
if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId))
{
@ -616,7 +621,7 @@ public class ScrobblingService : IScrobblingService
await SetAndCheckRateLimit(userRateLimits, user, license.Value);
}
var totalProgress = readEvents.Count + addToWantToRead.Count + removeWantToRead.Count + ratingEvents.Count + decisions.Count + reviewEvents.Count;
var totalProgress = readEvents.Count + decisions.Count + ratingEvents.Count + decisions.Count + reviewEvents.Count;
_logger.LogInformation("Found {TotalEvents} Scrobble Events", totalProgress);
try
@ -693,6 +698,24 @@ public class ScrobblingService : IScrobblingService
LocalizedSeriesName = evt.Series.LocalizedName,
Year = evt.Series.Metadata.ReleaseYear
}));
// After decisions, we need to mark all the want to read and remove from want to read as completed
if (decisions.All(d => d.IsProcessed))
{
foreach (var scrobbleEvent in addToWantToRead)
{
scrobbleEvent.IsProcessed = true;
scrobbleEvent.ProcessDateUtc = DateTime.UtcNow;
_unitOfWork.ScrobbleRepository.Update(scrobbleEvent);
}
foreach (var scrobbleEvent in removeWantToRead)
{
scrobbleEvent.IsProcessed = true;
scrobbleEvent.ProcessDateUtc = DateTime.UtcNow;
_unitOfWork.ScrobbleRepository.Update(scrobbleEvent);
}
await _unitOfWork.CommitAsync();
}
}
catch (FlurlHttpException)
{

View File

@ -26,6 +26,7 @@ public interface ICleanupService
Task CleanupBackups();
Task CleanupLogs();
void CleanupTemp();
Task EnsureChapterProgressIsCapped();
/// <summary>
/// Responsible to remove Series from Want To Read when user's have fully read the series and the series has Publication Status of Completed or Cancelled.
/// </summary>
@ -89,6 +90,8 @@ public class CleanupService : ICleanupService
await DeleteReadingListCoverImages();
await SendProgress(0.8F, "Cleaning old logs");
await CleanupLogs();
await SendProgress(0.9F, "Cleaning progress events that exceed 100%");
await EnsureChapterProgressIsCapped();
await SendProgress(1F, "Cleanup finished");
_logger.LogInformation("Cleanup finished");
}
@ -243,6 +246,17 @@ public class CleanupService : ICleanupService
_logger.LogInformation("Temp directory purged");
}
/// <summary>
/// Ensures that each chapter's progress (pages read) is capped at the total pages. This can get out of sync when a chapter is replaced after being read with one with lower page count.
/// </summary>
/// <returns></returns>
public async Task EnsureChapterProgressIsCapped()
{
_logger.LogInformation("Cleaning up any progress rows that exceed chapter page count");
await _unitOfWork.AppUserProgressRepository.UpdateAllProgressThatAreMoreThanChapterPages();
_logger.LogInformation("Cleaning up any progress rows that exceed chapter page count - complete");
}
/// <summary>
/// This does not cleanup any Series that are not Completed or Cancelled
/// </summary>

View File

@ -84,9 +84,11 @@ export class BookLineOverlayComponent implements OnInit {
const selection = window.getSelection();
if (!event.target) return;
if ((!selection || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) {
event.preventDefault();
event.stopPropagation();
if ((selection === null || selection === undefined || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) {
if (this.selectedText !== '') {
event.preventDefault();
event.stopPropagation();
}
this.reset();
return;
}

View File

@ -1643,8 +1643,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.clickToPaginate) {
if (this.isCursorOverLeftPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD);
return;
} else if (this.isCursorOverRightPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)
return;
}
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy} from '@angular/core';
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
@ -9,6 +9,7 @@ import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common';
import { SplashContainerComponent } from '../splash-container/splash-container.component';
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {take} from "rxjs/operators";
@Component({
selector: 'app-confirm-email',
@ -18,7 +19,7 @@ import {translate, TranslocoDirective} from "@ngneat/transloco";
standalone: true,
imports: [SplashContainerComponent, NgIf, NgFor, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoDirective]
})
export class ConfirmEmailComponent {
export class ConfirmEmailComponent implements OnDestroy {
/**
* Email token used for validating
*/
@ -55,6 +56,14 @@ export class ConfirmEmailComponent {
this.cdRef.markForCheck();
}
ngOnDestroy() {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.navService.showSideNav();
}
});
}
isNullOrEmpty(v: string | null | undefined) {
return v == undefined || v === '' || v === null;
}

View File

@ -68,7 +68,12 @@ export class EditListComponent implements OnInit {
const tokenToRemove = tokens[index];
this.combinedItems = tokens.filter(t => t != tokenToRemove).join(',');
this.form.removeControl('link' + index, {emitEvent: true});
for (const [index, [key, value]] of Object.entries(Object.entries(this.form.controls))) {
if (key.startsWith('link') && this.form.get(key)?.value === tokenToRemove) {
this.form.removeControl('link' + index, {emitEvent: true});
}
}
this.emit();
this.cdRef.markForCheck();
}

View File

@ -115,9 +115,10 @@
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<span class="mb-2">{{t('exclude-patterns-tooltip')}}</span>
<a class="ms-1" href="https://wiki.kavitareader.com/en/guides/managing-your-files/scanner/excluding-files-folders" rel="noopener noreferrer" target="_blank">{{t('help')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
<app-edit-list [items]="excludePatterns" [label]="t('exclude-patterns-label')" (updateItems)="updateGlobs($event)"></app-edit-list>
<span >{{t('exclude-patterns-tooltip')}}<a class="ms-1" href="https://wiki.kavitareader.com/en/guides/managing-your-files/scanner/excluding-files-folders" rel="noopener noreferrer" target="_blank">{{t('help')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a></span>
<div class="mt-2">
<app-edit-list [items]="excludePatterns" [label]="t('exclude-patterns-label')" (updateItems)="updateGlobs($event)"></app-edit-list>
</div>
</ng-template>
</div>
</div>

View File

@ -32,7 +32,7 @@ $image-width: 160px;
.card-title {
font-size: 13px;
width: 130px;
width: 140px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.10.17"
"version": "0.7.10.20"
},
"servers": [
{