More Metadata Stuff (#3537)
This commit is contained in:
parent
8d3dcc637e
commit
53b13da0c9
34 changed files with 4123 additions and 129 deletions
16
UI/Web/src/app/_pipes/library-name.pipe.ts
Normal file
16
UI/Web/src/app/_pipes/library-name.pipe.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {inject, Pipe, PipeTransform} from '@angular/core';
|
||||
import {LibraryService} from "../_services/library.service";
|
||||
import {Observable} from "rxjs";
|
||||
|
||||
@Pipe({
|
||||
name: 'libraryName',
|
||||
standalone: true
|
||||
})
|
||||
export class LibraryNamePipe implements PipeTransform {
|
||||
private readonly libraryService = inject(LibraryService);
|
||||
|
||||
transform(libraryId: number): Observable<string> {
|
||||
return this.libraryService.getLibraryName(libraryId);
|
||||
}
|
||||
|
||||
}
|
35
UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts
Normal file
35
UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {MetadataSettingField} from "../admin/_models/metadata-setting-field";
|
||||
import {translate} from "@jsverse/transloco";
|
||||
|
||||
@Pipe({
|
||||
name: 'metadataSettingFiled',
|
||||
standalone: true
|
||||
})
|
||||
export class MetadataSettingFiledPipe implements PipeTransform {
|
||||
|
||||
transform(value: MetadataSettingField): string {
|
||||
switch (value) {
|
||||
case MetadataSettingField.AgeRating:
|
||||
return translate('metadata-setting-field-pipe.age-rating');
|
||||
case MetadataSettingField.People:
|
||||
return translate('metadata-setting-field-pipe.people');
|
||||
case MetadataSettingField.Covers:
|
||||
return translate('metadata-setting-field-pipe.covers');
|
||||
case MetadataSettingField.Summary:
|
||||
return translate('metadata-setting-field-pipe.summary');
|
||||
case MetadataSettingField.PublicationStatus:
|
||||
return translate('metadata-setting-field-pipe.publication-status');
|
||||
case MetadataSettingField.StartDate:
|
||||
return translate('metadata-setting-field-pipe.start-date');
|
||||
case MetadataSettingField.Genres:
|
||||
return translate('metadata-setting-field-pipe.genres');
|
||||
case MetadataSettingField.Tags:
|
||||
return translate('metadata-setting-field-pipe.tags');
|
||||
case MetadataSettingField.LocalizedName:
|
||||
return translate('metadata-setting-field-pipe.localized-name');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
16
UI/Web/src/app/admin/_models/metadata-setting-field.ts
Normal file
16
UI/Web/src/app/admin/_models/metadata-setting-field.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export enum MetadataSettingField {
|
||||
Summary = 1,
|
||||
PublicationStatus = 2,
|
||||
StartDate = 3,
|
||||
Genres = 4,
|
||||
Tags = 5,
|
||||
LocalizedName = 6,
|
||||
Covers = 7,
|
||||
AgeRating = 8,
|
||||
People = 9
|
||||
}
|
||||
|
||||
export const allMetadataSettingField = Object.keys(MetadataSettingField)
|
||||
.filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0)
|
||||
.map(key => parseInt(key, 10)) as MetadataSettingField[];
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import {AgeRating} from "../../_models/metadata/age-rating";
|
||||
import {PersonRole} from "../../_models/metadata/person";
|
||||
import {MetadataSettingField} from "./metadata-setting-field";
|
||||
|
||||
export enum MetadataFieldType {
|
||||
Genre = 0,
|
||||
|
@ -22,6 +23,7 @@ export interface MetadataSettings {
|
|||
enableRelationships: boolean;
|
||||
enablePeople: boolean;
|
||||
enableStartDate: boolean;
|
||||
enableCoverImage: boolean;
|
||||
enableLocalizedName: boolean;
|
||||
enableGenres: boolean;
|
||||
enableTags: boolean;
|
||||
|
@ -31,4 +33,5 @@ export interface MetadataSettings {
|
|||
blacklist: Array<string>;
|
||||
whitelist: Array<string>;
|
||||
personRoles: Array<PersonRole>;
|
||||
overrides: Array<MetadataSettingField>;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
[footerHeight]="50"
|
||||
>
|
||||
|
||||
<ngx-datatable-column name="lastModifiedUtc" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="3">
|
||||
<ngx-datatable-column name="series.name" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="3">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('series-name-header')}}
|
||||
</ng-template>
|
||||
|
@ -34,6 +34,15 @@
|
|||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
<ngx-datatable-column name="series.libraryId" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="3">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('library-name-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" ngx-datatable-cell-template>
|
||||
{{item.series.libraryId | libraryName | async}}
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
|
||||
<ngx-datatable-column name="status" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="1">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
|
@ -70,9 +79,13 @@
|
|||
|
||||
<ngx-datatable-column name="" [width]="20" [sortable]="false" [draggable]="false" [resizeable]="false" [flexGrow]="1">
|
||||
<ng-template let-column="column" ngx-datatable-header-template>
|
||||
{{t('actions-header')}}
|
||||
</ng-template>
|
||||
<ng-template let-item="row" let-idx="index" ngx-datatable-cell-template>
|
||||
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event, item.series)"></app-card-actionables>
|
||||
<button class="btn btn-icon" (click)="fixMatch(item.series)">
|
||||
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('match-alt', {seriesName: item.series.name})}}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ngx-datatable-column>
|
||||
|
||||
|
|
|
@ -4,9 +4,7 @@ import {Router} from "@angular/router";
|
|||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
import {Series} from "../../_models/series";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
import {ManageService} from "../../_services/manage.service";
|
||||
import {ManageMatchSeries} from "../../_models/kavitaplus/manage-match-series";
|
||||
|
@ -19,46 +17,47 @@ import {MatchStateOptionPipe} from "../../_pipes/match-state.pipe";
|
|||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
import {debounceTime, distinctUntilChanged, switchMap, tap} from "rxjs";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter";
|
||||
import {ScrobbleEventType} from "../../_models/scrobbling/scrobble-event";
|
||||
import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable";
|
||||
import {LibraryNamePipe} from "../../_pipes/library-name.pipe";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {EVENTS, MessageHubService} from "../../_services/message-hub.service";
|
||||
import {ScanSeriesEvent} from "../../_models/events/scan-series-event";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-matched-metadata',
|
||||
standalone: true,
|
||||
imports: [
|
||||
TranslocoDirective,
|
||||
ImageComponent,
|
||||
CardActionablesComponent,
|
||||
VirtualScrollerModule,
|
||||
ReactiveFormsModule,
|
||||
Select2Module,
|
||||
MatchStateOptionPipe,
|
||||
UtcToLocalTimePipe,
|
||||
DefaultValuePipe,
|
||||
NgxDatatableModule,
|
||||
],
|
||||
imports: [
|
||||
TranslocoDirective,
|
||||
ImageComponent,
|
||||
VirtualScrollerModule,
|
||||
ReactiveFormsModule,
|
||||
Select2Module,
|
||||
MatchStateOptionPipe,
|
||||
UtcToLocalTimePipe,
|
||||
DefaultValuePipe,
|
||||
NgxDatatableModule,
|
||||
LibraryNamePipe,
|
||||
AsyncPipe,
|
||||
],
|
||||
templateUrl: './manage-matched-metadata.component.html',
|
||||
styleUrl: './manage-matched-metadata.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ManageMatchedMetadataComponent implements OnInit {
|
||||
protected readonly MatchState = MatchStateOption;
|
||||
protected readonly ColumnMode = ColumnMode;
|
||||
protected readonly allMatchStates = allMatchStates.filter(m => m !== MatchStateOption.Matched); // Matched will have too many
|
||||
|
||||
private readonly licenseService = inject(LicenseService);
|
||||
private readonly actionFactory = inject(ActionFactoryService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly manageService = inject(ManageService);
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
protected readonly imageService = inject(ImageService);
|
||||
|
||||
|
||||
isLoading: boolean = true;
|
||||
data: Array<ManageMatchSeries> = [];
|
||||
actions: Array<ActionItem<Series>> = this.actionFactory.getSeriesActions(this.fixMatch.bind(this))
|
||||
.filter(item => item.action === Action.Match);
|
||||
filterGroup = new FormGroup({
|
||||
'matchState': new FormControl(MatchStateOption.Error, []),
|
||||
});
|
||||
|
@ -71,6 +70,15 @@ export class ManageMatchedMetadataComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
|
||||
this.messageHub.messages$.subscribe(message => {
|
||||
if (message.event !== EVENTS.ScanSeries) return;
|
||||
|
||||
const evt = message.payload as ScanSeriesEvent;
|
||||
if (this.data.filter(d => d.series.id === evt.seriesId).length > 0) {
|
||||
this.loadData();
|
||||
}
|
||||
});
|
||||
|
||||
this.filterGroup.valueChanges.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
|
@ -86,7 +94,6 @@ export class ManageMatchedMetadataComponent implements OnInit {
|
|||
).subscribe();
|
||||
|
||||
this.loadData().subscribe();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -108,21 +115,11 @@ export class ManageMatchedMetadataComponent implements OnInit {
|
|||
}));
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Series>, series: Series) {
|
||||
if (action.callback) {
|
||||
action.callback(action, series);
|
||||
}
|
||||
}
|
||||
|
||||
fixMatch(actionItem: ActionItem<Series>, series: Series) {
|
||||
fixMatch(series: Series) {
|
||||
this.actionService.matchSeries(series, result => {
|
||||
if (!result) return;
|
||||
this.loadData().subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly LooseLeafOrDefaultNumber = LooseLeafOrDefaultNumber;
|
||||
protected readonly ScrobbleEventType = ScrobbleEventType;
|
||||
protected readonly SpecialVolumeNumber = SpecialVolumeNumber;
|
||||
protected readonly ColumnMode = ColumnMode;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,18 @@
|
|||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enableLocalizedName'); as formControl) {
|
||||
<app-setting-switch [title]="t('localized-name-label')" [subtitle]="t('localized-name-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="localized-name" type="checkbox" class="form-check-input" formControlName="enableLocalizedName">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enablePublicationStatus'); as formControl) {
|
||||
<app-setting-switch [title]="t('derive-publication-status-label')" [subtitle]="t('derive-publication-status-tooltip')">
|
||||
|
@ -65,6 +77,18 @@
|
|||
}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
@if(settingsForm.get('enableCoverImage'); as formControl) {
|
||||
<app-setting-switch [title]="t('enable-cover-image-label')" [subtitle]="t('enable-cover-image-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input id="enable-cover-image" type="checkbox" class="form-check-input" formControlName="enableCoverImage">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if(settingsForm.get('enablePeople'); as formControl) {
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
|
@ -183,13 +207,13 @@
|
|||
<div formArrayName="ageRatingMappings">
|
||||
@for(mapping of ageRatingMappings.controls; track mapping; let i = $index) {
|
||||
<div [formGroupName]="i" class="row mb-2">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-4 d-flex align-items-center justify-content-center">
|
||||
<input type="text" class="form-control" formControlName="str" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-2 d-flex align-items-center justify-content-center">
|
||||
<i class="fa fa-arrow-right" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-4 d-flex align-items-center justify-content-center">
|
||||
<select class="form-select" formControlName="rating">
|
||||
@for (ageRating of ageRatings; track ageRating.value) {
|
||||
<option [value]="ageRating.value">
|
||||
|
@ -202,13 +226,21 @@
|
|||
<button class="btn btn-icon" (click)="removeAgeRatingMappingRow(i)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@if($last) {
|
||||
<button class="btn btn-icon" (click)="addAgeRatingMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @empty {
|
||||
<button class="btn btn-secondary" (click)="addAgeRatingMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> {{t('add-age-rating-mapping-label')}}
|
||||
</button>
|
||||
}
|
||||
|
||||
<button class="btn btn-secondary" (click)="addAgeRatingMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> {{t('add-age-rating-mapping-label')}}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
@ -252,15 +284,39 @@
|
|||
<button class="btn btn-icon" (click)="removeFieldMappingRow(i)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@if ($last) {
|
||||
<button class="btn btn-icon" (click)="addFieldMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @empty {
|
||||
<button class="btn btn-secondary" (click)="addFieldMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> {{t('add-field-mapping-label')}}
|
||||
</button>
|
||||
}
|
||||
|
||||
<button class="btn btn-secondary float-end" (click)="addFieldMapping()">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> {{t('add-field-mapping-label')}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
@if (settingsForm.get('overrides')) {
|
||||
<h5>{{t('overrides-label')}}</h5>
|
||||
<p>{{t('overrides-description')}}</p>
|
||||
<div class="row g-0 mt-4 mb-4" formArrayName="overrides">
|
||||
@for(field of allMetadataSettingFields; track field; let i = $index) {
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" [formControlName]="'override_' + i" [id]="'override-' + field">
|
||||
<label class="form-check-label" [for]="'override-' + field">{{ field | metadataSettingFiled }}</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</form>
|
||||
}
|
||||
</ng-container>
|
||||
|
|
|
@ -17,6 +17,8 @@ import {MetadataFieldMapping, MetadataFieldType} from "../_models/metadata-setti
|
|||
import {PersonRole} from "../../_models/metadata/person";
|
||||
import {PersonRolePipe} from "../../_pipes/person-role.pipe";
|
||||
import {NgClass} from "@angular/common";
|
||||
import {allMetadataSettingField} from "../_models/metadata-setting-field";
|
||||
import {MetadataSettingFiledPipe} from "../../_pipes/metadata-setting-filed.pipe";
|
||||
|
||||
|
||||
@Component({
|
||||
|
@ -31,6 +33,7 @@ import {NgClass} from "@angular/common";
|
|||
TagBadgeComponent,
|
||||
AgeRatingPipe,
|
||||
PersonRolePipe,
|
||||
MetadataSettingFiledPipe,
|
||||
],
|
||||
templateUrl: './manage-metadata-settings.component.html',
|
||||
styleUrl: './manage-metadata-settings.component.scss',
|
||||
|
@ -52,6 +55,7 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
fieldMappings = this.fb.array([]);
|
||||
personRoles: PersonRole[] = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character];
|
||||
isLoaded = false;
|
||||
allMetadataSettingFields = allMetadataSettingField;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.metadataService.getAllAgeRatings().subscribe(ratings => {
|
||||
|
@ -66,6 +70,7 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
this.settingService.getMetadataSettings().subscribe(settings => {
|
||||
this.settingsForm.addControl('enabled', new FormControl(settings.enabled, []));
|
||||
this.settingsForm.addControl('enableSummary', new FormControl(settings.enableSummary, []));
|
||||
this.settingsForm.addControl('enableLocalizedName', new FormControl(settings.enableLocalizedName, []));
|
||||
this.settingsForm.addControl('enablePublicationStatus', new FormControl(settings.enablePublicationStatus, []));
|
||||
this.settingsForm.addControl('enableRelations', new FormControl(settings.enableRelationships, []));
|
||||
this.settingsForm.addControl('enableGenres', new FormControl(settings.enableGenres, []));
|
||||
|
@ -73,6 +78,7 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
this.settingsForm.addControl('enableRelationships', new FormControl(settings.enableRelationships, []));
|
||||
this.settingsForm.addControl('enablePeople', new FormControl(settings.enablePeople, []));
|
||||
this.settingsForm.addControl('enableStartDate', new FormControl(settings.enableStartDate, []));
|
||||
this.settingsForm.addControl('enableCoverImage', new FormControl(settings.enableCoverImage, []));
|
||||
|
||||
this.settingsForm.addControl('blacklist', new FormControl((settings.blacklist || '').join(','), []));
|
||||
this.settingsForm.addControl('whitelist', new FormControl((settings.whitelist || '').join(','), []));
|
||||
|
@ -86,6 +92,15 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
)
|
||||
));
|
||||
|
||||
this.settingsForm.addControl('overrides', this.fb.group(
|
||||
Object.fromEntries(
|
||||
this.allMetadataSettingFields.map((role, index) => [
|
||||
`override_${index}`,
|
||||
this.fb.control((settings.overrides || []).includes(role)),
|
||||
])
|
||||
)
|
||||
));
|
||||
|
||||
|
||||
if (settings.ageRatingMappings) {
|
||||
Object.entries(settings.ageRatingMappings).forEach(([str, rating]) => {
|
||||
|
@ -171,7 +186,10 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||
whitelist: (model.whitelist || '').split(',').map((item: string) => item.trim()),
|
||||
personRoles: Object.entries(this.settingsForm.get('personRoles')!.value)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key, _]) => this.personRoles[parseInt(key.split('_')[1], 10)])
|
||||
.map(([key, _]) => this.personRoles[parseInt(key.split('_')[1], 10)]),
|
||||
overrides: Object.entries(this.settingsForm.get('overrides')!.value)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key, _]) => this.allMetadataSettingFields[parseInt(key.split('_')[1], 10)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue