Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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,100 @@
package org.booklore.controller;
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent package naming: The new classes use 'org.booklore' as the base package, while the entire existing codebase uses 'com.adityachandel.booklore'. This creates a package structure inconsistency that could cause confusion and maintainability issues. All new code should follow the established 'com.adityachandel.booklore' package convention.

Copilot uses AI. Check for mistakes.

import org.booklore.config.security.annotation.CheckBookAccess;
import org.booklore.model.dto.request.CbzConversionRequest;
import org.booklore.model.dto.response.CbzConversionResponse;
import org.booklore.model.enums.EReaderProfile;
import org.booklore.service.conversion.StandaloneCbzConversionService;
import com.github.junrar.exception.RarException;
import freemarker.template.TemplateException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
* REST controller for standalone CBZ to EPUB conversion.
* Allows users to convert comic book archives to EPUB format with custom resolution settings.
*/
@Tag(name = "Book Conversion", description = "Endpoints for converting books between formats")
@Slf4j
@RestController
@RequestMapping("/api/v1/conversion")
@RequiredArgsConstructor
public class BookConversionController {

private final StandaloneCbzConversionService conversionService;

/**
* Get available e-reader device profiles
*/
@Operation(summary = "Get device profiles",
description = "Retrieve list of available e-reader device profiles with their screen resolutions.")
@ApiResponse(responseCode = "200", description = "Device profiles retrieved successfully")
@GetMapping("/profiles")
public ResponseEntity<List<DeviceProfileDto>> getDeviceProfiles() {
List<DeviceProfileDto> profiles = Arrays.stream(EReaderProfile.values())
.map(profile -> new DeviceProfileDto(
profile.name(),
profile.getDisplayName(),
profile.getWidth(),
profile.getHeight(),
profile.supportsCustomResolution()
))
.collect(Collectors.toList());

return ResponseEntity.ok(profiles);
}

/**
* Convert CBZ to EPUB with specified resolution
*/
@Operation(summary = "Convert CBZ to EPUB",
description = "Convert a CBZ/CBR/CB7 book to EPUB format with custom resolution settings. " +
"The converted EPUB will be created as a new book in the same library. " +
"Requires library management permission or admin.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Conversion completed successfully"),
@ApiResponse(responseCode = "400", description = "Invalid request or book is not CBX format"),
@ApiResponse(responseCode = "403", description = "Forbidden - requires library management permission"),
@ApiResponse(responseCode = "404", description = "Book not found")
})
@PostMapping("/cbz-to-epub")
@PreAuthorize("@securityUtil.canManageLibrary() or @securityUtil.isAdmin()")
@CheckBookAccess(bookIdParam = "request.bookId", checkFromRequestBody = true)
public ResponseEntity<CbzConversionResponse> convertCbzToEpub(
@Parameter(description = "Conversion request with device profile and optional custom dimensions")
@RequestBody @Valid CbzConversionRequest request) throws IOException, TemplateException, RarException {

log.info("Received CBZ to EPUB conversion request for book {}", request.getBookId());

CbzConversionResponse response = conversionService.convertCbzToEpub(request);

log.info("Successfully converted CBZ to EPUB: {}", response.getFileName());

return ResponseEntity.ok(response);
}

/**
* DTO for device profile information
*/
public record DeviceProfileDto(
String value,
String displayName,
int width,
int height,
boolean supportsCustom
) {}
}
Comment on lines +36 to +100
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test coverage exists for the BookConversionController. The repository has test coverage for other controllers (e.g., KoreaderControllerTest, HealthcheckControllerTest), so tests should be added for this new controller to maintain consistency with the codebase's testing practices.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.booklore.model.dto.request;

import org.booklore.model.enums.EReaderProfile;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

