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:
parent
642b23ed61
commit
d1e4878345
32 changed files with 2586 additions and 57 deletions
|
@ -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', {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
8
UI/Web/src/app/admin/_models/media-error.ts
Normal file
8
UI/Web/src/app/admin/_models/media-error.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export interface KavitaMediaError {
|
||||
extension: string;
|
||||
filePath: string;
|
||||
comment: string;
|
||||
details: string;
|
||||
created: string;
|
||||
createdUtc: string;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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)">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue