Skip to content

Commit 31ad351

Browse files
Snd-Rgotson
andauthored
feat(api): cover upload for books, read lists and collections
Co-authored-by: Gauthier Roebroeck <[email protected]>
1 parent ff358da commit 31ad351

29 files changed

+894
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
CREATE TABLE THUMBNAIL_COLLECTION
2+
(
3+
ID varchar NOT NULL PRIMARY KEY,
4+
SELECTED boolean NOT NULL DEFAULT 0,
5+
THUMBNAIL blob NOT NULL,
6+
TYPE varchar NOT NULL,
7+
COLLECTION_ID varchar NOT NULL,
8+
CREATED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
9+
LAST_MODIFIED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
10+
FOREIGN KEY (COLLECTION_ID) REFERENCES COLLECTION (ID)
11+
);
12+
13+
CREATE TABLE THUMBNAIL_READLIST
14+
(
15+
ID varchar NOT NULL PRIMARY KEY,
16+
SELECTED boolean NOT NULL DEFAULT 0,
17+
THUMBNAIL blob NOT NULL,
18+
TYPE varchar NOT NULL,
19+
READLIST_ID varchar NOT NULL,
20+
CREATED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
21+
LAST_MODIFIED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
22+
FOREIGN KEY (READLIST_ID) REFERENCES READLIST (ID)
23+
);

Diff for: komga/src/main/kotlin/org/gotson/komga/domain/model/DomainEvent.kt

