Skip to content

Commit

Permalink
New page that allow managing matched metadata for Kavita+ is done for…
Browse files Browse the repository at this point in the history
… basic filtering. I just need to maybe hook up a filter and allow triggering a job to trigger the automatic job (but need to expose the rate limit left first).
  • Loading branch information
majora2007 committed Jan 7, 2025
1 parent 6f52ad7 commit f0ad031
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 50 deletions.
8 changes: 4 additions & 4 deletions API/Controllers/ManageController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ public ManageController(IUnitOfWork unitOfWork, ILicenseService licenseService)
/// </summary>
/// <returns></returns>
[Authorize("RequireAdminRole")]
[HttpGet("series-metadata")]
public async Task<ActionResult<IList<ManageMatchSeriesDto>>> SeriesMetadata()
[HttpPost("series-metadata")]
public async Task<ActionResult<IList<ManageMatchSeriesDto>>> SeriesMetadata(ManageMatchFilterDto filter)
{
if (!await _licenseService.HasActiveLicense()) return Ok(Array.Empty<SeriesDto>());
// Need the series, Need the external match
return Ok(await _unitOfWork.ExternalSeriesMetadataRepository.GetAllSeries());

return Ok(await _unitOfWork.ExternalSeriesMetadataRepository.GetAllSeries(filter));
}
}
19 changes: 19 additions & 0 deletions API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace API.DTOs.KavitaPlus.Manage;

/// <summary>
/// Represents an option in the UI layer for Filtering
/// </summary>
public enum MatchStateOption
{
All = 0,
Matched = 1,
NotMatched = 2,
Error = 3,
DontMatch = 4
}

public class ManageMatchFilterDto
{
public MatchStateOption MatchStateOption { get; set; } = MatchStateOption.All;
public string SearchTerm { get; set; } = string.Empty;
}
10 changes: 6 additions & 4 deletions API/Data/Repositories/ExternalSeriesMetadataRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public interface IExternalSeriesMetadataRepository
Task LinkRecommendationsToSeries(Series series);
Task<bool> IsBlacklistedSeries(int seriesId);
Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit);
Task<IList<ManageMatchSeriesDto>> GetAllSeries();
Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter);
}

public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
Expand Down Expand Up @@ -221,12 +221,14 @@ public async Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit)
.ToListAsync();
}

public async Task<IList<ManageMatchSeriesDto>> GetAllSeries()
public async Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter)
{
return await _context.Series
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
.Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow)
.OrderByDescending(s => s.NormalizedName)
//.Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow)
.FilterMatchState(filter.MatchStateOption)
//.WhereNameLike(filter.SearchTerm)
.OrderBy(s => s.NormalizedName)
.ProjectTo<ManageMatchSeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
Expand Down
14 changes: 14 additions & 0 deletions API/Extensions/QueryExtensions/QueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using API.Data.Misc;
using API.Data.Repositories;
using API.DTOs.Filtering;
using API.DTOs.KavitaPlus.Manage;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Scrobble;
Expand Down Expand Up @@ -281,4 +282,17 @@ public static IQueryable<T> DoOrderBy<T, TKey>(this IQueryable<T> query, Express
{
return sortOptions.IsAscending ? query.OrderBy(keySelector) : query.OrderByDescending(keySelector);
}

public static IQueryable<Series> FilterMatchState(this IQueryable<Series> query, MatchStateOption stateOption)
{
return stateOption switch
{
MatchStateOption.All => query,
MatchStateOption.Matched => query.Where(s => s.ExternalSeriesMetadata != null),
MatchStateOption.NotMatched => query.Where(s => s.ExternalSeriesMetadata == null && !s.IsBlacklisted),
MatchStateOption.Error => query.Where(s => s.IsBlacklisted),
MatchStateOption.DontMatch => query.Where(s => s.DontMatch),
_ => throw new ArgumentOutOfRangeException(nameof(stateOption), stateOption, null)
};
}
}
9 changes: 6 additions & 3 deletions API/Helpers/AutoMapperProfiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,14 @@ public AutoMapperProfiles()
CreateMap<ExternalRecommendation, ExternalSeriesDto>();
CreateMap<Series, ManageMatchSeriesDto>()
.ForMember(dest => dest.Series,
opt => opt.MapFrom(src => src))
opt =>
opt.MapFrom(src => src))
.ForMember(dest => dest.IsMatched,
opt => opt.MapFrom(src => src.ExternalSeriesMetadata != null))
opt =>
opt.MapFrom(src => src.ExternalSeriesMetadata != null && src.ExternalSeriesMetadata.AniListId != 0 && src.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue))
.ForMember(dest => dest.ValidUntilUtc,
opt => opt.MapFrom(src => src.ExternalSeriesMetadata.ValidUntilUtc));
opt =>
opt.MapFrom(src => src.ExternalSeriesMetadata.ValidUntilUtc));


