feat: series metadata supports alternate titles

Closes: #878
This commit is contained in:
Gauthier Roebroeck 2023-01-16 17:30:09 +08:00
parent f9f02a395b
commit 8e0655f29a
17 changed files with 282 additions and 15 deletions

View file

@ -5,7 +5,7 @@
</div>
<template v-slot:more="value">
<v-btn text small color="grey darken-1">
{{ value.open ? $t('read_more.less') : $t('read_more.more') }}
{{ value.open ? $t(i18nLess) : $t(i18nMore) }}
<v-icon right>mdi-chevron-{{ value.open ? 'up' : 'down' }}</v-icon>
</v-btn>
</template>
@ -20,6 +20,16 @@ import Vue from 'vue'
export default Vue.extend({
name: 'ReadMore',
components: { VueReadMoreSmooth },
props: {
i18nMore: {
type: String,
default: 'read_more.more',
},
i18nLess: {
type: String,
default: 'read_more.less',
},
},
})
</script>

View file

@ -27,6 +27,10 @@
<v-icon left class="hidden-xs-only">mdi-format-align-center</v-icon>
{{ $t('dialog.edit_series.tab_general') }}
</v-tab>
<v-tab class="justify-start">
<v-icon left class="hidden-xs-only">mdi-format-title</v-icon>
{{ $t('dialog.edit_series.tab_titles') }}
</v-tab>
<v-tab class="justify-start">
<v-icon left class="hidden-xs-only">mdi-tag-multiple</v-icon>
{{ $t('dialog.edit_series.tab_tags') }}
@ -258,6 +262,77 @@
</v-card>
</v-tab-item>
<!-- Tab: Alternate Titles -->
<v-tab-item v-if="single">
<v-card flat min-height="100">
<v-container fluid>
<!-- Titles -->
<v-form
v-model="titlesValid"
ref="titlesForm"
>
<v-row
v-for="(title, i) in form.alternateTitles"
:key="i"
>
<v-col cols="4" class="py-0">
<v-text-field v-model="form.alternateTitles[i].label"
:label="$t('dialog.edit_books.field_link_label')"
filled
dense
:rules="[alternateTitleRules]"
@input="$v.form.alternateTitles.$touch()"
@blur="$v.form.alternateTitles.$touch()"
@change="form.alternateTitlesLock = true"
>
<template v-slot:prepend>
<v-icon :color="form.alternateTitlesLock ? 'secondary' : ''"
@click="form.alternateTitlesLock = !form.alternateTitlesLock"
>
{{ form.linksLock ? 'mdi-lock' : 'mdi-lock-open' }}
</v-icon>
</template>
</v-text-field>
</v-col>
<v-col cols="8" class="py-0">
<v-text-field v-model="form.alternateTitles[i].title"
:label="$t('dialog.edit_books.field_alternate_title')"
filled
dense
:rules="[alternateTitleRules]"
@input="$v.form.alternateTitles.$touch()"
@blur="$v.form.alternateTitles.$touch()"
@change="form.alternateTitlesLock = true"
>
<template v-slot:append-outer>
<v-icon @click="form.alternateTitles.splice(i, 1)">mdi-delete</v-icon>
</template>
</v-text-field>
</v-col>
</v-row>
</v-form>
<v-row>
<v-spacer/>
<v-col cols="auto">
<v-btn
elevation="2"
fab
small
bottom
right
color="primary"
@click="form.alternateTitles.push({label:'', title:''})"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
</v-card>
</v-tab-item>
<!-- Tab: Tags -->
<v-tab-item>
<v-card flat>
@ -500,6 +575,7 @@ export default Vue.extend({
modal: false,
tab: 0,
linksValid: false,
titlesValid: false,
form: {
status: '',
statusLock: false,
@ -527,6 +603,8 @@ export default Vue.extend({
sharingLabelsLock: false,
links: [],
linksLock: false,
alternateTitles: [],
alternateTitlesLock: false,
},
mixed: {
status: false,
@ -601,6 +679,7 @@ export default Vue.extend({
publisher: {},
totalBookCount: {minValue: minValue(1)},
links: {},
alternateTitles: {},
},
},
computed: {
@ -645,6 +724,10 @@ export default Vue.extend({
},
},
methods: {
alternateTitleRules(text: string): boolean | string {
if (!!this.$_.trim(text)) return true
return this.$t('common.required').toString()
},
linksLabelRules(label: string): boolean | string {
if (!!this.$_.trim(label)) return true
return this.$t('common.required').toString()
@ -678,7 +761,8 @@ export default Vue.extend({
dialogReset(series: SeriesDto | SeriesDto[]) {
this.tab = 0
this.$v.$reset();
(this.$refs.linksForm as any)?.resetValidation()
(this.$refs.linksForm as any)?.resetValidation();
(this.$refs.titlesForm as any)?.resetValidation()
if (Array.isArray(series) && series.length === 0) return
if (Array.isArray(series) && series.length > 0) {
const status = this.$_.uniq(series.map(x => x.metadata.status))
@ -732,11 +816,13 @@ export default Vue.extend({
this.form.sharingLabelsLock = sharingLabelsLock.length > 1 ? false : sharingLabelsLock[0]
this.form.links = []
this.form.alternateTitles = []
} else {
this.form.genres = []
this.form.tags = []
this.form.sharingLabels = []
this.form.links = []
this.form.alternateTitles = []
this.$_.merge(this.form, (series as SeriesDto).metadata)
this.poster.selectedThumbnail = ''
this.poster.deleteQueue = []
@ -754,7 +840,10 @@ export default Vue.extend({
}
},
validateForm(): any {
if (!this.$v.$invalid && (!this.single || !this.$refs.linksForm || (this.$refs.linksForm as any).validate())) {
if (!this.$v.$invalid
&& (!this.single || !this.$refs.linksForm || (this.$refs.linksForm as any).validate())
&& (!this.single || !this.$refs.titlesForm || (this.$refs.titlesForm as any).validate())
) {
const metadata = {
statusLock: this.form.statusLock,
readingDirectionLock: this.form.readingDirectionLock,
@ -766,6 +855,7 @@ export default Vue.extend({
totalBookCountLock: this.form.totalBookCountLock,
sharingLabelsLock: this.form.sharingLabelsLock,
linksLock: this.form.linksLock,
alternateTitlesLock: this.form.alternateTitlesLock,
}
if (this.$v.form?.status?.$dirty) {
@ -827,6 +917,10 @@ export default Vue.extend({
if (this.$v.form?.links?.$dirty || this.form.links.length != (this.series as SeriesDto).metadata.links?.length) {
this.$_.merge(metadata, {links: this.form.links})
}
if (this.$v.form?.alternateTitles?.$dirty || this.form.alternateTitles.length != (this.series as SeriesDto).metadata.alternateTitles?.length) {
this.$_.merge(metadata, {alternateTitles: this.form.alternateTitles})
}
}
return metadata

View file

@ -347,6 +347,7 @@
"button_confirm": "Save changes",
"dialog_title_multiple": "Edit {count} book | Edit {count} books",
"dialog_title_single": "Edit {book}",
"field_alternate_title": "Alternate title",
"field_isbn": "ISBN",
"field_isbn_error": "Must be a valid ISBN 13",
"field_link_label": "Label",
@ -457,6 +458,7 @@
"tab_poster": "Poster",
"tab_sharing": "Sharing",
"tab_tags": "Tags",
"tab_titles": "Alternate Titles",
"tags_notice_multiple_edit": "You are editing tags for multiple series. This will override existing tags of each series."
},
"edit_user": {
@ -812,6 +814,10 @@
"tooltip_too_big": "File too big!",
"tooltip_user_uploaded": "User uploaded"
},
"titles_more": {
"less": "Less titles",
"more": "More titles"
},
"user_roles": {
"ADMIN": "Administrator",
"FILE_DOWNLOAD": "File download",

View file

@ -47,8 +47,10 @@ export interface SeriesMetadataDto {
totalBookCountLock: boolean,
sharingLabels: string[],
sharingLabelsLock: boolean,
links?: WebLinkDto[],
linksLock?: boolean
links: WebLinkDto[],
linksLock: boolean,
alternateTitles: AlternateTitleDto[],
alternateTitlesLock: boolean,
}
export interface SeriesBooksMetadataDto {
@ -88,6 +90,8 @@ export interface SeriesMetadataUpdateDto {
sharingLabelsLock: boolean,
links?: WebLinkDto[],
linksLock?: boolean,
alternateTitles?: AlternateTitleDto[],
alternateTitlesLock?: boolean,
}
export interface GroupCountDto {
@ -101,3 +105,8 @@ export interface SeriesThumbnailDto {
type: string,
selected: boolean
}
export interface AlternateTitleDto {
label: string,
title: string
}

View file

@ -113,7 +113,10 @@
<v-tooltip right>
<template v-slot:activator="{ on }">
<span v-on="on">{{
new Intl.DateTimeFormat($i18n.locale, {year: 'numeric', timeZone: 'UTC'}).format(new Date(series.booksMetadata.releaseDate))
new Intl.DateTimeFormat($i18n.locale, {
year: 'numeric',
timeZone: 'UTC'
}).format(new Date(series.booksMetadata.releaseDate))
}}</span>
</template>
{{ $t('browse_series.earliest_year_from_release_dates') }}
@ -166,6 +169,17 @@
</v-row>
<template v-if="$vuetify.breakpoint.smAndUp">
<!-- Alternate titles -->
<read-more class="mb-4" i18n-less="titles_more.less" i18n-more="titles_more.more">
<v-row v-for="(a, i) in series.metadata.alternateTitles"
:key="i"
class="align-center text-caption"
>
<v-col cols="4" sm="3" md="2" xl="1" class="py-0 text-uppercase" :class="i===0 ? 'pt-4' : i === series.metadata.alternateTitles.length - 1 ? 'pb-4' : ''">{{ a.label }}</v-col>
<v-col cols="8" sm="9" md="10" xl="11" class="py-0" :class="i===0 ? 'pt-4' : i === series.metadata.alternateTitles.length - 1 ? 'pb-4' : ''">{{ a.title }}</v-col>
</v-row>
</read-more>
<v-row class="align-center">
<v-col cols="auto">
<v-btn :title="$t('menu.download_series')"
@ -203,6 +217,17 @@
</v-row>
<template v-if="$vuetify.breakpoint.xsOnly">
<!-- Alternate titles -->
<read-more class="mb-4" i18n-less="titles_more.less" i18n-more="titles_more.more">
<v-row v-for="(a, i) in series.metadata.alternateTitles"
:key="i"
class="align-center text-caption"
>
<v-col cols="4" class="py-0 text-uppercase" :class="i===0 ? 'pt-4' : i === series.metadata.alternateTitles.length - 1 ? 'pb-4' : ''">{{ a.label }}</v-col>
<v-col cols="8" class="py-0" :class="i===0 ? 'pt-4' : i === series.metadata.alternateTitles.length - 1 ? 'pb-4' : ''">{{ a.title }}</v-col>
</v-row>
</read-more>
<!-- Download button -->
<v-row class="align-center">
<v-col cols="auto">
@ -529,9 +554,9 @@ export default Vue.extend({
},
computed: {
itemContext(): ItemContext[] {
if(this.sortActive.key === 'metadata.releaseDate') return [ItemContext.RELEASE_DATE]
if(this.sortActive.key === 'createdDate') return [ItemContext.DATE_ADDED]
if(this.sortActive.key === 'fileSize') return [ItemContext.FILE_SIZE]
if (this.sortActive.key === 'metadata.releaseDate') return [ItemContext.RELEASE_DATE]
if (this.sortActive.key === 'createdDate') return [ItemContext.DATE_ADDED]
if (this.sortActive.key === 'fileSize') return [ItemContext.FILE_SIZE]
return []
},
sortOptions(): SortOption[] {

View file

@ -0,0 +1,13 @@
CREATE TABLE SERIES_METADATA_ALTERNATE_TITLE
(
LABEL varchar NOT NULL,
TITLE varchar NOT NULL,
SERIES_ID varchar NOT NULL,
FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID)
);
alter table series_metadata
add column ALTERNATE_TITLES_LOCK boolean NOT NULL DEFAULT 0;
create index idx__series_metadata_alternate_title__series_id
on SERIES_METADATA_ALTERNATE_TITLE (SERIES_ID);

View file

@ -0,0 +1,6 @@
package org.gotson.komga.domain.model
data class AlternateTitle(
val label: String,
val title: String,
)

View file

@ -17,6 +17,7 @@ class SeriesMetadata(
val totalBookCount: Int? = null,
sharingLabels: Set<String> = emptySet(),
val links: List<WebLink> = emptyList(),
val alternateTitles: List<AlternateTitle> = emptyList(),
val statusLock: Boolean = false,
val titleLock: Boolean = false,
@ -31,6 +32,7 @@ class SeriesMetadata(
val totalBookCountLock: Boolean = false,
val sharingLabelsLock: Boolean = false,
val linksLock: Boolean = false,
val alternateTitlesLock: Boolean = false,
val seriesId: String = "",
@ -60,6 +62,7 @@ class SeriesMetadata(
totalBookCount: Int? = this.totalBookCount,
sharingLabels: Set<String> = this.sharingLabels,
links: List<WebLink> = this.links,
alternateTitles: List<AlternateTitle> = this.alternateTitles,
statusLock: Boolean = this.statusLock,
titleLock: Boolean = this.titleLock,
titleSortLock: Boolean = this.titleSortLock,
@ -73,6 +76,7 @@ class SeriesMetadata(
totalBookCountLock: Boolean = this.totalBookCountLock,
sharingLabelsLock: Boolean = this.sharingLabelsLock,
linksLock: Boolean = this.linksLock,
alternateTitlesLock: Boolean = this.alternateTitlesLock,
seriesId: String = this.seriesId,
createdDate: LocalDateTime = this.createdDate,
lastModifiedDate: LocalDateTime = this.lastModifiedDate,
@ -91,6 +95,7 @@ class SeriesMetadata(
totalBookCount = totalBookCount,
sharingLabels = sharingLabels,
links = links,
alternateTitles = alternateTitles,
statusLock = statusLock,
titleLock = titleLock,
titleSortLock = titleSortLock,
@ -104,6 +109,7 @@ class SeriesMetadata(
totalBookCountLock = totalBookCountLock,
sharingLabelsLock = sharingLabelsLock,
linksLock = linksLock,
alternateTitlesLock = alternateTitlesLock,
seriesId = seriesId,
createdDate = createdDate,
lastModifiedDate = lastModifiedDate,
@ -121,5 +127,5 @@ class SeriesMetadata(
}
override fun toString(): String =
"SeriesMetadata(status=$status, readingDirection=$readingDirection, ageRating=$ageRating, totalBookCount=$totalBookCount, links=$links, statusLock=$statusLock, titleLock=$titleLock, titleSortLock=$titleSortLock, summaryLock=$summaryLock, readingDirectionLock=$readingDirectionLock, publisherLock=$publisherLock, ageRatingLock=$ageRatingLock, languageLock=$languageLock, genresLock=$genresLock, tagsLock=$tagsLock, totalBookCountLock=$totalBookCountLock, sharingLabelsLock=$sharingLabelsLock, linksLock=$linksLock, seriesId='$seriesId', createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', titleSort='$titleSort', summary='$summary', publisher='$publisher', language='$language', tags=$tags, genres=$genres, sharingLabels=$sharingLabels)"
"SeriesMetadata(status=$status, readingDirection=$readingDirection, ageRating=$ageRating, totalBookCount=$totalBookCount, links=$links, alternateTitles=$alternateTitles, statusLock=$statusLock, titleLock=$titleLock, titleSortLock=$titleSortLock, summaryLock=$summaryLock, readingDirectionLock=$readingDirectionLock, publisherLock=$publisherLock, ageRatingLock=$ageRatingLock, languageLock=$languageLock, genresLock=$genresLock, tagsLock=$tagsLock, totalBookCountLock=$totalBookCountLock, sharingLabelsLock=$sharingLabelsLock, linksLock=$linksLock, alternateTitlesLock=$alternateTitlesLock, seriesId='$seriesId', createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', titleSort='$titleSort', summary='$summary', publisher='$publisher', language='$language', tags=$tags, genres=$genres, sharingLabels=$sharingLabels)"
}

View file

@ -10,6 +10,7 @@ import org.gotson.komga.infrastructure.search.LuceneEntity
import org.gotson.komga.infrastructure.search.LuceneHelper
import org.gotson.komga.infrastructure.web.toFilePath
import org.gotson.komga.interfaces.api.persistence.SeriesDtoRepository
import org.gotson.komga.interfaces.api.rest.dto.AlternateTitleDto
import org.gotson.komga.interfaces.api.rest.dto.AuthorDto
import org.gotson.komga.interfaces.api.rest.dto.BookMetadataAggregationDto
import org.gotson.komga.interfaces.api.rest.dto.GroupCountDto
@ -62,6 +63,7 @@ class SeriesDtoDao(
private val st = Tables.SERIES_METADATA_TAG
private val sl = Tables.SERIES_METADATA_SHARING
private val slk = Tables.SERIES_METADATA_LINK
private val sat = Tables.SERIES_METADATA_ALTERNATE_TITLE
private val bma = Tables.BOOK_METADATA_AGGREGATION
private val bmaa = Tables.BOOK_METADATA_AGGREGATION_AUTHOR
private val bmat = Tables.BOOK_METADATA_AGGREGATION_TAG
@ -230,6 +232,7 @@ class SeriesDtoDao(
lateinit var tags: Map<String, List<String>>
lateinit var sharingLabels: Map<String, List<String>>
lateinit var links: Map<String, List<WebLinkDto>>
lateinit var alternateTitles: Map<String, List<AlternateTitleDto>>
lateinit var aggregatedAuthors: Map<String, List<AuthorDto>>
lateinit var aggregatedTags: Map<String, List<String>>
transactionTemplate.executeWithoutResult {
@ -250,6 +253,10 @@ class SeriesDtoDao(
.where(slk.SERIES_ID.`in`(dsl.selectTempStrings()))
.groupBy({ it.seriesId }, { WebLinkDto(it.label, it.url) })
alternateTitles = dsl.selectFrom(sat)
.where(sat.SERIES_ID.`in`(dsl.selectTempStrings()))
.groupBy({ it.seriesId }, { AlternateTitleDto(it.label, it.title) })
aggregatedAuthors = dsl.selectFrom(bmaa)
.where(bmaa.SERIES_ID.`in`(dsl.selectTempStrings()))
.filter { it.name != null }
@ -275,7 +282,7 @@ class SeriesDtoDao(
booksReadCount,
booksUnreadCount,
booksInProgressCount,
dr.toDto(genres[sr.id].orEmpty().toSet(), tags[sr.id].orEmpty().toSet(), sharingLabels[sr.id].orEmpty().toSet(), links[sr.id].orEmpty()),
dr.toDto(genres[sr.id].orEmpty().toSet(), tags[sr.id].orEmpty().toSet(), sharingLabels[sr.id].orEmpty().toSet(), links[sr.id].orEmpty(), alternateTitles[sr.id].orEmpty()),
bmar.toDto(aggregatedAuthors[sr.id].orEmpty(), aggregatedTags[sr.id].orEmpty().toSet()),
)
}
@ -372,7 +379,7 @@ class SeriesDtoDao(
deleted = deletedDate != null,
)
private fun SeriesMetadataRecord.toDto(genres: Set<String>, tags: Set<String>, sharingLabels: Set<String>, links: List<WebLinkDto>) =
private fun SeriesMetadataRecord.toDto(genres: Set<String>, tags: Set<String>, sharingLabels: Set<String>, links: List<WebLinkDto>, alternateTitles: List<AlternateTitleDto>) =
SeriesMetadataDto(
status = status,
statusLock = statusLock,
@ -402,6 +409,8 @@ class SeriesDtoDao(
sharingLabelsLock = sharingLabelsLock,
links = links,
linksLock = linksLock,
alternateTitles = alternateTitles,
alternateTitlesLock = alternateTitlesLock,
)
private fun BookMetadataAggregationRecord.toDto(authors: List<AuthorDto>, tags: Set<String>) =

View file

@ -1,5 +1,6 @@
package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.AlternateTitle
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.WebLink
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
@ -24,12 +25,13 @@ class SeriesMetadataDao(
private val st = Tables.SERIES_METADATA_TAG
private val sl = Tables.SERIES_METADATA_SHARING
private val slk = Tables.SERIES_METADATA_LINK
private val sat = Tables.SERIES_METADATA_ALTERNATE_TITLE
override fun findById(seriesId: String): SeriesMetadata =
findOne(seriesId)!!.toDomain(findGenres(seriesId), findTags(seriesId), findSharingLabels(seriesId), findLinks(seriesId))
findOne(seriesId)!!.toDomain(findGenres(seriesId), findTags(seriesId), findSharingLabels(seriesId), findLinks(seriesId), findAlternateTitles(seriesId))
override fun findByIdOrNull(seriesId: String): SeriesMetadata? =
findOne(seriesId)?.toDomain(findGenres(seriesId), findTags(seriesId), findSharingLabels(seriesId), findLinks(seriesId))
findOne(seriesId)?.toDomain(findGenres(seriesId), findTags(seriesId), findSharingLabels(seriesId), findLinks(seriesId), findAlternateTitles(seriesId))
private fun findOne(seriesId: String) =
dsl.selectFrom(d)
@ -53,6 +55,7 @@ class SeriesMetadataDao(
.from(sl)
.where(sl.SERIES_ID.eq(seriesId))
.fetchSet(sl.LABEL)
private fun findLinks(seriesId: String) =
dsl.select(slk.LABEL, slk.URL)
.from(slk)
@ -60,6 +63,13 @@ class SeriesMetadataDao(
.fetchInto(slk)
.map { WebLink(it.label, URI(it.url)) }
private fun findAlternateTitles(seriesId: String) =
dsl.select(sat.LABEL, sat.TITLE)
.from(sat)
.where(sat.SERIES_ID.eq(seriesId))
.fetchInto(sat)
.map { AlternateTitle(it.label, it.title) }
@Transactional
override fun insert(metadata: SeriesMetadata) {
dsl.insertInto(d)
@ -86,12 +96,14 @@ class SeriesMetadataDao(
.set(d.TOTAL_BOOK_COUNT_LOCK, metadata.totalBookCountLock)
.set(d.SHARING_LABELS_LOCK, metadata.sharingLabelsLock)
.set(d.LINKS_LOCK, metadata.linksLock)
.set(d.ALTERNATE_TITLES_LOCK, metadata.alternateTitlesLock)
.execute()
insertGenres(metadata)
insertTags(metadata)
insertSharingLabels(metadata)
insertLinks(metadata)
insertAlternateTitles(metadata)
}
@Transactional
@ -119,6 +131,7 @@ class SeriesMetadataDao(
.set(d.TOTAL_BOOK_COUNT_LOCK, metadata.totalBookCountLock)
.set(d.SHARING_LABELS_LOCK, metadata.sharingLabelsLock)
.set(d.LINKS_LOCK, metadata.linksLock)
.set(d.ALTERNATE_TITLES_LOCK, metadata.alternateTitlesLock)
.set(d.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.where(d.SERIES_ID.eq(metadata.seriesId))
.execute()
@ -139,10 +152,15 @@ class SeriesMetadataDao(
.where(slk.SERIES_ID.eq(metadata.seriesId))
.execute()
dsl.deleteFrom(sat)
.where(sat.SERIES_ID.eq(metadata.seriesId))
.execute()
insertGenres(metadata)
insertTags(metadata)
insertSharingLabels(metadata)
insertLinks(metadata)
insertAlternateTitles(metadata)
}
private fun insertGenres(metadata: SeriesMetadata) {
@ -205,12 +223,28 @@ class SeriesMetadataDao(
}
}
private fun insertAlternateTitles(metadata: SeriesMetadata) {
if (metadata.alternateTitles.isNotEmpty()) {
metadata.alternateTitles.chunked(batchSize).forEach { chunk ->
dsl.batch(
dsl.insertInto(sat, sat.SERIES_ID, sat.LABEL, sat.TITLE)
.values(null as String?, null, null),
).also { step ->
chunk.forEach {
step.bind(metadata.seriesId, it.label, it.title)
}
}.execute()
}
}
}
@Transactional
override fun delete(seriesId: String) {
dsl.deleteFrom(g).where(g.SERIES_ID.eq(seriesId)).execute()
dsl.deleteFrom(st).where(st.SERIES_ID.eq(seriesId)).execute()
dsl.deleteFrom(sl).where(sl.SERIES_ID.eq(seriesId)).execute()
dsl.deleteFrom(slk).where(slk.SERIES_ID.eq(seriesId)).execute()
dsl.deleteFrom(sat).where(sat.SERIES_ID.eq(seriesId)).execute()
dsl.deleteFrom(d).where(d.SERIES_ID.eq(seriesId)).execute()
}
@ -222,12 +256,13 @@ class SeriesMetadataDao(
dsl.deleteFrom(st).where(st.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
dsl.deleteFrom(sl).where(sl.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
dsl.deleteFrom(slk).where(slk.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
dsl.deleteFrom(sat).where(sat.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
dsl.deleteFrom(d).where(d.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
}
override fun count(): Long = dsl.fetchCount(d).toLong()
private fun SeriesMetadataRecord.toDomain(genres: Set<String>, tags: Set<String>, sharingLabels: Set<String>, links: List<WebLink>) =
private fun SeriesMetadataRecord.toDomain(genres: Set<String>, tags: Set<String>, sharingLabels: Set<String>, links: List<WebLink>, alternateTitles: List<AlternateTitle>) =
SeriesMetadata(
status = SeriesMetadata.Status.valueOf(status),
title = title,
@ -244,6 +279,7 @@ class SeriesMetadataDao(
totalBookCount = totalBookCount,
sharingLabels = sharingLabels,
links = links,
alternateTitles = alternateTitles,
statusLock = statusLock,
titleLock = titleLock,
@ -258,6 +294,7 @@ class SeriesMetadataDao(
totalBookCountLock = totalBookCountLock,
sharingLabelsLock = sharingLabelsLock,
linksLock = linksLock,
alternateTitlesLock = alternateTitlesLock,
seriesId = seriesId,

View file

@ -45,6 +45,7 @@ fun SeriesDto.toDocument() =
Document().apply {
add(TextField("title", metadata.title, Field.Store.NO))
if (metadata.titleSort != metadata.title) add(TextField("title", metadata.titleSort, Field.Store.NO))
metadata.alternateTitles.forEach { add(TextField("title", it.title, Field.Store.NO)) }
add(TextField("publisher", metadata.publisher, Field.Store.NO))
add(TextField("status", metadata.status, Field.Store.NO))
add(TextField("reading_direction", metadata.readingDirection, Field.Store.NO))

View file

@ -16,6 +16,7 @@ import org.gotson.komga.application.events.EventPublisher
import org.gotson.komga.application.tasks.HIGHEST_PRIORITY
import org.gotson.komga.application.tasks.HIGH_PRIORITY
import org.gotson.komga.application.tasks.TaskEmitter
import org.gotson.komga.domain.model.AlternateTitle
import org.gotson.komga.domain.model.Author
import org.gotson.komga.domain.model.BookSearchWithReadProgress
import org.gotson.komga.domain.model.DomainEvent
@ -550,6 +551,10 @@ class SeriesController(
if (links != null) links!!.map { WebLink(it.label!!, URI(it.url!!)) } else emptyList()
} else existing.links,
linksLock = linksLock ?: existing.linksLock,
alternateTitles = if (isSet("alternateTitles")) {
if (alternateTitles != null) alternateTitles!!.map { AlternateTitle(it.label!!, it.title!!) } else emptyList()
} else existing.alternateTitles,
alternateTitlesLock = alternateTitlesLock ?: existing.alternateTitlesLock,
)
}
seriesMetadataRepository.update(updated)

View file

@ -0,0 +1,10 @@
package org.gotson.komga.interfaces.api.rest.dto
import org.gotson.komga.domain.model.AlternateTitle
data class AlternateTitleDto(
val label: String,
val title: String,
)
fun AlternateTitle.toDto() = AlternateTitleDto(label, title)

View file

@ -0,0 +1,11 @@
package org.gotson.komga.interfaces.api.rest.dto
import javax.validation.constraints.NotBlank
class AlternateTitleUpdateDto {
@get:NotBlank
val label: String? = null
@get:NotBlank
val title: String? = null
}

View file

@ -54,6 +54,8 @@ data class SeriesMetadataDto(
val sharingLabelsLock: Boolean,
val links: List<WebLinkDto>,
val linksLock: Boolean,
val alternateTitles: List<AlternateTitleDto>,
val alternateTitlesLock: Boolean,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val created: LocalDateTime,

View file

@ -90,4 +90,12 @@ class SeriesMetadataUpdateDto {
}
var linksLock: Boolean? = null
@get:Valid
var alternateTitles: List<AlternateTitleUpdateDto>?
by Delegates.observable(null) { prop, _, _ ->
isSet[prop.name] = true
}
var alternateTitlesLock: Boolean? = null
}

View file

@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.catchThrowable
import org.gotson.komga.domain.model.AlternateTitle
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.WebLink
import org.gotson.komga.domain.model.makeLibrary
@ -66,6 +67,7 @@ class SeriesMetadataDaoTest(
totalBookCount = 5,
sharingLabels = setOf("kids"),
links = listOf(WebLink("Comicvine", URI("https://comicvine.gamespot.com/doctor-strange/4050-2676/"))),
alternateTitles = listOf(AlternateTitle("fr", "La Series")),
titleLock = true,
titleSortLock = true,
summaryLock = true,
@ -78,6 +80,7 @@ class SeriesMetadataDaoTest(
totalBookCountLock = true,
sharingLabelsLock = true,
linksLock = true,
alternateTitlesLock = true,
seriesId = series.id,
)
@ -104,6 +107,10 @@ class SeriesMetadataDaoTest(
assertThat(label).isEqualTo(metadata.links.first().label)
assertThat(url).isEqualTo(metadata.links.first().url)
}
with(created.alternateTitles.first()) {
assertThat(label).isEqualTo(metadata.alternateTitles.first().label)
assertThat(title).isEqualTo(metadata.alternateTitles.first().title)
}
assertThat(created.titleLock).isEqualTo(metadata.titleLock)
assertThat(created.titleSortLock).isEqualTo(metadata.titleSortLock)
@ -118,6 +125,7 @@ class SeriesMetadataDaoTest(
assertThat(created.totalBookCountLock).isEqualTo(metadata.totalBookCountLock)
assertThat(created.sharingLabelsLock).isEqualTo(metadata.sharingLabelsLock)
assertThat(created.linksLock).isEqualTo(metadata.linksLock)
assertThat(created.alternateTitlesLock).isEqualTo(metadata.alternateTitlesLock)
}
@Test
@ -150,6 +158,7 @@ class SeriesMetadataDaoTest(
assertThat(created.totalBookCount).isNull()
assertThat(created.sharingLabels).isEmpty()
assertThat(created.links).isEmpty()
assertThat(created.alternateTitles).isEmpty()
assertThat(created.titleLock).isFalse
assertThat(created.titleSortLock).isFalse
@ -164,6 +173,7 @@ class SeriesMetadataDaoTest(
assertThat(created.totalBookCountLock).isFalse
assertThat(created.sharingLabelsLock).isFalse
assertThat(created.linksLock).isFalse
assertThat(created.alternateTitlesLock).isFalse
}
@Test
@ -217,6 +227,7 @@ class SeriesMetadataDaoTest(
totalBookCount = 3,
sharingLabels = setOf("kids"),
links = listOf(WebLink("Comicvine", URI("https://comicvine.gamespot.com/doctor-strange/4050-2676/"))),
alternateTitles = listOf(AlternateTitle("fr", "La Series")),
seriesId = series.id,
)
seriesMetadataDao.insert(metadata)
@ -239,6 +250,7 @@ class SeriesMetadataDaoTest(
totalBookCount = 8,
sharingLabels = setOf("adult"),
links = emptyList(),
alternateTitles = emptyList(),
statusLock = true,
titleLock = true,
titleSortLock = true,
@ -252,6 +264,7 @@ class SeriesMetadataDaoTest(
totalBookCountLock = true,
sharingLabelsLock = true,
linksLock = true,
alternateTitlesLock = true,
)
}
@ -276,6 +289,7 @@ class SeriesMetadataDaoTest(
assertThat(modified.totalBookCount).isEqualTo(updated.totalBookCount)
assertThat(modified.sharingLabels).containsAll(updated.sharingLabels)
assertThat(modified.links).isEmpty()
assertThat(modified.alternateTitles).isEmpty()
assertThat(modified.titleLock).isTrue
assertThat(modified.titleSortLock).isTrue
@ -290,5 +304,6 @@ class SeriesMetadataDaoTest(
assertThat(modified.totalBookCountLock).isTrue
assertThat(modified.sharingLabelsLock).isTrue
assertThat(modified.linksLock).isTrue
assertThat(modified.alternateTitlesLock).isTrue
}
}