Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release Testing Day 1 #1491

Merged
merged 8 commits into from
Aug 29, 2022
12 changes: 10 additions & 2 deletions API/Controllers/LibraryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
using API.Services.Tasks.Scanner;
using API.SignalR;
using AutoMapper;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using TaskScheduler = API.Services.TaskScheduler;

namespace API.Controllers
{
Expand Down Expand Up @@ -133,7 +135,7 @@ public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForU
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
if (user == null) return BadRequest("Could not validate user");

var libraryString = String.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
var libraryString = string.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
_logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString);

var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
Expand Down Expand Up @@ -242,10 +244,16 @@ public async Task<ActionResult<bool>> DeleteLibrary(int libraryId)
var chapterIds =
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds);


try
{
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
{
// TODO: Figure out how to cancel a job
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
return BadRequest(
"You cannot delete a library while a scan is in progress. Please wait for scan to continue then try to delete");
}
_unitOfWork.LibraryRepository.Delete(library);
await _unitOfWork.CommitAsync();

Expand Down
1 change: 1 addition & 0 deletions API/Controllers/ReaderController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using API.Entities.Enums;
using API.Extensions;
using API.Services;
using API.Services.Tasks;
using Hangfire;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
Expand Down
8 changes: 7 additions & 1 deletion API/DTOs/Reader/BookmarkDto.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
namespace API.DTOs.Reader
using System.ComponentModel.DataAnnotations;

namespace API.DTOs.Reader
{
public class BookmarkDto
{
public int Id { get; set; }
[Required]
public int Page { get; set; }
[Required]
public int VolumeId { get; set; }
[Required]
public int SeriesId { get; set; }
[Required]
public int ChapterId { get; set; }
}
}
1 change: 1 addition & 0 deletions API/Services/MetadataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public interface IMetadataService

public class MetadataService : IMetadataService
{
public const string Name = "MetadataService";
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<MetadataService> _logger;
private readonly IEventHub _eventHub;
Expand Down
20 changes: 15 additions & 5 deletions API/Services/TaskScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public class TaskScheduler : ITaskScheduler
public const string ScanQueue = "scan";
public const string DefaultQueue = "default";

public static readonly IList<string> ScanTasks = new List<string>()
{"ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"};

private static readonly Random Rnd = new Random();


Expand Down Expand Up @@ -172,7 +175,7 @@ public void ScanFolder(string folderPath)

public void ScanLibraries()
{
if (RunningAnyTasksByMethod(new List<string>() {"ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"}, ScanQueue))
if (RunningAnyTasksByMethod(ScanTasks, ScanQueue))
{
_logger.LogInformation("A Scan is already running, rescheduling ScanLibraries in 3 hours");
BackgroundJob.Schedule(() => ScanLibraries(), TimeSpan.FromHours(3));
Expand All @@ -191,7 +194,7 @@ public void ScanLibrary(int libraryId, bool force = false)
_logger.LogInformation("A duplicate request to scan library for library occured. Skipping");
return;
}
if (RunningAnyTasksByMethod(new List<string>() {"ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"}, ScanQueue))
if (RunningAnyTasksByMethod(ScanTasks, ScanQueue))
{
_logger.LogInformation("A Scan is already running, rescheduling ScanLibrary in 3 hours");
BackgroundJob.Schedule(() => ScanLibrary(libraryId, force), TimeSpan.FromHours(3));
Expand All @@ -211,7 +214,7 @@ public void CleanupChapters(int[] chapterIds)

public void RefreshMetadata(int libraryId, bool forceUpdate = true)
{
var alreadyEnqueued = HasAlreadyEnqueuedTask("MetadataService", "GenerateCoversForLibrary",
var alreadyEnqueued = HasAlreadyEnqueuedTask(MetadataService.Name, "GenerateCoversForLibrary",
new object[] {libraryId, true}) ||
HasAlreadyEnqueuedTask("MetadataService", "GenerateCoversForLibrary",
new object[] {libraryId, false});
Expand All @@ -227,7 +230,7 @@ public void RefreshMetadata(int libraryId, bool forceUpdate = true)

public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false)
{
if (HasAlreadyEnqueuedTask("MetadataService","GenerateCoversForSeries", new object[] {libraryId, seriesId, forceUpdate}))
if (HasAlreadyEnqueuedTask(MetadataService.Name,"GenerateCoversForSeries", new object[] {libraryId, seriesId, forceUpdate}))
{
_logger.LogInformation("A duplicate request to refresh metadata for library occured. Skipping");
return;
Expand All @@ -244,7 +247,7 @@ public void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false)
_logger.LogInformation("A duplicate request to scan series occured. Skipping");
return;
}
if (RunningAnyTasksByMethod(new List<string>() {ScannerService.Name, "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"}, ScanQueue))
if (RunningAnyTasksByMethod(ScanTasks, ScanQueue))
{
_logger.LogInformation("A Scan is already running, rescheduling ScanSeries in 10 minutes");
BackgroundJob.Schedule(() => ScanSeries(libraryId, seriesId, forceUpdate), TimeSpan.FromMinutes(10));
Expand Down Expand Up @@ -282,6 +285,13 @@ public async Task CheckForUpdate()
await _versionUpdaterService.PushUpdate(update);
}

public static bool HasScanTaskRunningForLibrary(int libraryId)
{
return
HasAlreadyEnqueuedTask("ScannerService", "ScanLibrary", new object[] {libraryId, true}, ScanQueue) ||
HasAlreadyEnqueuedTask("ScannerService", "ScanLibrary", new object[] {libraryId, false}, ScanQueue);
}

/// <summary>
/// Checks if this same invocation is already enqueued
/// </summary>
Expand Down
22 changes: 21 additions & 1 deletion API/Services/Tasks/CleanupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public interface ICleanupService
Task DeleteChapterCoverImages();
Task DeleteTagCoverImages();
Task CleanupBackups();
void CleanupTemp();
}
/// <summary>
/// Cleans up after operations on reoccurring basis
Expand Down Expand Up @@ -127,16 +128,18 @@ public async Task DeleteReadingListCoverImages()
}

/// <summary>
/// Removes all files and directories in the cache directory
/// Removes all files and directories in the cache and temp directory
/// </summary>
public void CleanupCacheDirectory()
{
_logger.LogInformation("Performing cleanup of Cache directory");
_directoryService.ExistOrCreate(_directoryService.CacheDirectory);
_directoryService.ExistOrCreate(_directoryService.TempDirectory);

try
{
_directoryService.ClearDirectory(_directoryService.CacheDirectory);
_directoryService.ClearDirectory(_directoryService.TempDirectory);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -175,5 +178,22 @@ public async Task CleanupBackups()
}
_logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now);
}

public void CleanupTemp()
{
_logger.LogInformation("Performing cleanup of Temp directory");
_directoryService.ExistOrCreate(_directoryService.TempDirectory);

try
{
_directoryService.ClearDirectory(_directoryService.TempDirectory);
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup");
}

_logger.LogInformation("Temp directory purged");
}
}
}
11 changes: 9 additions & 2 deletions API/Services/Tasks/Scanner/ParseScannedFiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,19 @@ await ProcessFiles(folderPath, isLibraryScan, seriesPaths, async (files, folder)
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", libraryName, ProgressEventType.Ended));
}

