Report Media Issues (#1964)

* Started working on a report problems implementation.

* Started code

* Added logging to book and archive service.

* Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point.

* Added basic implementation for media errors.

* MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan.

* Fixed unit tests

* Basic code in place to view and clear. Just UI Cleanup needed.

* Slight css upgrade

* Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working.

* Fixed unit tests

* Fixed unit tests for real
This commit is contained in:
Joe Milazzo 2023-05-07 12:14:39 -05:00 committed by GitHub
parent 642b23ed61
commit d1e4878345
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 2586 additions and 57 deletions

View file

@ -4,6 +4,7 @@ import { environment } from 'src/environments/environment';
import { ServerInfo } from '../admin/_models/server-info';
import { UpdateVersionEvent } from '../_models/events/update-version-event';
import { Job } from '../_models/job/job';
import { KavitaMediaError } from '../admin/_models/media-error';
@Injectable({
providedIn: 'root'
@ -61,4 +62,12 @@ export class ServerService {
convertCovers() {
return this.httpClient.post(this.baseUrl + 'server/convert-covers', {});
}
getMediaErrors() {
return this.httpClient.get<Array<KavitaMediaError>>(this.baseUrl + 'server/media-errors', {});
}
clearMediaAlerts() {
return this.httpClient.post(this.baseUrl + 'server/clear-media-alerts', {});
}
}

View file

@ -18,6 +18,7 @@ export interface SortEvent<T> {
'(click)': 'rotate()',
},
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class SortableHeader<T> {
@Input() sortable: SortColumn<T> = '';
@Input() direction: SortDirection = '';

View file

@ -0,0 +1,8 @@
export interface KavitaMediaError {
extension: string;
filePath: string;
comment: string;
details: string;
created: string;
createdUtc: string;
}

View file

@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdminRoutingModule } from './admin-routing.module';
import { DashboardComponent } from './dashboard/dashboard.component';
import { NgbDropdownModule, NgbNavModule, NgbTooltipModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { NgbAccordionModule, NgbDropdownModule, NgbNavModule, NgbTooltipModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { ManageLibraryComponent } from './manage-library/manage-library.component';
import { ManageUsersComponent } from './manage-users/manage-users.component';
import { SharedModule } from '../shared/shared.module';
@ -25,6 +25,7 @@ import { ManageTasksSettingsComponent } from './manage-tasks-settings/manage-tas
import { ManageLogsComponent } from './manage-logs/manage-logs.component';
import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
import { StatisticsModule } from '../statistics/statistics.module';
import { ManageAlertsComponent } from './manage-alerts/manage-alerts.component';
@ -47,6 +48,7 @@ import { StatisticsModule } from '../statistics/statistics.module';
ManageEmailSettingsComponent,
ManageTasksSettingsComponent,
ManageLogsComponent,
ManageAlertsComponent,
],
imports: [
CommonModule,
@ -57,6 +59,7 @@ import { StatisticsModule } from '../statistics/statistics.module';
NgbTooltipModule,
NgbTypeaheadModule, // Directory Picker
NgbDropdownModule,
NgbAccordionModule,
SharedModule,
PipeModule,
SidenavModule,

View file

@ -0,0 +1,39 @@
<p>This table contains issues found during scan or reading of your media. This list is non-managed. You can clear it at any time and use Library (Force) Scan to perform analysis.</p>
<button class="btn btn-primary mb-2" (click)="clear()">Clear Alerts</button>
<table class="table table-light table-hover table-sm table-hover">
<thead #header>
<tr>
<th scope="col"sortable="extension" (sort)="onSort($event)">
Extension
</th>
<th scope="col" sortable="filePath" (sort)="onSort($event)">
File
</th>
<th scope="col" sortable="comment" (sort)="onSort($event)">
Comment
</th>
<th scope="col" sortable="details" (sort)="onSort($event)">
Details
</th>
</tr>
</thead>
<tbody #container>
<tr *ngIf="isLoading"><td colspan="4" style="text-align: center;"><app-loading [loading]="isLoading"></app-loading></td></tr>
<tr *ngIf="data.length === 0 && !isLoading"><td colspan="4" style="text-align: center;">No issues</td></tr>
<tr *ngFor="let item of data; index as i">
<td>
{{item.extension}}
</td>
<td>
{{item.filePath}}
</td>
<td>
{{item.comment}}
</td>
<td>
{{item.details}}
</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,75 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, QueryList, ViewChildren, inject } from '@angular/core';
import { BehaviorSubject, Observable, Subject, combineLatest, filter, map, shareReplay, takeUntil } from 'rxjs';
import { SortEvent, SortableHeader, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { KavitaMediaError } from '../_models/media-error';
import { ServerService } from 'src/app/_services/server.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
@Component({
selector: 'app-manage-alerts',
templateUrl: './manage-alerts.component.html',
styleUrls: ['./manage-alerts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManageAlertsComponent implements OnInit {
@ViewChildren(SortableHeader<KavitaMediaError>) headers!: QueryList<SortableHeader<KavitaMediaError>>;
private readonly serverService = inject(ServerService);
private readonly messageHub = inject(MessageHubService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly onDestroy = new Subject<void>();
messageHubUpdate$ = this.messageHub.messages$.pipe(takeUntil(this.onDestroy), filter(m => m.event === EVENTS.ScanSeries), shareReplay());
currentSort = new BehaviorSubject<SortEvent<KavitaMediaError>>({column: 'extension', direction: 'asc'});
currentSort$: Observable<SortEvent<KavitaMediaError>> = this.currentSort.asObservable();
data: Array<KavitaMediaError> = [];
isLoading = true;
constructor() {}
ngOnInit(): void {
this.loadData();
this.messageHubUpdate$.subscribe(_ => this.loadData());
this.currentSort$.subscribe(sortConfig => {
this.data = (sortConfig.column) ? this.data.sort((a: KavitaMediaError, b: KavitaMediaError) => {
if (sortConfig.column === '') return 0;
const res = compare(a[sortConfig.column], b[sortConfig.column]);
return sortConfig.direction === 'asc' ? res : -res;
}) : this.data;
this.cdRef.markForCheck();
});
}
onSort(evt: any) {
//SortEvent<KavitaMediaError>
this.currentSort.next(evt);
// Must clear out headers here
this.headers.forEach((header) => {
if (header.sortable !== evt.column) {
header.direction = '';
}
});
}
loadData() {
this.isLoading = true;
this.cdRef.markForCheck();
this.serverService.getMediaErrors().subscribe(d => {
this.data = d;
this.isLoading = false;
console.log(this.data)
console.log(this.isLoading)
this.cdRef.detectChanges();
});
}
clear() {
this.serverService.clearMediaAlerts().subscribe(_ => this.loadData());
}
}

View file

@ -1,5 +1,5 @@
<div class="container-fluid">
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined" class="mb-2">
<div class="row g-0">
<p>WebP can drastically reduce space requirements for files. WebP is not supported on all browsers or versions. To learn if these settings are appropriate for your setup, visit <a href="https://caniuse.com/?search=webp" target="_blank" rel="noopener noreferrer">Can I Use</a>.</p>
@ -48,6 +48,14 @@
</div>
</form>
<!-- Accordion with Issues from Media anaysis -->
<ngb-accordion #a="ngbAccordion">
<ngb-panel>
<ng-template ngbPanelTitle>
Media Issues
</ng-template>
<ng-template ngbPanelContent>
<app-manage-alerts></app-manage-alerts>
</ng-template>
</ngb-panel>
</ngb-accordion>
</div>

View file

@ -1,28 +1,28 @@
<ng-container>
<ng-container *ngIf="items.length > 100; else dragList">
<div class="example-list list-group-flush">
<virtual-scroller #scroll [items]="items" [bufferAmount]="BufferAmount" [parentScroll]="parentScroll">
<div class="example-box" *ngFor="let item of scroll.viewPortItems; index as i; trackBy: trackByIdentity">
<div class="d-flex list-container">
<div class="me-3 align-middle">
<div style="padding-top: 40px">
<label for="reorder-{{i}}" class="form-label visually-hidden">Reorder</label>
<input *ngIf="accessibilityMode" id="reorder-{{i}}" class="form-control" type="number" inputmode="numeric" min="0" [max]="items.length - 1" [value]="i" style="width: 60px"
(focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
</div>
<div class="example-list list-group-flush">
<virtual-scroller #scroll [items]="items" [bufferAmount]="BufferAmount" [parentScroll]="parentScroll">
<div class="example-box" *ngFor="let item of scroll.viewPortItems; index as i; trackBy: trackByIdentity">
<div class="d-flex list-container">
<div class="me-3 align-middle">
<div style="padding-top: 40px">
<label for="reorder-{{i}}" class="form-label visually-hidden">Reorder</label>
<input *ngIf="accessibilityMode" id="reorder-{{i}}" class="form-control" type="number" inputmode="numeric" min="0" [max]="items.length - 1" [value]="i" style="width: 60px"
(focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
</div>
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton">
<i class="fa fa-times" aria-hidden="true"></i>
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">Remove item</span>
</button>
</div>
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton">
<i class="fa fa-times" aria-hidden="true"></i>
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">Remove item</span>
</button>
</div>
</virtual-scroller>
</div>
</div>
</virtual-scroller>
</div>
</ng-container>
<ng-template #dragList>
<div cdkDropList class="{{items.length > 0 ? 'example-list list-group-flush' : ''}}" (cdkDropListDropped)="drop($event)">