CreateMap<MangaFile, FileExtensionExportDto>();
Expand Down
6 changes: 6 additions & 0 deletions UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {MatchStateOption} from "./match-state-option";

export interface ManageMatchFilter {
matchStateOption: MatchStateOption;
searchTerm: string;
}
11 changes: 11 additions & 0 deletions UI/Web/src/app/_models/kavitaplus/match-state-option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum MatchStateOption {
All = 0,
Matched = 1,
NotMatched = 2,
Error = 3,
DontMatch = 4
}

export const allMatchStates = [
MatchStateOption.Matched, MatchStateOption.NotMatched, MatchStateOption.Error, MatchStateOption.DontMatch
];
27 changes: 27 additions & 0 deletions UI/Web/src/app/_pipes/match-state.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Pipe, PipeTransform } from '@angular/core';
import {MatchStateOption} from "../_models/kavitaplus/match-state-option";
import {translate} from "@jsverse/transloco";

@Pipe({
name: 'matchStateOption',
standalone: true
})
export class MatchStateOptionPipe implements PipeTransform {

transform(value: MatchStateOption): string {
switch (value) {
case MatchStateOption.DontMatch:
return translate('manage-matched-metadata.dont-match-label');
case MatchStateOption.All:
return translate('manage-matched-metadata.all-status-label');
case MatchStateOption.Matched:
return translate('manage-matched-metadata.matched-status-label');
case MatchStateOption.NotMatched:
return translate('manage-matched-metadata.unmatched-status-label');
case MatchStateOption.Error:
return translate('manage-matched-metadata.blacklist-status-label');

}
}

}
6 changes: 3 additions & 3 deletions UI/Web/src/app/_services/manage.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {inject, Injectable} from '@angular/core';
import {environment} from "../../environments/environment";
import {LicenseInfo} from "../_models/kavitaplus/license-info";
import {HttpClient} from "@angular/common/http";
import {ManageMatchSeries} from "../_models/kavitaplus/manage-match-series";
import {ManageMatchFilter} from "../_models/kavitaplus/manage-match-filter";

@Injectable({
providedIn: 'root'
Expand All @@ -12,7 +12,7 @@ export class ManageService {
baseUrl = environment.apiUrl;
private readonly httpClient = inject(HttpClient);

getAllKavitaPlusSeries() {
return this.httpClient.get<Array<ManageMatchSeries>>(this.baseUrl + `manage/series-metadata`)
getAllKavitaPlusSeries(filter: ManageMatchFilter) {
return this.httpClient.post<Array<ManageMatchSeries>>(this.baseUrl + `manage/series-metadata`, filter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,48 @@
<h4>{{t('title')}}</h4>
<p>{{t('description')}}</p>

<!-- Note that this is rate limited -->
<button class="btn btn-secondary">Try matching all unmatched</button>
<form [formGroup]="filterGroup">
<div class="row g-0">
<div class="col-auto">
<label for="match-filter">Match State</label>
<select class="form-select" formControlName="matchState" id="match-filter">
@for(state of allMatchStates; track state) {
<option [value]="state">{{state | matchStateOption}}</option>
}
</select>
</div>

<!-- Some filter or something to show: Unmatched only, Matched only, Blacklisted Only -->
<div class="col-auto ms-2">
<!-- Note that this is rate limited -->
<button class="btn btn-secondary mt-4">Trigger Auto-Match</button>
</div>
</div>
</form>

<div class="table-responsive-md">
<table class="table table-striped table-sm">
<thead #header>
<tr>
<!-- Series Image -->
<th scope="col"></th>

<th scope="col">
{{t('series-name-header')}}
</th>
<th scope="col">
{{t('status-header')}}
</th>
<!-- Actions header -->
<th scope="col"></th>
</tr>
</thead>
<tbody>
<virtual-scroller #scroll [items]="data" [bufferAmount]="1">

<div class="table-responsive-md">
<virtual-scroller #scroll [items]="data">
<table class="table table-striped table-sm">
<thead #header>
<tr>
<!-- Series Image -->
<th scope="col"></th>

<th scope="col">
{{t('series-name-header')}}
</th>
<th scope="col">
{{t('status-header')}}
</th>
<th scope="col">
{{t('valid-until-header')}}
</th>
<!-- Actions header -->
<th scope="col"></th>
</tr>
</thead>
<tbody #container>
@for(item of scroll.viewPortItems; track item; let index = $index) {
<tr>
<td>
Expand All @@ -39,10 +56,19 @@ <h4>{{t('title')}}</h4>
<td>
@if (item.series.isBlacklisted) {
{{t('blacklist-status-label')}}
} @else if (item.series.dontMatch) {
{{t('dont-match-status-label')}}
} @else {
{{item.isMatched ? t('matched-status-label') : t('unmatched-status-label') }}
}
</td>
<td>
@if (item.series.isBlacklisted || item.series.dontMatch || !item.isMatched) {
{{null | defaultValue}}
} @else {
{{item.validUntilUtc | utcToLocalTime}}
}
</td>
<td>
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event, item.series)"></app-card-actionables>
</td>
Expand All @@ -54,9 +80,8 @@ <h4>{{t('title')}}</h4>
<tr><td colspan="4" style="text-align: center;">{{t('no-data')}}</td></tr>
}
}

</virtual-scroller>
</tbody>
</table>
</tbody>
</table>
</virtual-scroller>
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
import {LicenseService} from "../../_services/license.service";
import {take} from "rxjs/operators";
import {Router} from "@angular/router";
import {TranslocoDirective} from "@jsverse/transloco";
import {LoadingComponent} from "../../shared/loading/loading.component";
Expand All @@ -13,6 +12,14 @@ import {ActionService} from "../../_services/action.service";
import {ManageService} from "../../_services/manage.service";
import {ManageMatchSeries} from "../../_models/kavitaplus/manage-match-series";
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
import {Select2Module} from "ng-select2-component";
import {ManageMatchFilter} from "../../_models/kavitaplus/manage-match-filter";
import {allMatchStates, MatchStateOption} from "../../_models/kavitaplus/match-state-option";
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";

@Component({
selector: 'app-manage-matched-metadata',
Expand All @@ -22,13 +29,21 @@ import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
ImageComponent,
CardActionablesComponent,
LoadingComponent,
VirtualScrollerModule
VirtualScrollerModule,
ReactiveFormsModule,
Select2Module,
MatchStateOptionPipe,
UtcToLocalTimePipe,
DefaultValuePipe
],
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 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);
Expand All @@ -42,25 +57,51 @@ export class ManageMatchedMetadataComponent implements OnInit {
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, []),
});


constructor() {
this.licenseService.hasValidLicense$.pipe(take(1)).subscribe(license => {
ngOnInit() {
this.licenseService.hasValidLicense$.subscribe(license => {
if (!license) {
// Navigate home
this.router.navigate(['/']);
return;
}

this.filterGroup.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
tap(_ => {
this.isLoading = true;
this.cdRef.markForCheck();
}),
switchMap(_ => this.loadData()),
tap(_ => {
this.isLoading = false;
this.cdRef.markForCheck();
}),
).subscribe();

this.loadData().subscribe();

});
}

ngOnInit() {
loadData() {
const filter: ManageMatchFilter = {
matchStateOption: parseInt(this.filterGroup.get('matchState')!.value + '', 10),
searchTerm: ''
};

this.isLoading = true;
this.data = [];
this.cdRef.markForCheck();
this.manageService.getAllKavitaPlusSeries().subscribe(data => {

return this.manageService.getAllKavitaPlusSeries(filter).pipe(tap(data => {
this.data = data;
this.isLoading = false;
this.cdRef.markForCheck();
});
}));
}

performAction(action: ActionItem<Series>, series: Series) {
Expand All @@ -71,8 +112,8 @@ export class ManageMatchedMetadataComponent implements OnInit {

fixMatch(actionItem: ActionItem<Series>, series: Series) {
this.actionService.matchSeries(series, result => {

if (!result) return;
this.loadData().subscribe();
});
}

}
Loading

0 comments on commit f0ad031

Please sign in to comment.