From 51416c537b01ae24f214394298f1fc14928d071e Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Tue, 14 Jan 2025 10:00:01 +0800 Subject: [PATCH] feat: allow book import for one-shots --- komga-webui/src/components/FileImportRow.vue | 18 ++++++--- komga-webui/src/types/series-slim.ts | 5 +++ .../komga/domain/service/BookImporter.kt | 39 ++++++++++++------- 3 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 komga-webui/src/types/series-slim.ts diff --git a/komga-webui/src/components/FileImportRow.vue b/komga-webui/src/components/FileImportRow.vue index 3e455dea4bc..bbe5fdf58f6 100644 --- a/komga-webui/src/components/FileImportRow.vue +++ b/komga-webui/src/components/FileImportRow.vue @@ -31,17 +31,19 @@ - + - + {{ $t('common.oneshot') }} @@ -129,7 +131,7 @@ import TransientBookViewerDialog from '@/components/dialogs/TransientBookViewerD import {bookPageUrl, transientBookPageUrl} from '@/functions/urls' import {convertErrorCodes} from '@/functions/error-codes' import FileNameChooserDialog from '@/components/dialogs/FileNameChooserDialog.vue' -import {ReadListRequestBookMatchSeriesDto} from '@/types/komga-readlists' +import {SeriesSelected} from '@/types/series-slim' export default Vue.extend({ name: 'FileImportRow', @@ -167,6 +169,7 @@ export default Vue.extend({ if (val) this.selectedSeries = { seriesId: val.id, title: val.metadata.title, + oneshot: val.oneshot, } }, immediate: true, @@ -189,7 +192,7 @@ export default Vue.extend({ convertErrorCodes, innerSelect: false, bookAnalyzed: undefined as unknown as TransientBookDto, - selectedSeries: undefined as ReadListRequestBookMatchSeriesDto | undefined, + selectedSeries: undefined as SeriesSelected | undefined, seriesBooks: [] as BookDto[], bookToUpgrade: undefined as BookDto | undefined, bookToUpgradePages: [] as PageDto[], @@ -253,12 +256,16 @@ export default Vue.extend({ this.selectedSeries = { seriesId: seriesDto.id, title: seriesDto.metadata.title, + oneshot: seriesDto.oneshot, } } }, - async getSeriesBooks(series: ReadListRequestBookMatchSeriesDto) { + async getSeriesBooks(series: SeriesSelected) { if (series) { this.seriesBooks = (await this.$komgaSeries.getBooks(series.seriesId, {unpaged: true})).content + if (series.oneshot) { + this.bookNumber = this.seriesBooks[0].metadata.numberSort + } this.checkForUpgrade(this.bookNumber) } }, @@ -271,6 +278,7 @@ export default Vue.extend({ this.selectedSeries = { seriesId: series.id, title: series.metadata.title, + oneshot: series.oneshot, } }, }, diff --git a/komga-webui/src/types/series-slim.ts b/komga-webui/src/types/series-slim.ts new file mode 100644 index 00000000000..b2784d52cfe --- /dev/null +++ b/komga-webui/src/types/series-slim.ts @@ -0,0 +1,5 @@ +export interface SeriesSelected { + seriesId: string, + title: string, + oneshot: boolean, +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt index 42ff75f9d2c..4b031a24569 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt @@ -20,6 +20,7 @@ import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.persistence.MediaRepository import org.gotson.komga.domain.persistence.ReadListRepository import org.gotson.komga.domain.persistence.ReadProgressRepository +import org.gotson.komga.domain.persistence.SeriesRepository import org.gotson.komga.domain.persistence.SidecarRepository import org.gotson.komga.domain.persistence.ThumbnailBookRepository import org.gotson.komga.language.toIndexedMap @@ -62,6 +63,7 @@ class BookImporter( private val eventPublisher: ApplicationEventPublisher, private val taskEmitter: TaskEmitter, private val historicalEventRepository: HistoricalEventRepository, + private val seriesRepository: SeriesRepository, ) { fun importBook( sourceFile: Path, @@ -72,14 +74,29 @@ class BookImporter( ): Book { try { if (sourceFile.notExists()) throw FileNotFoundException("File not found: $sourceFile").withCode("ERR_1018") - if (series.oneshot) throw IllegalArgumentException("Destination series is oneshot") + if (series.oneshot && upgradeBookId.isNullOrEmpty()) throw IllegalArgumentException("Destination series is oneshot but upgradeBookId is missing") libraryRepository.findAll().forEach { library -> if (sourceFile.startsWith(library.path)) throw PathContainedInPath("Cannot import file that is part of an existing library", "ERR_1019") } + val bookToUpgrade = + if (upgradeBookId != null) { + bookRepository.findByIdOrNull(upgradeBookId)?.also { + if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series").withCode("ERR_1020") + } + } else { + null + } + + val destDir = + if (series.oneshot) + series.path.parent + else + series.path + val destFile = - series.path.resolve( + destDir.resolve( if (destinationName != null) Paths.get("$destinationName.${sourceFile.extension}").name else @@ -87,7 +104,7 @@ class BookImporter( ) val sidecars = fileSystemScanner.scanBookSidecars(sourceFile).associateWith { - series.path.resolve( + destDir.resolve( if (destinationName != null) it.url .toURI() @@ -102,15 +119,6 @@ class BookImporter( ) } - val bookToUpgrade = - if (upgradeBookId != null) { - bookRepository.findByIdOrNull(upgradeBookId)?.also { - if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series").withCode("ERR_1020") - } - } else { - null - } - var deletedUpgradedFile = false when { bookToUpgrade?.path != null && destFile == bookToUpgrade.path -> { @@ -184,7 +192,7 @@ class BookImporter( val importedBook = fileSystemScanner .scanFile(destFile) - ?.copy(libraryId = series.libraryId) + ?.copy(libraryId = series.libraryId, oneshot = series.oneshot) ?: throw IllegalStateException("Newly imported book could not be scanned: $destFile").withCode("ERR_1022") seriesLifecycle.addBooks(series, listOf(importedBook)) @@ -238,6 +246,11 @@ class BookImporter( // delete upgraded book bookLifecycle.deleteOne(bookToUpgrade) + + // update series if one-shot, so it's not marked as not found during the next scan + if (series.oneshot) { + seriesRepository.update(series.copy(url = importedBook.url, fileLastModified = importedBook.fileLastModified)) + } } seriesLifecycle.sortBooks(series)