+9
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,14 @@ sealed class DomainEvent : Serializable {
3232
data class ReadProgressSeriesDeleted(val seriesId: String, val userId: String) : DomainEvent()
3333

3434
data class ThumbnailBookAdded(val thumbnail: ThumbnailBook) : DomainEvent()
35+
data class ThumbnailBookDeleted(val thumbnail: ThumbnailBook) : DomainEvent()
36+
3537
data class ThumbnailSeriesAdded(val thumbnail: ThumbnailSeries) : DomainEvent()
38+
data class ThumbnailSeriesDeleted(val thumbnail: ThumbnailSeries) : DomainEvent()
39+
40+
data class ThumbnailSeriesCollectionAdded(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
41+
data class ThumbnailSeriesCollectionDeleted(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
42+
43+
data class ThumbnailReadListAdded(val thumbnail: ThumbnailReadList) : DomainEvent()
44+
data class ThumbnailReadListDeleted(val thumbnail: ThumbnailReadList) : DomainEvent()
3645
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package org.gotson.komga.domain.model
22

33
enum class MarkSelectedPreference {
4-
NO, YES, IF_NONE_EXIST
4+
NO, YES, IF_NONE_OR_GENERATED
55
}

Diff for: komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailBook.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ data class ThumbnailBook(
2020
override val lastModifiedDate: LocalDateTime = createdDate,
2121
) : Auditable(), Serializable {
2222
enum class Type {
23-
GENERATED, SIDECAR
23+
GENERATED, SIDECAR, USER_UPLOADED
2424
}
2525

2626
override fun equals(other: Any?): Boolean {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.gotson.komga.domain.model
2+
3+
import com.github.f4b6a3.tsid.TsidCreator
4+
import java.io.Serializable
5+
import java.time.LocalDateTime
6+
7+
data class ThumbnailReadList(
8+
val thumbnail: ByteArray,
9+
val selected: Boolean = false,
10+
val type: Type,
11+
12+
val id: String = TsidCreator.getTsid256().toString(),
13+
val readListId: String = "",
14+
15+
override val createdDate: LocalDateTime = LocalDateTime.now(),
16+
override val lastModifiedDate: LocalDateTime = createdDate,
17+
) : Auditable(), Serializable {
18+
enum class Type {
19+
USER_UPLOADED
20+
}
21+
22+
override fun equals(other: Any?): Boolean {
23+
if (this === other) return true
24+
if (other !is ThumbnailReadList) return false
25+
26+
if (!thumbnail.contentEquals(other.thumbnail)) return false
27+
if (selected != other.selected) return false
28+
if (type != other.type) return false
29+
if (id != other.id) return false
30+
if (readListId != other.readListId) return false
31+
if (createdDate != other.createdDate) return false
32+
if (lastModifiedDate != other.lastModifiedDate) return false
33+
34+
return true
35+
}
36+
37+
override fun hashCode(): Int {
38+
var result = thumbnail.contentHashCode()
39+
result = 31 * result + selected.hashCode()
40+
result = 31 * result + type.hashCode()
41+
result = 31 * result + id.hashCode()
42+
result = 31 * result + readListId.hashCode()
43+
result = 31 * result + createdDate.hashCode()
44+
result = 31 * result + lastModifiedDate.hashCode()
45+
return result
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.gotson.komga.domain.model
2+
3+
import com.github.f4b6a3.tsid.TsidCreator
4+
import java.io.Serializable
5+
import java.time.LocalDateTime
6+
7+
data class ThumbnailSeriesCollection(
8+
val thumbnail: ByteArray,
9+
val selected: Boolean = false,
10+
val type: Type,
11+
12+
val id: String = TsidCreator.getTsid256().toString(),
13+
val collectionId: String = "",
14+
15+
override val createdDate: LocalDateTime = LocalDateTime.now(),
16+
override val lastModifiedDate: LocalDateTime = createdDate,
17+
) : Auditable(), Serializable {
18+
enum class Type {
19+
USER_UPLOADED
20+
}
21+
22+
override fun equals(other: Any?): Boolean {
23+
if (this === other) return true
24+
if (other !is ThumbnailSeriesCollection) return false
25+
26+
if (!thumbnail.contentEquals(other.thumbnail)) return false
27+
if (selected != other.selected) return false
28+
if (type != other.type) return false
29+
if (id != other.id) return false
30+
if (collectionId != other.collectionId) return false
31+
if (createdDate != other.createdDate) return false
32+
if (lastModifiedDate != other.lastModifiedDate) return false
33+
34+
return true
35+
}
36+
37+
override fun hashCode(): Int {
38+
var result = thumbnail.contentHashCode()
39+
result = 31 * result + selected.hashCode()
40+
result = 31 * result + type.hashCode()
41+
result = 31 * result + id.hashCode()
42+
result = 31 * result + collectionId.hashCode()
43+
result = 31 * result + createdDate.hashCode()
44+
result = 31 * result + lastModifiedDate.hashCode()
45+
return result
46+
}
47+
}

Diff for: komga/src/main/kotlin/org/gotson/komga/domain/persistence/ThumbnailBookRepository.kt

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package org.gotson.komga.domain.persistence
33
import org.gotson.komga.domain.model.ThumbnailBook
44

55
interface ThumbnailBookRepository {
6+
fun findByIdOrNull(thumbnailId: String): ThumbnailBook?
7+
68
fun findSelectedByBookIdOrNull(bookId: String): ThumbnailBook?
79

810
fun findAllByBookId(bookId: String): Collection<ThumbnailBook>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.gotson.komga.domain.persistence
2+
3+
import org.gotson.komga.domain.model.ThumbnailReadList
4+
5+
interface ThumbnailReadListRepository {
6+
fun findByIdOrNull(thumbnailId: String): ThumbnailReadList?
7+
fun findSelectedByReadListIdOrNull(readListId: String): ThumbnailReadList?
8+
fun findAllByReadListId(readListId: String): Collection<ThumbnailReadList>
9+
10+
fun insert(thumbnail: ThumbnailReadList)
11+
fun update(thumbnail: ThumbnailReadList)
12+
fun markSelected(thumbnail: ThumbnailReadList)
13+
14+
fun delete(thumbnailReadListId: String)
15+
fun deleteByReadListId(readListId: String)
16+
fun deleteByReadListIds(readListIds: Collection<String>)
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.gotson.komga.domain.persistence
2+
3+
import org.gotson.komga.domain.model.ThumbnailSeriesCollection
4+
5+
interface ThumbnailSeriesCollectionRepository {
6+
fun findByIdOrNull(thumbnailId: String): ThumbnailSeriesCollection?
7+
fun findSelectedByCollectionIdOrNull(collectionId: String): ThumbnailSeriesCollection?
8+
fun findAllByCollectionId(collectionId: String): Collection<ThumbnailSeriesCollection>
9+
10+
fun insert(thumbnail: ThumbnailSeriesCollection)
11+
fun update(thumbnail: ThumbnailSeriesCollection)
12+
fun markSelected(thumbnail: ThumbnailSeriesCollection)
13+
14+
fun delete(thumbnailCollectionId: String)
15+
fun deleteByCollectionId(collectionId: String)
16+
fun deleteByCollectionIds(collectionIds: Collection<String>)
17+
}

Diff for: komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt

+42-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.gotson.komga.domain.model.BookWithMedia
88
import org.gotson.komga.domain.model.DomainEvent
99
import org.gotson.komga.domain.model.ImageConversionException
1010
import org.gotson.komga.domain.model.KomgaUser
11+
import org.gotson.komga.domain.model.MarkSelectedPreference
1112
import org.gotson.komga.domain.model.Media
1213
import org.gotson.komga.domain.model.MediaNotReadyException
1314
import org.gotson.komga.domain.model.ReadProgress
@@ -82,18 +83,18 @@ class BookLifecycle(
8283
fun generateThumbnailAndPersist(book: Book) {
8384
logger.info { "Generate thumbnail and persist for book: $book" }
8485
try {
85-
addThumbnailForBook(bookAnalyzer.generateThumbnail(BookWithMedia(book, mediaRepository.findById(book.id))))
86+
addThumbnailForBook(bookAnalyzer.generateThumbnail(BookWithMedia(book, mediaRepository.findById(book.id))), MarkSelectedPreference.IF_NONE_OR_GENERATED)
8687
} catch (ex: Exception) {
8788
logger.error(ex) { "Error while creating thumbnail" }
8889
}
8990
}
9091

91-
fun addThumbnailForBook(thumbnail: ThumbnailBook) {
92+
fun addThumbnailForBook(thumbnail: ThumbnailBook, markSelected: MarkSelectedPreference) {
9293
when (thumbnail.type) {
9394
ThumbnailBook.Type.GENERATED -> {
9495
// only one generated thumbnail is allowed
9596
thumbnailBookRepository.deleteByBookIdAndType(thumbnail.bookId, ThumbnailBook.Type.GENERATED)
96-
thumbnailBookRepository.insert(thumbnail)
97+
thumbnailBookRepository.insert(thumbnail.copy(selected = false))
9798
}
9899
ThumbnailBook.Type.SIDECAR -> {
99100
// delete existing thumbnail with the same url
@@ -102,16 +103,37 @@ class BookLifecycle(
102103
.forEach {
103104
thumbnailBookRepository.delete(it.id)
104105
}
105-
thumbnailBookRepository.insert(thumbnail)
106+
thumbnailBookRepository.insert(thumbnail.copy(selected = false))
107+
}
108+
ThumbnailBook.Type.USER_UPLOADED -> {
109+
thumbnailBookRepository.insert(thumbnail.copy(selected = false))
110+
}
111+
}
112+
113+
when (markSelected) {
114+
MarkSelectedPreference.YES -> {
115+
thumbnailBookRepository.markSelected(thumbnail)
116+
}
117+
MarkSelectedPreference.IF_NONE_OR_GENERATED -> {
118+
val selectedThumbnail = thumbnailBookRepository.findSelectedByBookIdOrNull(thumbnail.bookId)
119+
120+
if (selectedThumbnail == null || selectedThumbnail.type == ThumbnailBook.Type.GENERATED)
121+
thumbnailBookRepository.markSelected(thumbnail)
122+
else thumbnailsHouseKeeping(thumbnail.bookId)
123+
}
124+
MarkSelectedPreference.NO -> {
125+
thumbnailsHouseKeeping(thumbnail.bookId)
106126
}
107127
}
108128

109129
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(thumbnail))
130+
}
110131

111-
if (thumbnail.selected)
112-
thumbnailBookRepository.markSelected(thumbnail)
113-
else
114-
thumbnailsHouseKeeping(thumbnail.bookId)
132+
fun deleteThumbnailForBook(thumbnail: ThumbnailBook) {
133+
require(thumbnail.type == ThumbnailBook.Type.USER_UPLOADED) { "Only uploaded thumbnails can be deleted" }
134+
thumbnailBookRepository.delete(thumbnail.id)
135+
thumbnailsHouseKeeping(thumbnail.bookId)
136+
eventPublisher.publishEvent(DomainEvent.ThumbnailBookDeleted(thumbnail))
115137
}
116138

117139
fun getThumbnail(bookId: String): ThumbnailBook? {
@@ -136,6 +158,18 @@ class BookLifecycle(
136158
return null
137159
}
138160

161+
fun getThumbnailBytesByThumbnailId(thumbnailId: String): ByteArray? =
162+
thumbnailBookRepository.findByIdOrNull(thumbnailId)?.let {
163+
getBytesFromThumbnailBook(it)
164+
}
165+
166+
private fun getBytesFromThumbnailBook(thumbnail: ThumbnailBook): ByteArray? =
167+
when {
168+
thumbnail.thumbnail != null -> thumbnail.thumbnail
169+
thumbnail.url != null -> File(thumbnail.url.toURI()).readBytes()
170+
else -> null
171+
}
172+
139173
private fun thumbnailsHouseKeeping(bookId: String) {
140174
logger.info { "House keeping thumbnails for book: $bookId" }
141175
val all = thumbnailBookRepository.findAllByBookId(bookId)

Diff for: komga/src/main/kotlin/org/gotson/komga/domain/service/LocalArtworkLifecycle.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class LocalArtworkLifecycle(
2424

2525
if (library.importLocalArtwork)
2626
localArtworkProvider.getBookThumbnails(book).forEach {
27-
bookLifecycle.addThumbnailForBook(it)
27+
bookLifecycle.addThumbnailForBook(it, if (it.selected) MarkSelectedPreference.IF_NONE_OR_GENERATED else MarkSelectedPreference.NO)
2828
}
2929
else
3030
logger.info { "Library is not set to import local artwork, skipping" }
@@ -36,7 +36,7 @@ class LocalArtworkLifecycle(
3636

3737
if (library.importLocalArtwork)
3838
localArtworkProvider.getSeriesThumbnails(series).forEach {
39-
seriesLifecycle.addThumbnailForSeries(it, if (it.selected) MarkSelectedPreference.IF_NONE_EXIST else MarkSelectedPreference.NO)
39+
seriesLifecycle.addThumbnailForSeries(it, if (it.selected) MarkSelectedPreference.IF_NONE_OR_GENERATED else MarkSelectedPreference.NO)
4040
}
4141
else
4242
logger.info { "Library is not set to import local artwork, skipping" }

0 commit comments

Comments
 (0)