Scan Loop Fixes (#1459)

* Added Last Folder Scanned time to series info modal.

Tweaked the info event detail modal to have a primary and thus be auto-dismissable

* Added an error event when multiple series are found in processing a series.

* Fixed a bug where a series could get stuck with other series due to a bad select query.

Started adding the force flag hook for the UI and designing the confirm.

Confirm service now also has ability to hide the close button.

Updated error events and logging in the loop, to be more informative

* Fixed a bug where confirm service wasn't showing the proper body content.

* Hooked up force scan series

* refresh metadata now has force update

* Fixed up the messaging with the prompt on scan, hooked it up properly in the scan library to avoid the check if the whole library needs to even be scanned. Fixed a bug where NormalizedLocalizedName wasn't being calculated on new entities.

Started adding unit tests for this problematic repo method.

* Fixed a bug where we updated NormalizedLocalizedName before we set it.

* Send an info to the UI when series are spread between multiple library level folders.

* Added some logger output when there are no files found in a folder. Return early if there are no files found, so we can avoid some small loops of code.

* Fixed an issue where multiple series in a folder with localized series would cause unintended grouping. This is not supported and hence we will warn them and allow the bad grouping.

* Added a case where scan series fails due to the folder being removed. We will now log an error

* Normalize paths when finding the highest directory till root.

* Fixed an issue with Scan Series where changing a series' folder to a different path but the original series folder existed with another series in it, would cause the series to not be deleted.

* Fixed some bugs around specials causing a series merge issue on scan series.

* Removed a bug marker

* Cleaned up some of the scan loop and removed a test I don't need.

* Remove any prompts for force flow, it doesn't work well. Leave the API as is though.

* Fixed up a check for duplicate ScanLibrary calls
This commit is contained in:
Joseph Milazzo 2022-08-22 12:14:31 -05:00 committed by GitHub
parent 354be09c4c
commit 1c9544fc47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 367 additions and 222 deletions

View file

@ -48,6 +48,10 @@ export interface Series {
* DateTime representing last time a chapter was added to the Series
*/
lastChapterAdded: string;
/**
* DateTime representing last time the series folder was scanned
*/
lastFolderScanned: string;
/**
* Number of words in the series
*/

View file

@ -18,9 +18,9 @@ export enum Action {
*/
MarkAsUnread = 1,
/**
* Invoke a Scan Library
* Invoke a Scan on Series/Library
*/
ScanLibrary = 2,
Scan = 2,
/**
* Delete the entity
*/
@ -129,7 +129,7 @@ export class ActionFactoryService {
});
this.seriesActions.push({
action: Action.ScanLibrary,
action: Action.Scan,
title: 'Scan Series',
callback: this.dummyCallback,
requiresAdmin: true
@ -171,7 +171,7 @@ export class ActionFactoryService {
});
this.libraryActions.push({
action: Action.ScanLibrary,
action: Action.Scan,
title: 'Scan Library',
callback: this.dummyCallback,
requiresAdmin: true

View file

@ -52,11 +52,15 @@ export class ActionService implements OnDestroy {
* @param callback Optional callback to perform actions after API completes
* @returns
*/
scanLibrary(library: Partial<Library>, callback?: LibraryActionCallback) {
async scanLibrary(library: Partial<Library>, callback?: LibraryActionCallback) {
if (!library.hasOwnProperty('id') || library.id === undefined) {
return;
}
this.libraryService.scan(library?.id).pipe(take(1)).subscribe((res: any) => {
// Prompt user if we should do a force or not
const force = false; // await this.promptIfForce();
this.libraryService.scan(library.id, force).pipe(take(1)).subscribe((res: any) => {
this.toastr.info('Scan queued for ' + library.name);
if (callback) {
callback(library);
@ -83,7 +87,9 @@ export class ActionService implements OnDestroy {
return;
}
this.libraryService.refreshMetadata(library?.id).pipe(take(1)).subscribe((res: any) => {
const forceUpdate = true; //await this.promptIfForce();
this.libraryService.refreshMetadata(library?.id, forceUpdate).pipe(take(1)).subscribe((res: any) => {
this.toastr.info('Scan queued for ' + library.name);
if (callback) {
callback(library);
@ -152,7 +158,7 @@ export class ActionService implements OnDestroy {
* @param series Series, must have libraryId and name populated
* @param callback Optional callback to perform actions after API completes
*/
scanSeries(series: Series, callback?: SeriesActionCallback) {
async scanSeries(series: Series, callback?: SeriesActionCallback) {
this.seriesService.scan(series.libraryId, series.id).pipe(take(1)).subscribe((res: any) => {
this.toastr.info('Scan queued for ' + series.name);
if (callback) {
@ -545,4 +551,16 @@ export class ActionService implements OnDestroy {
}
});
}
private async promptIfForce(extraContent: string = '') {
// Prompt user if we should do a force or not
const config = this.confirmService.defaultConfirm;
config.header = 'Force Scan';
config.buttons = [
{text: 'Yes', type: 'secondary'},
{text: 'No', type: 'primary'},
];
const msg = 'Do you want to force this scan? This is will ignore optimizations that reduce processing and I/O. ' + extraContent;
return !await this.confirmService.confirm(msg, config); // Not because primary is the false state
}
}

View file

@ -76,16 +76,16 @@ export class LibraryService {
return this.httpClient.post(this.baseUrl + 'library/grant-access', {username, selectedLibraries});
}
scan(libraryId: number) {
return this.httpClient.post(this.baseUrl + 'library/scan?libraryId=' + libraryId, {});
scan(libraryId: number, force = false) {
return this.httpClient.post(this.baseUrl + 'library/scan?libraryId=' + libraryId + '&force=' + force, {});
}
analyze(libraryId: number) {
return this.httpClient.post(this.baseUrl + 'library/analyze?libraryId=' + libraryId, {});
}
refreshMetadata(libraryId: number) {
return this.httpClient.post(this.baseUrl + 'library/refresh-metadata?libraryId=' + libraryId, {});
refreshMetadata(libraryId: number, forceUpdate = false) {
return this.httpClient.post(this.baseUrl + 'library/refresh-metadata?libraryId=' + libraryId + '&force=' + forceUpdate, {});
}
create(model: {name: string, type: number, folders: string[]}) {

View file

@ -153,8 +153,8 @@ export class SeriesService {
return this.httpClient.post(this.baseUrl + 'series/refresh-metadata', {libraryId: series.libraryId, seriesId: series.id});
}
scan(libraryId: number, seriesId: number) {
return this.httpClient.post(this.baseUrl + 'series/scan', {libraryId: libraryId, seriesId: seriesId});
scan(libraryId: number, seriesId: number, force = false) {
return this.httpClient.post(this.baseUrl + 'series/scan', {libraryId: libraryId, seriesId: seriesId, forceUpdate: force});
}
analyzeFiles(libraryId: number, seriesId: number) {

View file

@ -344,9 +344,10 @@
<div class="col-md-6">Format: <app-tag-badge>{{series.format | mangaFormat}}</app-tag-badge></div>
</div>
<div class="row g-0 mb-2">
<div class="col-md-6" >Created: {{series.created | date:'shortDate'}}</div>
<div class="col-md-6">Created: {{series.created | date:'shortDate'}}</div>
<div class="col-md-6">Last Read: {{series.latestReadDate | date:'shortDate' | defaultDate}}</div>
<div class="col-md-6">Last Added To: {{series.lastChapterAdded | date:'shortDate' | defaultDate}}</div>
<div class="col-md-6">Last Added To: {{series.lastChapterAdded | date:'short' | defaultDate}}</div>
<div class="col-md-6">Last Scanned: {{series.lastFolderScanned | date:'short' | defaultDate}}</div>
<div class="col-md-6">Folder Path: {{series.folderPath | defaultValue}}</div>
</div>
<div class="row g-0 mb-2" *ngIf="metadata">

View file

@ -7,5 +7,5 @@
<button ngbDropdownItem *ngFor="let action of adminActions" (click)="performAction($event, action)">{{action.title}}</button>
</div>
</div>
<!-- TODO: If we are not on desktop, then let's open a bottom drawer instead-->
<!-- IDEA: If we are not on desktop, then let's open a bottom drawer instead-->
</ng-container>

View file

@ -82,7 +82,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
case(Action.MarkAsUnread):
this.markAsUnread(series);
break;
case(Action.ScanLibrary):
case(Action.Scan):
this.scanLibrary(series);
break;
case(Action.RefreshMetadata):

View file

@ -203,7 +203,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
lib = {id: this.libraryId, name: this.libraryName};
}
switch (action) {
case(Action.ScanLibrary):
case(Action.Scan):
this.actionService.scanLibrary(lib);
break;
case(Action.RefreshMetadata):

View file

@ -152,11 +152,15 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
async seeMore(event: ErrorEvent | InfoEvent) {
const config = new ConfirmConfig();
config.buttons = [
{text: 'Ok', type: 'secondary'},
];
if (event.name === EVENTS.Error) {
config.buttons = [{text: 'Dismiss', type: 'primary'}, ...config.buttons];
config.buttons = [
{text: 'Ok', type: 'secondary'},
{text: 'Dismiss', type: 'primary'}
];
} else {
config.buttons = [
{text: 'Ok', type: 'primary'},
];
}
config.header = event.title;
config.content = event.subTitle;

View file

@ -345,7 +345,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
this.loadSeries(series.id);
});
break;
case(Action.ScanLibrary):
case(Action.Scan):
this.actionService.scanSeries(series, () => {
this.actionInProgress = false;
this.changeDetectionRef.markForCheck();

View file

@ -3,5 +3,5 @@ export interface ConfirmButton {
/**
* Type for css class. ie) primary, secondary
*/
type: string;
type: 'secondary' | 'primary';
}

View file

@ -5,4 +5,8 @@ export class ConfirmConfig {
header: string = 'Confirm';
content: string = '';
buttons: Array<ConfirmButton> = [];
/**
* If the close button shouldn't be rendered
*/
disableEscape: boolean = false;
}

View file

@ -2,9 +2,7 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{config.header}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
</button>
<button type="button" class="btn-close" aria-label="Close" (click)="close()" *ngIf="!config.disableEscape"></button>
</div>
<div class="modal-body" style="overflow-x: auto" [innerHtml]="config.content | safeHtml">
</div>
@ -12,5 +10,4 @@
<div *ngFor="let btn of config.buttons">
<button type="button" class="btn btn-{{btn.type}}" (click)="clickButton(btn)">{{btn.text}}</button>
</div>
</div>

View file

@ -34,6 +34,9 @@ export class ConfirmService {
config = this.defaultConfirm;
config.content = content;
}
if (content !== undefined && content !== '' && config!.content === '') {
config!.content = content;
}
const modalRef = this.modalService.open(ConfirmDialogComponent);
modalRef.componentInstance.config = config;

View file

@ -78,7 +78,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
handleAction(action: Action, library: Library) {
switch (action) {
case(Action.ScanLibrary):
case(Action.Scan):
this.actionService.scanLibrary(library);
break;
case(Action.RefreshMetadata):