perf(webui): reduce amount of API requests when matching cbl

This commit is contained in:
Gauthier Roebroeck 2023-03-13 18:26:09 +08:00
parent 2461c835ad
commit e3d9cb7fd5
3 changed files with 106 additions and 101 deletions

View file

@ -1,32 +1,32 @@
<template>
<tr v-if="match">
<tr v-if="request">
<slot/>
<td>
<div v-for="s in match.request.series" :key="s">{{ s }}</div>
<div v-for="s in request.series" :key="s">{{ s }}</div>
</td>
<td>{{ match.request.number }}</td>
<td>{{ request.number }}</td>
<!-- Series picker -->
<td @click="modalSeriesPicker = true" style="cursor: pointer">
<template v-if="selectedSeries">{{ selectedSeries.metadata.title }}</template>
<template v-if="series">{{ series.title }}</template>
<template v-else>
<div style="height: 2em" class="missing"></div>
</template>
<series-picker-dialog v-model="modalSeriesPicker" :series.sync="selectedSeries"></series-picker-dialog>
<series-picker-dialog v-model="modalSeriesPicker" @update:series="pickedSeries"></series-picker-dialog>
</td>
<!-- Book picker -->
<td @click="selectedSeries ? modalBookPicker = true : undefined" :style="selectedSeries ? 'cursor: pointer': ''">
<template v-if="selectedBook">
{{ selectedBook.metadata.number }} - {{ selectedBook.metadata.title }}
<td @click="series ? openBookPicker() : undefined" :style="series ? 'cursor: pointer': ''">
<template v-if="book">
{{ book.number }} - {{ book.title }}
</template>
<template v-else-if="selectedSeries">
<template v-else-if="series">
<div style="height: 2em" class="missing"></div>
</template>
<book-picker-dialog
v-model="modalBookPicker"
:books="seriesBooks"
:book.sync="selectedBook"
@update:book="pickedBook"
></book-picker-dialog>
</td>
@ -57,10 +57,11 @@ import Vue, {PropType} from 'vue'
import {SeriesDto} from '@/types/komga-series'
import {BookDto} from '@/types/komga-books'
import SeriesPickerDialog from '@/components/dialogs/SeriesPickerDialog.vue'
import TransientBookDetailsDialog from '@/components/dialogs/TransientBookDetailsDialog.vue'
import TransientBookViewerDialog from '@/components/dialogs/TransientBookViewerDialog.vue'
import FileNameChooserDialog from '@/components/dialogs/FileNameChooserDialog.vue'
import {ReadListRequestBookMatchesDto} from '@/types/komga-readlists'
import {
ReadListRequestBookDto,
ReadListRequestBookMatchBookDto,
ReadListRequestBookMatchSeriesDto,
} from '@/types/komga-readlists'
import BookPickerDialog from '@/components/dialogs/BookPickerDialog.vue'
export default Vue.extend({
@ -70,82 +71,72 @@ export default Vue.extend({
SeriesPickerDialog,
},
props: {
match: {
type: Object as PropType<ReadListRequestBookMatchesDto>,
request: {
type: Object as PropType<ReadListRequestBookDto>,
required: true,
},
series: {
type: Object as PropType<ReadListRequestBookMatchSeriesDto>,
required: false,
},
book: {
type: Object as PropType<ReadListRequestBookMatchBookDto>,
required: false,
},
duplicate: {
type: Boolean,
default: false,
},
bookId: {
type: String,
required: false,
},
},
watch: {
match: {
handler(val) {
this.processMatch(val)
},
immediate: true,
},
selectedSeries: {
handler(val, old) {
if (!old) {
this.getSeriesBooks(val)
} else if (val?.id !== old?.id) {
this.selectedBook = undefined
this.getSeriesBooks(val)
series: {
handler(val: ReadListRequestBookMatchSeriesDto, old: ReadListRequestBookMatchSeriesDto) {
if(val?.seriesId !== old?.seriesId) {
this.seriesBooksCached = false
}
},
immediate: true,
},
selectedBook: {
handler(val) {
this.$emit('update:bookId', val?.id)
},
immediate: true,
},
},
data: () => ({
innerSelect: false,
selectedSeries: undefined as SeriesDto | undefined,
selectedBook: undefined as BookDto | undefined,
seriesBooks: [] as BookDto[],
modalSeriesPicker: false,
modalBookPicker: false,
seriesBooks: [] as BookDto[],
seriesBooksCached: false,
}),
computed: {
existingFileNames(): string[] {
return this.seriesBooks.map(x => x.name)
},
error(): string {
if (!this.selectedSeries) return this.$t('book_import.row.error_choose_series').toString()
if (!this.selectedBook) return this.$t('readlist_import.row.error_choose_book').toString()
if (!this.series) return this.$t('book_import.row.error_choose_series').toString()
if (!this.book) return this.$t('readlist_import.row.error_choose_book').toString()
return ''
},
},
methods: {
async processMatch(match: ReadListRequestBookMatchesDto) {
let seriesId: string | undefined
if (match.matches.length === 1) {
seriesId = match.matches[0].seriesId
} else if (match.matches.length > 1) {
seriesId = match.matches.find((m) => m.bookIds.length > 0)?.seriesId
}
if (seriesId) {
this.selectedSeries = await this.$komgaSeries.getOneSeries(seriesId)
const bookId = match.matches.find((m) => m.seriesId === seriesId)?.bookIds.find(Boolean)
if (bookId) {
this.selectedBook = await this.$komgaBooks.getBook(bookId)
}
openBookPicker() {
if(!this.seriesBooksCached) {
this.$komgaSeries.getBooks(this.series?.seriesId, {unpaged: true})
.then(r => {
this.seriesBooks = r.content
this.seriesBooksCached = true
})
}
this.modalBookPicker = true
},
async getSeriesBooks(series: SeriesDto) {
if (series) {
this.seriesBooks = (await this.$komgaSeries.getBooks(series.id, {unpaged: true})).content
}
pickedSeries(series: SeriesDto) {
this.$emit('update:series', {
seriesId: series.id,
title: series.metadata.title,
} as ReadListRequestBookMatchSeriesDto)
},
pickedBook(book: BookDto) {
this.$emit('update:book', {
bookId: book.id,
number: book.metadata.number,
title: book.metadata.title,
} as ReadListRequestBookMatchBookDto)
},
},
})

View file

@ -23,24 +23,7 @@ export interface ReadListUpdateDto {
bookIds?: string[]
}
export interface ReadListRequestResultDto {
readList?: ReadListDto,
unmatchedBooks: ReadListRequestResultBookDto[],
errorCode: string,
requestName: string,
}
export interface ReadListRequestResultBookDto {
book: ReadListRequestBookDto,
errorCode: string,
}
export interface ReadListRequestBookDto {
series: string,
number: string,
}
export interface ReadListRequestBookV2Dto {
series: string[],
number: string,
}
@ -54,7 +37,7 @@ export interface ReadListThumbnailDto {
export interface ReadListRequestMatchDto {
readListMatch: ReadListMatchDto,
matches: ReadListRequestBookMatchesDto[],
requests: ReadListRequestBookMatchesDto[],
errorCode: string,
}
@ -64,11 +47,22 @@ export interface ReadListMatchDto {
}
export interface ReadListRequestBookMatchesDto {
request: ReadListRequestBookV2Dto,
request: ReadListRequestBookDto,
matches: ReadListRequestBookMatchDto[],
}
export interface ReadListRequestBookMatchDto {
seriesId: string,
bookIds: string[],
series: ReadListRequestBookMatchSeriesDto,
books: ReadListRequestBookMatchBookDto[],
}
export interface ReadListRequestBookMatchSeriesDto {
seriesId: string,
title: string,
}
export interface ReadListRequestBookMatchBookDto {
bookId: string,
number: string,
title: string,
}

View file

@ -18,7 +18,8 @@
<v-col cols="auto">
<v-btn
color="primary"
:disabled="!validMatch"
:disabled="!validMatch || matching"
:loading="matching"
@click="matchFile"
>{{ $t('data_import.button_match') }}
</v-btn>
@ -43,11 +44,14 @@
</tr>
</thead>
<tbody
v-for="(match, i) in result.matches"
v-for="(match, i) in result.requests"
:key="i"
>
<read-list-match-row :match="match" :book-id.sync="form.bookIds[i]"
:duplicate="isDuplicateBook(form.bookIds[i])">
<read-list-match-row :request="match.request"
:series.sync="series[i]"
:book.sync="form.books[i]"
:duplicate="isDuplicateBook(form.books[i])"
>
<td>{{ i + 1 }}</td>
</read-list-match-row>
</tbody>
@ -98,13 +102,18 @@
<script lang="ts">
import Vue from 'vue'
import {convertErrorCodes} from '@/functions/error-codes'
import {ReadListCreationDto, ReadListDto, ReadListRequestMatchDto} from '@/types/komga-readlists'
import {
ReadListCreationDto,
ReadListDto,
ReadListRequestBookMatchBookDto,
ReadListRequestMatchDto, ReadListRequestBookMatchSeriesDto,
} from '@/types/komga-readlists'
import ReadListMatchRow from '@/components/ReadListMatchRow.vue'
import {ERROR, NOTIFICATION, NotificationEvent} from '@/types/events'
import {helpers, required} from 'vuelidate/lib/validators'
function validBookIds(this: any, value: string[]) {
return value.filter(Boolean).length === this.result.matches.length && value.filter(Boolean).length === [...new Set(value)].length
function validBookIds(this: any, value: any[]) {
return value.filter(Boolean).length === this.result.requests.length && value.filter(Boolean).length === [...new Set(value)].length
}
function validName(this: any, value: string) {
@ -115,26 +124,27 @@ export default Vue.extend({
name: 'ImportReadLists',
components: {ReadListMatchRow},
data: () => ({
convertErrorCodes,
file: undefined,
result: undefined as unknown as ReadListRequestMatchDto,
result: undefined as unknown as ReadListRequestMatchDto | undefined,
validMatch: true,
validCreate: true,
series: [] as (ReadListRequestBookMatchSeriesDto | undefined)[],
form: {
name: '',
ordered: true,
summary: '',
bookIds: [] as string[],
books: [] as (ReadListRequestBookMatchBookDto | undefined)[],
},
readLists: [] as ReadListDto[],
creationFinished: false,
matching: false,
}),
validations: {
form: {
name: {required, validName},
ordered: {},
summary: {},
bookIds: {validBookIds},
books: {validBookIds},
},
},
computed: {
@ -156,27 +166,37 @@ export default Vue.extend({
async mounted() {
this.readLists = (await this.$komgaReadLists.getReadLists(undefined, {unpaged: true} as PageRequest)).content
},
watch: {},
methods: {
isDuplicateBook(bookId: string): boolean {
return this.form.bookIds.filter((b) => b === bookId).length > 1
isDuplicateBook(book: ReadListRequestBookMatchBookDto): boolean {
return this.form.books.filter((b) => b?.bookId === book?.bookId).length > 1
},
async matchFile() {
this.matching = true
this.result = undefined
this.form.summary = ''
this.form.ordered = true
this.form.books = []
this.series = []
this.creationFinished = false
try {
this.result = await this.$komgaReadLists.postReadListMatch(this.file)
this.form.name = this.result.readListMatch.name
this.result.requests.forEach((request, index) => {
const match = request.matches.find(Boolean)
this.$set(this.series, index, match?.series)
this.$set(this.form.books, index, match?.books.find(Boolean))
})
} catch (e) {
this.$eventHub.$emit(ERROR, {message: convertErrorCodes(e.message)} as ErrorEvent)
}
this.form.summary = ''
this.form.ordered = true
this.form.bookIds = []
this.creationFinished = false
this.matching = false
},
validateForm(): ReadListCreationDto | undefined {
if (this.$v.$invalid) return undefined
return {
name: this.form.name,
bookIds: this.form.bookIds,
bookIds: this.form.books.map(b => b?.bookId).filter(Boolean) as string[],
ordered: this.form.ordered,
summary: this.form.summary,
}