/// <summary>
/// Checks against all folder paths on file if the last scanned is >= the directory's last write down to the second
/// </summary>
/// <param name="seriesPaths"></param>
/// <param name="normalizedFolder"></param>
/// <param name="forceCheck"></param>
/// <returns></returns>
private bool HasSeriesFolderNotChangedSinceLastScan(IDictionary<string, IList<SeriesModified>> seriesPaths, string normalizedFolder, bool forceCheck = false)
{
if (forceCheck) return false;

return seriesPaths.ContainsKey(normalizedFolder) && seriesPaths[normalizedFolder].All(f => f.LastScanned.Truncate(TimeSpan.TicksPerMinute) >=
_directoryService.GetLastWriteTime(normalizedFolder).Truncate(TimeSpan.TicksPerMinute));
return seriesPaths.ContainsKey(normalizedFolder) && seriesPaths[normalizedFolder].All(f => f.LastScanned.Truncate(TimeSpan.TicksPerSecond) >=
_directoryService.GetLastWriteTime(normalizedFolder).Truncate(TimeSpan.TicksPerSecond));
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions API/Services/Tasks/Scanner/ProcessSeries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,13 @@ private static void UpdateSeriesMetadata(Series series, LibraryType libraryType)
}
}

var genres = chapters.SelectMany(c => c.Genres).ToList();
GenreHelper.KeepOnlySameGenreBetweenLists(series.Metadata.Genres.ToList(), genres, genre =>
{
if (series.Metadata.GenresLocked) return;
series.Metadata.Genres.Remove(genre);
});

