mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Release Bugfixes (#2470)
This commit is contained in:
parent
d796d06fd1
commit
bdcd3965fd
@ -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]
|
||||
|
14
API.Tests/Services/ScrobblingServiceTests.cs
Normal file
14
API.Tests/Services/ScrobblingServiceTests.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -32,7 +32,7 @@ $image-width: 160px;
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
width: 130px;
|
||||
width: 140px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -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": [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user