fix: restore user uploaded thumbnail when restoring deleted books and series

This commit is contained in:
Gauthier Roebroeck 2024-11-18 11:13:40 +08:00
parent 25a1cfa866
commit 812f82207a
7 changed files with 58 additions and 11 deletions

View file

@ -13,7 +13,7 @@ interface ThumbnailBookRepository {
fun findAllByBookIdAndType(
bookId: String,
type: ThumbnailBook.Type,
type: Set<ThumbnailBook.Type>,
): Collection<ThumbnailBook>
fun findAllWithoutMetadata(pageable: Pageable): Page<ThumbnailBook>

View file

@ -20,6 +20,8 @@ interface ThumbnailSeriesRepository {
fun insert(thumbnail: ThumbnailSeries)
fun update(thumbnail: ThumbnailSeries)
fun updateMetadata(thumbnails: Collection<ThumbnailSeries>)
fun markSelected(thumbnail: ThumbnailSeries)

View file

@ -143,7 +143,7 @@ class BookLifecycle(
ThumbnailBook.Type.SIDECAR -> {
// delete existing thumbnail with the same url
thumbnailBookRepository.findAllByBookIdAndType(thumbnail.bookId, ThumbnailBook.Type.SIDECAR)
thumbnailBookRepository.findAllByBookIdAndType(thumbnail.bookId, setOf(ThumbnailBook.Type.SIDECAR))
.filter { it.url == thumbnail.url }
.forEach {
thumbnailBookRepository.delete(it.id)
@ -517,7 +517,7 @@ class BookLifecycle(
if (!book.path.isWritable()) return logger.info { "Cannot delete book file, path is not writable: ${book.path}" }
val thumbnails =
thumbnailBookRepository.findAllByBookIdAndType(book.id, ThumbnailBook.Type.SIDECAR)
thumbnailBookRepository.findAllByBookIdAndType(book.id, setOf(ThumbnailBook.Type.SIDECAR))
.mapNotNull { it.url?.toURI()?.toPath() }
.filter { it.exists() && it.isWritable() }

View file

@ -13,6 +13,7 @@ import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.SeriesSearch
import org.gotson.komga.domain.model.Sidecar
import org.gotson.komga.domain.model.ThumbnailBook
import org.gotson.komga.domain.model.ThumbnailSeries
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
@ -24,6 +25,7 @@ import org.gotson.komga.domain.persistence.SeriesMetadataRepository
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.domain.persistence.ThumbnailSeriesRepository
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.infrastructure.hash.Hasher
import org.gotson.komga.language.notEquals
@ -60,6 +62,7 @@ class LibraryContentLifecycle(
private val collectionRepository: SeriesCollectionRepository,
private val thumbnailBookRepository: ThumbnailBookRepository,
private val eventPublisher: ApplicationEventPublisher,
private val thumbnailSeriesRepository: ThumbnailSeriesRepository,
) {
fun scanRootFolder(
library: Library,
@ -313,6 +316,11 @@ class LibraryContentLifecycle(
)
}
// copy user uploaded thumbnails
thumbnailSeriesRepository.findAllBySeriesIdIdAndType(match.first.id, ThumbnailSeries.Type.USER_UPLOADED).forEach { deleted ->
thumbnailSeriesRepository.update(deleted.copy(seriesId = newSeries.id))
}
// replace deleted series by new series in collections
collectionRepository.findAllContainingSeriesId(match.first.id, filterOnLibraryIds = null)
.forEach { col ->
@ -368,8 +376,8 @@ class LibraryContentLifecycle(
mediaRepository.update(deleted.copy(bookId = bookToAdd.id))
}
// copy generated thumbnails
thumbnailBookRepository.findAllByBookIdAndType(match.id, ThumbnailBook.Type.GENERATED).forEach { deleted ->
// copy generated and user uploaded thumbnails
thumbnailBookRepository.findAllByBookIdAndType(match.id, setOf(ThumbnailBook.Type.GENERATED, ThumbnailBook.Type.USER_UPLOADED)).forEach { deleted ->
thumbnailBookRepository.update(deleted.copy(bookId = bookToAdd.id))
}

View file

@ -31,11 +31,11 @@ class ThumbnailBookDao(
override fun findAllByBookIdAndType(
bookId: String,
type: ThumbnailBook.Type,
type: Set<ThumbnailBook.Type>,
): Collection<ThumbnailBook> =
dsl.selectFrom(tb)
.where(tb.BOOK_ID.eq(bookId))
.and(tb.TYPE.eq(type.toString()))
.and(tb.TYPE.`in`(type.map { it.name }))
.fetchInto(tb)
.map { it.toDomain() }

View file

@ -87,6 +87,21 @@ class ThumbnailSeriesDao(
.execute()
}
override fun update(thumbnail: ThumbnailSeries) {
dsl.update(ts)
.set(ts.SERIES_ID, thumbnail.seriesId)
.set(ts.THUMBNAIL, thumbnail.thumbnail)
.set(ts.URL, thumbnail.url?.toString())
.set(ts.SELECTED, thumbnail.selected)
.set(ts.TYPE, thumbnail.type.toString())
.set(ts.MEDIA_TYPE, thumbnail.mediaType)
.set(ts.WIDTH, thumbnail.dimension.width)
.set(ts.HEIGHT, thumbnail.dimension.height)
.set(ts.FILE_SIZE, thumbnail.fileSize)
.where(ts.ID.eq(thumbnail.id))
.execute()
}
override fun updateMetadata(thumbnails: Collection<ThumbnailSeries>) {
dsl.batched { c ->
thumbnails.forEach {

View file

@ -20,6 +20,7 @@ import org.gotson.komga.domain.model.ReadList
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.SeriesCollection
import org.gotson.komga.domain.model.ThumbnailBook
import org.gotson.komga.domain.model.ThumbnailSeries
import org.gotson.komga.domain.model.makeBook
import org.gotson.komga.domain.model.makeBookPage
import org.gotson.komga.domain.model.makeLibrary
@ -566,6 +567,8 @@ class LibraryContentLifecycleTest(
bookRepository.findByIdOrNull(book2.id)?.let {
bookRepository.update(it.copy(fileHash = "sameHash"))
mediaRepository.update(mediaRepository.findById(it.id).copy(status = Media.Status.READY))
bookMetadataRepository.update(bookMetadataRepository.findById(it.id).copy(tags = setOf("my-tag")))
bookLifecycle.addThumbnailForBook(ThumbnailBook(ByteArray(10), type = ThumbnailBook.Type.USER_UPLOADED, mediaType = "image/jpeg", fileSize = 10L, dimension = Dimension(1, 1), bookId = it.id), MarkSelectedPreference.YES)
}
every { mockHasher.computeHash(any<Path>()) } returns "sameHash"
@ -587,6 +590,11 @@ class LibraryContentLifecycleTest(
with(allBooks.last()) {
assertThat(mediaRepository.findById(id).status).`as` { "Book media should be kept intact" }.isEqualTo(Media.Status.READY)
assertThat(bookMetadataRepository.findById(id).tags).containsExactlyInAnyOrder("my-tag")
val thumbnail = bookLifecycle.getThumbnail(id)
assertThat(thumbnail).isNotNull
assertThat(thumbnail!!.type).isEqualTo(ThumbnailBook.Type.USER_UPLOADED)
assertThat(thumbnail.fileSize).isEqualTo(10L)
}
}
@ -609,6 +617,11 @@ class LibraryContentLifecycleTest(
mediaRepository.findById(book.id).let { mediaRepository.update(it.copy(status = Media.Status.READY)) }
}
seriesRepository.findAll().forEach { series ->
seriesMetadataRepository.findById(series.id).let { seriesMetadataRepository.update(it.copy(language = "en")) }
seriesLifecycle.addThumbnailForSeries(ThumbnailSeries(ByteArray(10), type = ThumbnailSeries.Type.USER_UPLOADED, mediaType = "image/jpeg", fileSize = 10L, dimension = Dimension(1, 1), seriesId = series.id), MarkSelectedPreference.YES)
}
val slot = slot<Path>()
every { mockHasher.computeHash(capture(slot)) } answers {
"HASH-${slot.captured.nameWithoutExtension}"
@ -627,6 +640,15 @@ class LibraryContentLifecycleTest(
assertThat(allSeries.map { it.deletedDate }).containsOnlyNulls()
assertThat(allSeries).hasSize(1)
allSeries.forEach { series ->
assertThat(seriesMetadataRepository.findById(series.id).language).isEqualTo("en")
val thumbnail = seriesLifecycle.getSelectedThumbnail(series.id)
assertThat(thumbnail).isNotNull
assertThat(thumbnail!!.type).isEqualTo(ThumbnailSeries.Type.USER_UPLOADED)
assertThat(thumbnail.fileSize).isEqualTo(10L)
}
assertThat(allBooks.map { it.deletedDate }).containsOnlyNulls()
assertThat(allBooks).hasSize(2)
@ -678,8 +700,8 @@ class LibraryContentLifecycleTest(
with(allBooks.last()) {
assertThat(name).`as` { "Book name should have changed to match the filename" }.isEqualTo("book3")
assertThat(mediaRepository.findById(id).status).`as` { "Book media should be kept intact" }.isEqualTo(Media.Status.READY)
assertThat(thumbnailBookRepository.findAllByBookIdAndType(id, ThumbnailBook.Type.SIDECAR)).hasSize(0)
assertThat(thumbnailBookRepository.findAllByBookIdAndType(id, ThumbnailBook.Type.GENERATED)).hasSize(1)
assertThat(thumbnailBookRepository.findAllByBookIdAndType(id, setOf(ThumbnailBook.Type.SIDECAR))).hasSize(0)
assertThat(thumbnailBookRepository.findAllByBookIdAndType(id, setOf(ThumbnailBook.Type.GENERATED))).hasSize(1)
}
}
@ -724,8 +746,8 @@ class LibraryContentLifecycleTest(
with(allBooks.last()) {
assertThat(name).`as` { "Book name should have changed to match the filename" }.isEqualTo("book3")
assertThat(mediaRepository.findById(id).status).`as` { "Book media should be kept intact" }.isEqualTo(Media.Status.READY)
assertThat(thumbnailBookRepository.findAllByBookIdAndType(id, ThumbnailBook.Type.SIDECAR)).hasSize(0)
assertThat(thumbnailBookRepository.findAllByBookIdAndType(id, ThumbnailBook.Type.GENERATED)).hasSize(1)
assertThat(thumbnailBookRepository.findAllByBookIdAndType(id, setOf(ThumbnailBook.Type.SIDECAR))).hasSize(0)
assertThat(thumbnailBookRepository.findAllByBookIdAndType(id, setOf(ThumbnailBook.Type.GENERATED))).hasSize(1)
}
}