mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Polish for Release (#3714)
This commit is contained in:
parent
9d9938bce2
commit
c80d046fc7
@ -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});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
|
||||
using System;
|
||||
using API.DTOs.Account;
|
||||
|
||||
namespace API.DTOs;
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
3409
API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs
generated
Normal file
3409
API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
||||
|
@ -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>
|
||||
|
@ -624,7 +624,14 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -11,4 +11,6 @@ export interface User {
|
||||
apiKey: string;
|
||||
email: string;
|
||||
ageRestriction: AgeRestriction;
|
||||
hasRunScrobbleEventGeneration: boolean;
|
||||
scrobbleEventGenerationRan: string; // datetime
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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 = [
|
||||
|
@ -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>
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
||||
@if (showRemoveButton) {
|
||||
<ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -14,8 +14,6 @@
|
||||
|
||||
.non-virtualized-container {
|
||||
width: 100%;
|
||||
max-height: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.dropdown-toggle-split {
|
||||
|
@ -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') {
|
||||
|
Loading…
x
Reference in New Issue
Block a user