Polish for Release (#3714)

This commit is contained in:
Joe Milazzo 2025-04-08 17:25:37 -06:00 committed by GitHub
parent 9d9938bce2
commit c80d046fc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 3611 additions and 41 deletions

View File

@ -270,4 +270,15 @@ public class ScrobblingController : BaseApiController
await _unitOfWork.CommitAsync();
return Ok();
}
/// <summary>
/// Has the logged in user ran scrobble generation
/// </summary>
/// <returns></returns>
[HttpGet("has-ran-scrobble-gen")]
public async Task<ActionResult<bool>> HasRanScrobbleGen()
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
return Ok(user is {HasRunScrobbleEventGeneration: true});
}
}

View File

@ -1,4 +1,5 @@

using System;
using API.DTOs.Account;
namespace API.DTOs;

View File

@ -0,0 +1,55 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.6 - Manually check when a user triggers scrobble event generation
/// </summary>
public static class ManualMigrateScrobbleEventGen
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateScrobbleEventGen"))
{
return;
}
logger.LogCritical("Running ManualMigrateScrobbleEventGen migration - Please be patient, this may take some time. This is not an error");
var users = await context.Users
.Where(u => u.AniListAccessToken != null)
.ToListAsync();
foreach (var user in users)
{
if (await context.ScrobbleEvent.AnyAsync(se => se.AppUserId == user.Id))
{
user.HasRunScrobbleEventGeneration = true;
user.ScrobbleEventGenerationRan = DateTime.UtcNow;
context.AppUser.Update(user);
}
}
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateScrobbleEventGen",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateScrobbleEventGen migration - Completed. This is not an error");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ScrobbleGenerationDbCapture : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "HasRunScrobbleEventGeneration",
table: "AspNetUsers",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "ScrobbleEventGenerationRan",
table: "AspNetUsers",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HasRunScrobbleEventGeneration",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "ScrobbleEventGenerationRan",
table: "AspNetUsers");
}
}
}

View File

@ -85,6 +85,9 @@ namespace API.Data.Migrations
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("HasRunScrobbleEventGeneration")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastActive")
.HasColumnType("TEXT");
@ -124,6 +127,9 @@ namespace API.Data.Migrations
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<DateTime>("ScrobbleEventGenerationRan")
.HasColumnType("TEXT");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");

View File

@ -76,6 +76,16 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
/// </summary>
public string? MalAccessToken { get; set; }
/// <summary>
/// Has the user ran Scrobble Event Generation
/// </summary>
/// <remarks>Only applicable for Kavita+ and when a Token is present</remarks>
public bool HasRunScrobbleEventGeneration { get; set; }
/// <summary>
/// The timestamp of when Scrobble Event Generation ran (Utc)
/// </summary>
/// <remarks>Kavita+ only</remarks>
public DateTime ScrobbleEventGenerationRan { get; set; }
/// <summary>

View File

@ -624,8 +624,15 @@ public class ScrobblingService : IScrobblingService
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null || string.IsNullOrEmpty(user.AniListAccessToken)) return;
if (user.HasRunScrobbleEventGeneration)
{
_logger.LogWarning("User {UserName} has already run scrobble event generation, Kavita will not generate more events", user.UserName);
return;
}
}
var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync())
.ToDictionary(lib => lib.Id, lib => lib.AllowScrobbling);
@ -667,6 +674,14 @@ public class ScrobblingService : IScrobblingService
if (series.PagesRead <= 0) continue; // Since we only scrobble when things are higher, we can
await ScrobbleReadingUpdate(uId, series.Id);
}
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(uId);
if (user != null)
{
user.HasRunScrobbleEventGeneration = true;
user.ScrobbleEventGenerationRan = DateTime.UtcNow;
await _unitOfWork.CommitAsync();
}
}
}

View File

