Skip to content

Commit

Permalink
feat: add support for KOReader Sync
Browse files Browse the repository at this point in the history
Closes: #1760
  • Loading branch information
gotson committed Jan 9, 2025
1 parent cbb0d61 commit 623b2e3
Show file tree
Hide file tree
Showing 26 changed files with 507 additions and 2 deletions.
19 changes: 19 additions & 0 deletions komga-webui/src/components/dialogs/LibraryEditDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,22 @@
</template>
</v-checkbox>

<v-checkbox
v-model="form.hashKoreader"
:label="$t('dialog.edit_library.field_analysis_hash_koreader')"
hide-details
class="mx-4"
>
<template v-slot:append>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon v-on="on" color="warning">mdi-alert-circle-outline</v-icon>
</template>
{{ $t('dialog.edit_library.tooltip_use_resources') }}
</v-tooltip>
</template>
</v-checkbox>

<v-checkbox
v-model="form.analyzeDimensions"
:label="$t('dialog.edit_library.field_analysis_analyze_dimensions')"
Expand Down Expand Up @@ -465,6 +481,7 @@ export default Vue.extend({
seriesCover: SeriesCoverDto.FIRST as SeriesCoverDto,
hashFiles: true,
hashPages: false,
hashKoreader: false,
analyzeDimensions: true,
oneshotsDirectory: '',
},
Expand Down Expand Up @@ -624,6 +641,7 @@ export default Vue.extend({
this.form.seriesCover = library ? library.seriesCover : SeriesCoverDto.FIRST
this.form.hashFiles = library ? library.hashFiles : true
this.form.hashPages = library ? library.hashPages : false
this.form.hashKoreader = library ? library.hashKoreader : false
this.form.analyzeDimensions = library ? library.analyzeDimensions : true
this.form.oneshotsDirectory = library ? library.oneshotsDirectory : ''
this.$v.$reset()
Expand Down Expand Up @@ -658,6 +676,7 @@ export default Vue.extend({
seriesCover: this.form.seriesCover,
hashFiles: this.form.hashFiles,
hashPages: this.form.hashPages,
hashKoreader: this.form.hashKoreader,
analyzeDimensions: this.form.analyzeDimensions,
oneshotsDirectory: this.form.oneshotsDirectory,
}
Expand Down
2 changes: 2 additions & 0 deletions komga-webui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@
"dialot_title_edit": "Edit Library",
"field_analysis_analyze_dimensions": "Analyze pages dimensions",
"field_analysis_hash_files": "Compute hash for files",
"field_analysis_hash_koreader": "Compute hash for files for KOReader",
"field_analysis_hash_pages": "Compute hash for pages",
"field_convert_to_cbz": "Automatically convert to CBZ",
"field_import_barcode_isbn": "ISBN barcode",
Expand Down Expand Up @@ -1004,6 +1005,7 @@
"ADMIN": "Administrator",
"FILE_DOWNLOAD": "File download",
"KOBO_SYNC": "Kobo Sync",
"KOREADER_SYNC": "KOReader Sync",
"PAGE_STREAMING": "Page streaming",
"USER": "User"
},
Expand Down
3 changes: 2 additions & 1 deletion komga-webui/src/types/enum-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ export enum UserRoles {
ADMIN = 'ADMIN',
FILE_DOWNLOAD = 'FILE_DOWNLOAD',
PAGE_STREAMING = 'PAGE_STREAMING',
KOBO_SYNC = 'KOBO_SYNC'
KOBO_SYNC = 'KOBO_SYNC',
KOREADER_SYNC = 'KOREADER_SYNC'
}

export enum AllowExclude {
Expand Down
3 changes: 3 additions & 0 deletions komga-webui/src/types/komga-libraries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface LibraryDto {
seriesCover: SeriesCoverDto,
hashFiles: boolean,
hashPages: boolean,
hashKoreader: boolean,
analyzeDimensions: boolean,
oneshotsDirectory: string,
unavailable: boolean,
Expand Down Expand Up @@ -58,6 +59,7 @@ export interface LibraryCreationDto {
seriesCover: SeriesCoverDto,
hashFiles: boolean,
hashPages: boolean,
hashKoreader: boolean,
analyzeDimensions: boolean,
oneshotsDirectory: string,
}
Expand Down Expand Up @@ -88,6 +90,7 @@ export interface LibraryUpdateDto {
seriesCover: SeriesCoverDto,
hashFiles: boolean,
hashPages: boolean,
hashKoreader: boolean,
analyzeDimensions: boolean,
oneshotsDirectory: string,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ALTER TABLE LIBRARY
add column HASH_KOREADER boolean NOT NULL DEFAULT 0;

ALTER TABLE BOOK
ADD COLUMN FILE_HASH_KOREADER varchar NOT NULL DEFAULT '';

Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ sealed class Task(
override fun toString(): String = "HashBookPages(bookId='$bookId', priority='$priority')"
}

class HashBookKoreader(
val bookId: String,
priority: Int = DEFAULT_PRIORITY,
) : Task(priority) {
override val uniqueId = "HASH_BOOK_KOREADER_$bookId"

override fun toString(): String = "HashBookKoreader(bookId='$bookId', priority='$priority')"
}

class RefreshSeriesMetadata(
val seriesId: String,
priority: Int = DEFAULT_PRIORITY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ class TaskEmitter(
.let { submitTasks(it) }
}

fun hashBooksWithoutHashKoreader(library: Library) {
if (library.hashKoreader)
bookRepository
.findAllByLibraryIdAndWithEmptyHashKoreader(library.id)
.map { Task.HashBookKoreader(it.id, LOWEST_PRIORITY) }
.let { submitTasks(it) }
}

fun findBooksWithMissingPageHash(
library: Library,
priority: Int = DEFAULT_PRIORITY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class TaskHandler(
taskEmitter.findBooksWithMissingPageHash(library, LOWEST_PRIORITY)
taskEmitter.findDuplicatePagesToDelete(library, LOWEST_PRIORITY)
taskEmitter.hashBooksWithoutHash(library)
taskEmitter.hashBooksWithoutHashKoreader(library)
} ?: logger.warn { "Cannot execute task $task: Library does not exist" }

is Task.FindBooksToConvert ->
Expand Down Expand Up @@ -150,6 +151,11 @@ class TaskHandler(
bookLifecycle.hashAndPersist(book)
} ?: logger.warn { "Cannot execute task $task: Book does not exist" }

is Task.HashBookKoreader ->
bookRepository.findByIdOrNull(task.bookId)?.let { book ->
bookLifecycle.hashKoreaderAndPersist(book)
} ?: logger.warn { "Cannot execute task $task: Book does not exist" }

is Task.HashBookPages ->
bookRepository.findByIdOrNull(task.bookId)?.let { book ->
bookLifecycle.hashPagesAndPersist(book)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ data class Book(
val fileLastModified: LocalDateTime,
val fileSize: Long = 0,
val fileHash: String = "",
val fileHashKoreader: String = "",
val number: Int = 0,
val id: String = TsidCreator.getTsid256().toString(),
val seriesId: String = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ data class Library(
val seriesCover: SeriesCover = SeriesCover.FIRST,
val hashFiles: Boolean = true,
val hashPages: Boolean = false,
val hashKoreader: Boolean = false,
val analyzeDimensions: Boolean = true,
val oneshotsDirectory: String? = null,
val unavailableDate: LocalDateTime? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ enum class UserRoles {
FILE_DOWNLOAD,
PAGE_STREAMING,
KOBO_SYNC,
KOREADER_SYNC,
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ interface BookRepository {

fun findAllByLibraryIdAndWithEmptyHash(libraryId: String): Collection<Book>

fun findAllByLibraryIdAndWithEmptyHashKoreader(libraryId: String): Collection<Book>

fun findAllByHashKoreader(hashKoreader: String): Collection<Book>

fun findAllByLibraryIdAndMediaTypes(
libraryId: String,
mediaTypes: Collection<String>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import org.gotson.komga.domain.persistence.ReadProgressRepository
import org.gotson.komga.domain.persistence.ThumbnailBookRepository
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.infrastructure.hash.Hasher
import org.gotson.komga.infrastructure.hash.KoreaderHasher
import org.gotson.komga.infrastructure.image.ImageConverter
import org.gotson.komga.infrastructure.image.ImageType
import org.gotson.komga.language.toCurrentTimeZone
Expand Down Expand Up @@ -66,6 +67,7 @@ class BookLifecycle(
private val eventPublisher: ApplicationEventPublisher,
private val transactionTemplate: TransactionTemplate,
private val hasher: Hasher,
private val hasherKoreader: KoreaderHasher,
private val historicalEventRepository: HistoricalEventRepository,
private val komgaSettingsProvider: KomgaSettingsProvider,
@Qualifier("pdfImageType")
Expand Down Expand Up @@ -113,6 +115,19 @@ class BookLifecycle(
}
}

fun hashKoreaderAndPersist(book: Book) {
if (!libraryRepository.findById(book.libraryId).hashKoreader)
return logger.info { "File hashing for Koreader is disabled for the library, it may have changed since the task was submitted, skipping" }

logger.info { "Hash Koreader and persist book: $book" }
if (book.fileHashKoreader.isBlank()) {
val hash = hasherKoreader.computeHash(book.path)
bookRepository.update(book.copy(fileHashKoreader = hash))
} else {
logger.info { "Book already has a Koreader hash, skipping" }
}
}

fun hashPagesAndPersist(book: Book) {
if (!libraryRepository.findById(book.libraryId).hashPages)
return logger.info { "Page hashing is disabled for the library, it may have changed since the task was submitted, skipping" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.gotson.komga.infrastructure.hash

import com.appmattus.crypto.Algorithm
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import java.io.RandomAccessFile
import java.nio.file.Path

private val logger = KotlinLogging.logger {}

@Component
class KoreaderHasher {
fun computeHash(path: Path): String {
logger.debug { "Koreader hashing: $path" }

return partialMd5(path)
}

/**
* From https://github.com/koreader/koreader/blob/5bd3f3b42c95fd143d98f8fc9695d486fd92b7c8/frontend/util.lua#L1093-L1119
*/
@OptIn(ExperimentalStdlibApi::class)
private fun partialMd5(path: Path): String {
val step = 1024L
val size = 1024
val digest = Algorithm.MD5.createDigest()

val file = RandomAccessFile(path.toFile(), "r")

val buffer = ByteArray(size)
(-1..10).forEach {
file.seek(step shl (2 * it))
val s = file.read(buffer)
if (s > 0) digest.update(buffer)
}

return digest.digest().toHexString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,21 @@ class BookDao(
.fetchInto(b)
.map { it.toDomain() }

override fun findAllByLibraryIdAndWithEmptyHashKoreader(libraryId: String): Collection<Book> =
dsl
.selectFrom(b)
.where(b.LIBRARY_ID.eq(libraryId))
.and(b.FILE_HASH_KOREADER.eq(""))
.fetchInto(b)
.map { it.toDomain() }

override fun findAllByHashKoreader(hashKoreader: String): Collection<Book> =
dsl
.selectFrom(b)
.where(b.FILE_HASH_KOREADER.eq(hashKoreader))
.fetchInto(b)
.map { it.toDomain() }

@Transactional
override fun insert(book: Book) {
insert(listOf(book))
Expand All @@ -321,11 +336,12 @@ class BookDao(
b.FILE_LAST_MODIFIED,
b.FILE_SIZE,
b.FILE_HASH,
b.FILE_HASH_KOREADER,
b.LIBRARY_ID,
b.SERIES_ID,
b.DELETED_DATE,
b.ONESHOT,
).values(null as String?, null, null, null, null, null, null, null, null, null, null),
).values(null as String?, null, null, null, null, null, null, null, null, null, null, null),
).also { step ->
chunk.forEach {
step.bind(
Expand All @@ -336,6 +352,7 @@ class BookDao(
it.fileLastModified,
it.fileSize,
it.fileHash,
it.fileHashKoreader,
it.libraryId,
it.seriesId,
it.deletedDate,
Expand Down Expand Up @@ -366,6 +383,7 @@ class BookDao(
.set(b.FILE_LAST_MODIFIED, book.fileLastModified)
.set(b.FILE_SIZE, book.fileSize)
.set(b.FILE_HASH, book.fileHash)
.set(b.FILE_HASH_KOREADER, book.fileHashKoreader)
.set(b.LIBRARY_ID, book.libraryId)
.set(b.SERIES_ID, book.seriesId)
.set(b.DELETED_DATE, book.deletedDate)
Expand Down Expand Up @@ -413,6 +431,7 @@ class BookDao(
fileLastModified = fileLastModified,
fileSize = fileSize,
fileHash = fileHash,
fileHashKoreader = fileHashKoreader,
id = id,
libraryId = libraryId,
seriesId = seriesId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class LibraryDao(
.set(l.SERIES_COVER, library.seriesCover.toString())
.set(l.HASH_FILES, library.hashFiles)
.set(l.HASH_PAGES, library.hashPages)
.set(l.HASH_KOREADER, library.hashKoreader)
.set(l.ANALYZE_DIMENSIONS, library.analyzeDimensions)
.set(l.ONESHOTS_DIRECTORY, library.oneshotsDirectory)
.set(l.UNAVAILABLE_DATE, library.unavailableDate)
Expand Down Expand Up @@ -138,6 +139,7 @@ class LibraryDao(
.set(l.SERIES_COVER, library.seriesCover.toString())
.set(l.HASH_FILES, library.hashFiles)
.set(l.HASH_PAGES, library.hashPages)
.set(l.HASH_KOREADER, library.hashKoreader)
.set(l.ANALYZE_DIMENSIONS, library.analyzeDimensions)
.set(l.ONESHOTS_DIRECTORY, library.oneshotsDirectory)
.set(l.UNAVAILABLE_DATE, library.unavailableDate)
Expand Down Expand Up @@ -200,6 +202,7 @@ class LibraryDao(
seriesCover = Library.SeriesCover.valueOf(seriesCover),
hashFiles = hashFiles,
hashPages = hashPages,
hashKoreader = hashKoreader,
analyzeDimensions = analyzeDimensions,
oneshotsDirectory = oneshotsDirectory,
unavailableDate = unavailableDate,
Expand Down
Loading

0 comments on commit 623b2e3

Please sign in to comment.