/**
* Request DTO for converting CBZ files to EPUB with custom resolution settings.
*/
@Data
public class CbzConversionRequest {

@NotNull(message = "Book ID is required")
private Long bookId;

@NotNull(message = "Device profile is required")
private EReaderProfile deviceProfile;

/**
* Custom width in pixels (optional, only used when deviceProfile is OTHER)
*/
@Min(value = 100, message = "Custom width must be at least 100 pixels")
private Integer customWidth;

/**
* Custom height in pixels (optional, only used when deviceProfile is OTHER)
*/
@Min(value = 100, message = "Custom height must be at least 100 pixels")
private Integer customHeight;
Comment on lines +23 to +30
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom dimension fields are missing @max validation annotations. While the PR description mentions a maximum of 5000×5000 pixels, this constraint is not enforced in the validation annotations. This could allow users to specify unreasonably large dimensions that could cause performance or memory issues during conversion.

Copilot uses AI. Check for mistakes.

/**
* Image compression percentage (1-100)
* Default is 85% to match existing Kobo conversion setting
*/
@Min(value = 1, message = "Compression percentage must be between 1 and 100")
private Integer compressionPercentage = 85;
Comment on lines +36 to +37
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation annotation @min(value = 100) on compressionPercentage field is missing a corresponding @max annotation. While the validation logic in StandaloneCbzConversionService.validateRequest checks for a maximum of 100, the DTO should have complete validation annotations for consistency and early validation failure.

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.booklore.model.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* Response DTO for CBZ to EPUB conversion result.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CbzConversionResponse {

/**
* ID of the newly created EPUB book
*/
private Long newBookId;

/**
* Filename of the converted EPUB
*/
private String fileName;

/**
* File size in kilobytes
*/
private Long fileSizeKb;

/**
* Target width used for conversion
*/
private Integer targetWidth;

/**
* Target height used for conversion
*/
private Integer targetHeight;

/**
* Number of pages converted
*/
private Integer pageCount;

/**
* Conversion success message
*/
private String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.booklore.model.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

/**
* E-reader device profiles with screen resolutions for CBZ to EPUB conversion.
* Based on KCC (Kindle Comic Converter) device profiles.
*/
@Getter
@RequiredArgsConstructor
public enum EReaderProfile {
// Kindle devices
KINDLE("Kindle", 600, 800),
KINDLE_DX("Kindle DX", 824, 1200),
KINDLE_PAPERWHITE("Kindle Paperwhite", 1072, 1448),
KINDLE_PAPERWHITE_5("Kindle Paperwhite 5", 1236, 1648),
KINDLE_VOYAGE("Kindle Voyage", 1072, 1448),
KINDLE_OASIS("Kindle Oasis", 1264, 1680),
KINDLE_SCRIBE("Kindle Scribe", 1860, 2480),

// Kobo devices
KOBO_AURA("Kobo Aura", 1404, 1872),
KOBO_AURA_HD("Kobo Aura HD", 1080, 1440),
KOBO_AURA_H2O("Kobo Aura H2O", 1080, 1430),
KOBO_AURA_ONE("Kobo Aura ONE", 1404, 1872),
KOBO_CLARA("Kobo Clara", 1072, 1448),
KOBO_CLARA_HD("Kobo Clara HD", 1072, 1448),
KOBO_ELIPSA("Kobo Elipsa", 1404, 1872),
KOBO_FORMA("Kobo Forma", 1440, 1920),
KOBO_GLO("Kobo Glo", 1024, 768),
KOBO_GLO_HD("Kobo Glo HD", 1072, 1448),
KOBO_LIBRA("Kobo Libra", 1264, 1680),
KOBO_LIBRA_H2O("Kobo Libra H2O", 1264, 1680),
KOBO_NIA("Kobo Nia", 758, 1024),
KOBO_SAGE("Kobo Sage", 1440, 1920),

// Other devices
NOOK_SIMPLE_TOUCH("Nook Simple Touch", 600, 800),
NOOK_GLOWLIGHT("Nook GlowLight", 1024, 758),
NOOK_GLOWLIGHT_PLUS("Nook GlowLight Plus", 1072, 1448),
SONY_PRS_T3("Sony PRS-T3", 758, 1024),
TOLINO_SHINE("Tolino Shine", 758, 1024),
TOLINO_VISION("Tolino Vision", 1072, 1448),
POCKETBOOK_TOUCH_HD("PocketBook Touch HD", 1072, 1448),
POCKETBOOK_INKPAD("PocketBook InkPad", 1200, 1600),

// Generic profiles
OTHER("Other (specify custom)", 0, 0);

private final String displayName;
private final int width;
private final int height;

public boolean supportsCustomResolution() {
return this == OTHER;
}
}
Loading
Loading