Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
35b736d
fix(kobo-sync): pass file position to device
alexhb1 Mar 27, 2026
e5f20be
fix(kobo-sync): preserve CFI value for sync
alexhb1 Mar 27, 2026
392486c
fix(kobo-sync): add KEPUB output type
alexhb1 Mar 27, 2026
5557173
fix(kobo-sync): Full KoboSpan positioning
alexhb1 Mar 28, 2026
0594eb8
fix(kobo-sync): Cleanup and simplification
alexhb1 Mar 28, 2026
e5a7bf0
fix(kobo-sync): fix CFI fallback
alexhb1 Mar 28, 2026
1d4fcb2
fix(kobo-sync): Better calculate KoboSpan via CFI
alexhb1 Mar 28, 2026
1608533
fix(kobo-sync): Strip unwanted CFI elements
alexhb1 Mar 28, 2026
d70fc5e
fix(kobo-sync): Improve two-way sync freshness
alexhb1 Mar 28, 2026
cf0d98d
fix(kobo-sync): Fix web reader using stale CFI over Kobo position
alexhb1 Mar 28, 2026
de6078f
fix(kobo-sync): Fix web reader not parsing Kobo progress
alexhb1 Mar 28, 2026
244dbb7
fix(kobo-sync): pass kobo in-chapter progress to grimmory
alexhb1 Mar 28, 2026
8387483
fix(kobo-sync): Clean up KEPUB option code
alexhb1 Mar 28, 2026
7568228
fix(kobo-sync): Time normalization
alexhb1 Mar 28, 2026
eda769b
fix(kobo-sync): Sync batch size increased to 100
alexhb1 Mar 28, 2026
964018e
fix(kobo-sync): Re-add KEPUB setting and update language
alexhb1 Mar 28, 2026
4fc9819
fix(kobo-sync): review followup + regression tests
alexhb1 Mar 28, 2026
794ce34
fix(kobo-sync): Update null check with method ref
alexhb1 Mar 28, 2026
72a497b
fix(kobo-sync): code cleanup + freshness fix
alexhb1 Mar 28, 2026
ce9a690
fix(kobo-sync): Validate web reader progress for Kobo use
alexhb1 Mar 29, 2026
13bd242
fix(kobo-sync): fix Hardcover progress using potentially stale source
alexhb1 Mar 29, 2026
9c4cfc4
fix(kobo-sync): Fix kobospan freshness regression
alexhb1 Mar 30, 2026
7459a67
fix(kobo-sync): reassert kobospan priority in outbound sync
alexhb1 Mar 30, 2026
add1e97
fix(kobo-sync): Re-add chapter progress
alexhb1 Mar 30, 2026
c1be069
fix(kobo-sync): remove progress filter
alexhb1 Mar 30, 2026
a071cc5
fix(kobo-sync): restore broken filter
alexhb1 Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.booklore.convertor;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import org.booklore.model.dto.kobo.KoboSpanPositionMap;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;