@ -536,7 +536,7 @@ public class CoverDbService : ICoverDbService
if (!string.IsNullOrEmpty(filePath))
{
// Additional check to see if downloaded image is similar and we have a higher resolution
if (chooseBetterImage)
if (chooseBetterImage && !string.IsNullOrEmpty(series.CoverImage))
{
try
{

View File

@ -287,6 +287,7 @@ public class Startup
// v0.8.6
await ManualMigrateScrobbleSpecials.Migrate(dataContext, logger);
await ManualMigrateScrobbleEventGen.Migrate(dataContext, logger);
// Update the version in the DB after all migrations are run
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);

View File

@ -1,16 +1,16 @@
import { AgeRestriction } from '../metadata/age-restriction';
import { Library } from '../library/library';
import {AgeRestriction} from '../metadata/age-restriction';
import {Library} from '../library/library';
export interface Member {
id: number;
username: string;
email: string;
lastActive: string; // datetime
lastActiveUtc: string; // datetime
created: string; // datetime
createdUtc: string; // datetime
roles: string[];
libraries: Library[];
ageRestriction: AgeRestriction;
isPending: boolean;
id: number;
username: string;
email: string;
lastActive: string; // datetime
lastActiveUtc: string; // datetime
created: string; // datetime
createdUtc: string; // datetime
roles: string[];
libraries: Library[];
ageRestriction: AgeRestriction;
isPending: boolean;
}

View File

@ -1,14 +1,16 @@
import { AgeRestriction } from './metadata/age-restriction';
import { Preferences } from './preferences/preferences';
import {AgeRestriction} from './metadata/age-restriction';
import {Preferences} from './preferences/preferences';
// This interface is only used for login and storing/retrieving JWT from local storage
export interface User {
username: string;
token: string;
refreshToken: string;
roles: string[];
preferences: Preferences;
apiKey: string;
email: string;
ageRestriction: AgeRestriction;
username: string;
token: string;
refreshToken: string;
roles: string[];
preferences: Preferences;
apiKey: string;
email: string;
ageRestriction: AgeRestriction;
hasRunScrobbleEventGeneration: boolean;
scrobbleEventGenerationRan: string; // datetime
}

View File

@ -1,8 +1,8 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import {HttpClient, HttpParams} from '@angular/common/http';
import {Injectable} from '@angular/core';
import { map } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
import { TextResonse } from '../_types/text-response';
import {map} from 'rxjs/operators';
import {environment} from 'src/environments/environment';
import {TextResonse} from '../_types/text-response';
import {ScrobbleError} from "../_models/scrobbling/scrobble-error";
import {ScrobbleEvent} from "../_models/scrobbling/scrobble-event";
import {ScrobbleHold} from "../_models/scrobbling/scrobble-hold";
@ -56,6 +56,11 @@ export class ScrobblingService {
return this.httpClient.get<{username: string, accessToken: string}>(this.baseUrl + 'scrobbling/mal-token');
}
hasRunScrobbleGen() {
return this.httpClient.get(this.baseUrl + 'scrobbling/has-ran-scrobble-gen ', TextResonse).pipe(map(r => r === 'true'));
}
getScrobbleErrors() {
return this.httpClient.get<Array<ScrobbleError>>(this.baseUrl + 'scrobbling/scrobble-errors');
}

View File

@ -30,8 +30,8 @@ export class VersionService implements OnDestroy{
// Check intervals
private readonly VERSION_CHECK_INTERVAL = 30 * 60 * 1000; // 30 minutes
private readonly OUT_OF_DATE_CHECK_INTERVAL = this.VERSION_CHECK_INTERVAL; // 2 * 60 * 60 * 1000; // 2 hours
private readonly OUT_Of_BAND_AMOUNT = 2; // How many releases before we show "You're X releases out of date"
private readonly OUT_OF_DATE_CHECK_INTERVAL = 2 * 60 * 60 * 1000; // 2 hours
private readonly OUT_Of_BAND_AMOUNT = 3; // How many releases before we show "You're X releases out of date"
// Routes where version update modals should not be shown
private readonly EXCLUDED_ROUTES = [

View File

@ -1,14 +1,16 @@
<ng-container *transloco="let t; read:'user-scrobble-history'">
@let currentUser = accountService.currentUser$ | async;
<div class="position-relative">
<button class="btn btn-outline-primary position-absolute custom-position" [disabled]="events.length > 0" (click)="generateScrobbleEvents()" [title]="t('generate-scrobble-events')">
<button class="btn btn-outline-primary position-absolute custom-position" [disabled]="hasRunScrobbleGen" (click)="generateScrobbleEvents()" [title]="t('generate-scrobble-events')">
<i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden ms-1">{{t('generate-scrobble-events')}}</span>
</button>
</div>
@if (tokenExpired) {
<p class="alert alert-warning">{{t('token-expired')}}</p>
} @else if (!(accountService.currentUser$ | async)!.preferences.aniListScrobblingEnabled) {
} @else if (!currentUser!.preferences.aniListScrobblingEnabled) {
<p class="alert alert-warning">{{t('scrobbling-disabled')}}</p>
}

View File

@ -29,8 +29,8 @@ export interface DataTablePage {
@Component({
selector: 'app-user-scrobble-history',
imports: [ScrobbleEventTypePipe, ReactiveFormsModule, TranslocoModule,
DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, AsyncPipe],
imports: [ScrobbleEventTypePipe, ReactiveFormsModule, TranslocoModule,
DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, AsyncPipe],
templateUrl: './user-scrobble-history.component.html',
styleUrls: ['./user-scrobble-history.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@ -66,12 +66,18 @@ export class UserScrobbleHistoryComponent implements OnInit {
column: 'lastModifiedUtc',
direction: 'desc'
};
hasRunScrobbleGen: boolean = false;
ngOnInit() {
this.pageInfo.pageNumber = 0;
this.cdRef.markForCheck();
this.scrobblingService.hasRunScrobbleGen().subscribe(res => {
this.hasRunScrobbleGen = res;
this.cdRef.markForCheck();
})
this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => {
this.tokenExpired = hasExpired;
this.cdRef.markForCheck();

View File

@ -7,7 +7,11 @@
<div class="d-flex list-container">
<ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: true }"></ng-container>
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
<ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container>
@if (showRemoveButton) {
<ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container>
}
</div>
</div>
}

View File

@ -233,9 +233,13 @@
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
[disabled]="!(formGroup.get('edit')?.value || false)" [showRemoveButton]="formGroup.get('edit')?.value || false">
<ng-template #draggableItem let-item let-position="idx">
<app-reading-list-item [ngClass]="{'content-container': items.length < 100, 'non-virtualized-container': items.length >= 100}" [item]="item" [position]="position" [libraryTypes]="libraryTypes"
[promoted]="item.promoted" (read)="readChapter($event)" (remove)="itemRemoved($event, position)" [showRead]="!(formGroup.get('edit')?.value || false)"></app-reading-list-item>
<app-reading-list-item [ngClass]="{'content-container': items.length < 100, 'non-virtualized-container': items.length >= 100}" [item]="item"
[position]="position" [libraryTypes]="libraryTypes"
[promoted]="item.promoted" (read)="readChapter($event)"
(remove)="itemRemoved($event, position)"
[showRemove]="false"/>
</ng-template>
</app-draggable-ordered-list>
</div>

View File

@ -14,8 +14,6 @@
.non-virtualized-container {
width: 100%;
max-height: 140px;
height: 140px;
}
.dropdown-toggle-split {

View File

@ -1,5 +1,5 @@
<ng-container *transloco="let t; read: 'reading-list-item'">
<div class="d-flex flex-row g-0 mb-2 reading-list-item">
<div class="d-flex flex-row g-0reading-list-item">
<div class="d-none d-md-block pe-2">
<app-image width="106px" [styles]="{'max-height': '125px'}" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
@if (item.pagesRead === 0 && item.pagesTotal > 0) {
@ -42,7 +42,7 @@
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
</div>
<app-read-more [text]="item.summary || ''"></app-read-more>
<app-read-more [text]="item.summary || ''" [showToggle]="false" [maxLength]="500"></app-read-more>
@if (item.releaseDate !== '0001-01-01T00:00:00') {