// NOTE: The issue here is that people is just from chapter, but series metadata might already have some people on it
// I might be able to filter out people that are in locked fields?
var people = chapters.SelectMany(c => c.People).ToList();
Expand Down
15 changes: 8 additions & 7 deletions API/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,14 @@ await MigrateBookmarks.Migrate(directoryService, unitOfWork,

app.Use(async (context, next) =>
{
context.Response.GetTypedHeaders().CacheControl =
new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
{
Public = false,
MaxAge = TimeSpan.FromSeconds(10),
};
context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] =
// Note: I removed this as I caught Chrome caching api responses when it shouldn't have
// context.Response.GetTypedHeaders().CacheControl =
// new CacheControlHeaderValue()
// {
// Public = false,
// MaxAge = TimeSpan.FromSeconds(10),
// };
context.Response.Headers[HeaderNames.Vary] =
new[] { "Accept-Encoding" };

// Don't let the site be iframed outside the same origin (clickjacking)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<form>
<div class="row g-0 mb-3" *ngFor="let relation of relations; let idx = index; let isLast = last;">
<div class="col-sm-12 col-md-7">
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings" id="relation--{{idx}}">
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings" id="relation--{{idx}}" [focus]="focusTypeahead">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}} ({{libraryNames[item.libraryId]}})
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { FormControl } from '@angular/forms';
import { map, Subject, Observable, of, firstValueFrom, takeUntil, ReplaySubject } from 'rxjs';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings';
Expand All @@ -13,7 +13,7 @@ import { SeriesService } from 'src/app/_services/series.service';
interface RelationControl {
series: {id: number, name: string} | undefined; // Will add type as well
typeaheadSettings: TypeaheadSettings<SearchResult>;
formControl: UntypedFormControl;
formControl: FormControl;
}

@Component({
Expand All @@ -37,6 +37,8 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
seriesSettings: TypeaheadSettings<SearchResult> = new TypeaheadSettings();
libraryNames: {[key:number]: string} = {};

focusTypeahead = new EventEmitter();

get RelationKind() {
return RelationKind;
}
Expand Down Expand Up @@ -79,9 +81,9 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
}

setupRelationRows(relations: Array<Series>, kind: RelationKind) {
relations.map(async item => {
const settings = await firstValueFrom(this.createSeriesTypeahead(item, kind));
const form = new UntypedFormControl(kind, []);
relations.map(async (item, indx) => {
const settings = await firstValueFrom(this.createSeriesTypeahead(item, kind, indx));
const form = new FormControl(kind, []);
if (kind === RelationKind.Parent) {
form.disable();
}
Expand All @@ -93,15 +95,13 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
}

async addNewRelation() {
this.relations.push({series: undefined, formControl: new UntypedFormControl(RelationKind.Adaptation, []), typeaheadSettings: await firstValueFrom(this.createSeriesTypeahead(undefined, RelationKind.Adaptation))});
this.relations.push({series: undefined, formControl: new FormControl(RelationKind.Adaptation, []), typeaheadSettings: await firstValueFrom(this.createSeriesTypeahead(undefined, RelationKind.Adaptation, this.relations.length))});
this.cdRef.markForCheck();

// Focus on the new typeahead
setTimeout(() => {
const typeahead = document.querySelector(`#relation--${this.relations.length - 1} .typeahead-input input`) as HTMLInputElement;
if (typeahead) typeahead.focus();
this.focusTypeahead.emit(`relation--${this.relations.length - 1}`);
}, 10);

}

removeRelation(index: number) {
Expand All @@ -120,11 +120,11 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
}

createSeriesTypeahead(series: Series | undefined, relationship: RelationKind): Observable<TypeaheadSettings<SearchResult>> {
createSeriesTypeahead(series: Series | undefined, relationship: RelationKind, index: number): Observable<TypeaheadSettings<SearchResult>> {
const seriesSettings = new TypeaheadSettings<SearchResult>();
seriesSettings.minCharacters = 0;
seriesSettings.multiple = false;
seriesSettings.id = 'format';
seriesSettings.id = 'relation--' + index;
seriesSettings.unique = true;
seriesSettings.addIfNonExisting = false;
seriesSettings.fetchFn = (searchFilter: string) => this.libraryService.search(searchFilter).pipe(
Expand Down Expand Up @@ -165,7 +165,7 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
const alternativeVersions = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.AlternativeVersion && item.series !== undefined).map(item => item.series!.id);
const doujinshis = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Doujinshi && item.series !== undefined).map(item => item.series!.id);

// TODO: We can actually emit this onto an observable and in main parent, use mergeMap into the forkJoin
// NOTE: We can actually emit this onto an observable and in main parent, use mergeMap into the forkJoin
this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis).subscribe(() => {});

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ export class ReadingListItemComponent implements OnInit {
}

if (item.seriesFormat === MangaFormat.EPUB) {
this.title = 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber);
const specialTitle = this.utilityService.cleanSpecialTitle(item.chapterNumber);
if (specialTitle === '0') {
this.title = 'Volume ' + this.utilityService.cleanSpecialTitle(item.volumeNumber);
} else {
this.title = 'Volume ' + specialTitle;
}
}

let chapterNum = item.chapterNumber;
Expand Down
3 changes: 3 additions & 0 deletions UI/Web/src/app/series-detail/series-detail.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,9 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
if (this.relations.length > 0) {
this.hasRelations = true;
this.changeDetectionRef.markForCheck();
} else {
this.hasRelations = false;
this.changeDetectionRef.markForCheck();
}
});

Expand Down
11 changes: 11 additions & 0 deletions UI/Web/src/app/typeahead/typeahead.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
* If disabled, a user will not be able to interact with the typeahead
*/
@Input() disabled: boolean = false;
/**
* When triggered, will focus the input if the passed string matches the id
*/
@Input() focus: EventEmitter<string> | undefined;
@Output() selectedData = new EventEmitter<any[] | any>();
@Output() newItemAdded = new EventEmitter<any[] | any>();
@Output() onUnlock = new EventEmitter<void>();
Expand Down Expand Up @@ -203,6 +207,13 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
this.init();
});

if (this.focus) {
this.focus.pipe(takeUntil(this.onDestroy)).subscribe((id: string) => {
if (this.settings.id !== id) return;
this.onInputFocus();
});
}

this.init();
}

Expand Down