@Converter
public class KoboSpanMapJsonConverter implements AttributeConverter<KoboSpanPositionMap, String> {

private static final ObjectMapper objectMapper = new ObjectMapper();

@Override
public String convertToDatabaseColumn(KoboSpanPositionMap attribute) {
if (attribute == null) {
return null;
}
try {
return objectMapper.writeValueAsString(attribute);
} catch (JacksonException e) {
throw new IllegalStateException("Failed to serialize Kobo span map to JSON", e);
}
}

@Override
public KoboSpanPositionMap convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isEmpty()) {
return null;
}
try {
return objectMapper.readValue(dbData, KoboSpanPositionMap.class);
} catch (JacksonException e) {
throw new IllegalStateException("Failed to deserialize Kobo span map from JSON", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.booklore.model.dto.kobo;

import java.util.List;

public record KoboSpanPositionMap(List<Chapter> chapters) {

public KoboSpanPositionMap {
chapters = chapters == null ? List.of() : List.copyOf(chapters);
}

public record Chapter(String sourceHref,
String normalizedHref,
int spineIndex,
float globalStartProgress,
float globalEndProgress,
List<Span> spans) {

public Chapter {
spans = spans == null ? List.of() : List.copyOf(spans);
}
}

public record Span(String id, float progression) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
@AllArgsConstructor
@NoArgsConstructor
public class EpubProgress {
@NotNull
String cfi;
String href;
Float contentSourceProgressPercent;
@NotNull
Float percentage;
String ttsPositionCfi;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public record BookFileProgress(
String positionData,
String positionHref,
@NotNull Float progressPercent,
String ttsPositionCfi) {
String ttsPositionCfi,
Float contentSourceProgressPercent) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@NoArgsConstructor
public class KoboSettings {
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean convertToKepub = false;
private boolean convertToKepub = true;
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private int conversionLimitInMb = 100;
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.booklore.model.entity;

import jakarta.persistence.*;
import lombok.*;
import org.booklore.convertor.KoboSpanMapJsonConverter;
import org.booklore.model.dto.kobo.KoboSpanPositionMap;

import java.time.Instant;

@Entity
@Table(name = "kobo_span_map",
uniqueConstraints = @UniqueConstraint(name = "uk_kobo_span_map_book_file", columnNames = "book_file_id"))
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KoboSpanMapEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "book_file_id", nullable = false)
private BookFileEntity bookFile;

@Column(name = "file_hash", nullable = false, length = 128)
private String fileHash;

@Convert(converter = KoboSpanMapJsonConverter.class)
@Column(name = "span_map_json", nullable = false, columnDefinition = "LONGTEXT")
private KoboSpanPositionMap spanMap;

@Column(name = "created_at", nullable = false)
private Instant createdAt;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class UserBookFileProgressEntity {
@Column(name = "progress_percent")
private Float progressPercent;

@Column(name = "content_source_progress_percent")
private Float contentSourceProgressPercent;

@Column(name = "tts_position_cfi", length = 1000)
private String ttsPositionCfi;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.booklore.repository;

import org.booklore.model.entity.KoboSpanMapEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

@Repository
public interface KoboSpanMapRepository extends JpaRepository<KoboSpanMapEntity, Long> {

@Query("SELECT ksm FROM KoboSpanMapEntity ksm WHERE ksm.bookFile.id = :bookFileId")
Optional<KoboSpanMapEntity> findByBookFileId(@Param("bookFileId") Long bookFileId);

@Query("SELECT ksm FROM KoboSpanMapEntity ksm WHERE ksm.bookFile.id IN :bookFileIds")
List<KoboSpanMapEntity> findByBookFileIdIn(@Param("bookFileIds") Collection<Long> bookFileIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ List<UserBookFileProgressEntity> findByUserIdAndBookFileBookIdIn(
@Param("bookIds") Iterable<Long> bookIds
);

@EntityGraph(attributePaths = {"bookFile", "bookFile.book"})
@Query("""
SELECT ubfp FROM UserBookFileProgressEntity ubfp
WHERE ubfp.user.id = :userId
AND ubfp.bookFile.id IN :bookFileIds
""")
List<UserBookFileProgressEntity> findByUserIdAndBookFileIdIn(
@Param("userId") Long userId,
@Param("bookFileIds") Iterable<Long> bookFileIds
);

@Modifying
@Transactional
@Query("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ public MetadataPublicReviewsSettings getDefaultMetadataPublicReviewsSettings() {

public KoboSettings getDefaultKoboSettings() {
return KoboSettings.builder()
.convertToKepub(false)
.convertToKepub(true)
.conversionLimitInMb(100)
.convertCbxToEpub(false)
.conversionLimitInMbForCbx(100)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.booklore.service.appsettings.AppSettingService;
import org.booklore.service.kobo.CbxConversionService;
import org.booklore.service.kobo.KepubConversionService;
import org.booklore.service.kobo.KoboSpanMapService;
import org.booklore.util.FileUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
Expand Down Expand Up @@ -48,6 +49,7 @@ public class BookDownloadService {
private final BookFileRepository bookFileRepository;
private final KepubConversionService kepubConversionService;
private final CbxConversionService cbxConversionService;
private final KoboSpanMapService koboSpanMapService;
private final AppSettingService appSettingService;

public ResponseEntity<Resource> downloadBook(Long bookId) {
Expand Down Expand Up @@ -252,7 +254,7 @@ public void downloadKoboBook(Long bookId, HttpServletResponse response) {
throw ApiError.GENERIC_BAD_REQUEST.createException("Kobo settings not found.");
}

boolean convertEpubToKepub = isEpub && koboSettings.isConvertToKepub() && primaryFile.getFileSizeKb() <= (long) koboSettings.getConversionLimitInMb() * 1024;
boolean convertEpubToKepub = isEpub && !primaryFile.isFixedLayout() && koboSettings.isConvertToKepub() && primaryFile.getFileSizeKb() <= (long) koboSettings.getConversionLimitInMb() * 1024;
boolean convertCbxToEpub = isCbx && koboSettings.isConvertCbxToEpub() && primaryFile.getFileSizeKb() <= (long) koboSettings.getConversionLimitInMbForCbx() * 1024;

int compressionPercentage = koboSettings.getConversionImageCompressionPercentage();
Expand All @@ -272,6 +274,11 @@ public void downloadKoboBook(Long bookId, HttpServletResponse response) {
if (convertEpubToKepub) {
fileToSend = kepubConversionService.convertEpubToKepub(inputFile, tempDir.toFile(),
koboSettings.isForceEnableHyphenation());
try {
koboSpanMapService.computeAndStoreIfNeeded(primaryFile, fileToSend);
} catch (Exception e) {
log.warn("Failed to compute Kobo span map for file {}: {}", primaryFile.getId(), e.getMessage());
}
}

setResponseHeaders(response, fileToSend);
Expand Down
Loading
Loading