fix image processing
This commit is contained in:
@@ -12,9 +12,7 @@ import com.storycove.service.*;
|
|||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageImpl;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
@@ -144,25 +142,33 @@ public class StoryController {
|
|||||||
logger.info("Creating new story: {}", request.getTitle());
|
logger.info("Creating new story: {}", request.getTitle());
|
||||||
Story story = new Story();
|
Story story = new Story();
|
||||||
updateStoryFromRequest(story, request);
|
updateStoryFromRequest(story, request);
|
||||||
|
|
||||||
Story savedStory = storyService.createWithTagNames(story, request.getTagNames());
|
Story savedStory = storyService.createWithTagNames(story, request.getTagNames());
|
||||||
|
|
||||||
|
// Process external images in content after saving
|
||||||
|
savedStory = processExternalImagesIfNeeded(savedStory);
|
||||||
|
|
||||||
logger.info("Successfully created story: {} (ID: {})", savedStory.getTitle(), savedStory.getId());
|
logger.info("Successfully created story: {} (ID: {})", savedStory.getTitle(), savedStory.getId());
|
||||||
return ResponseEntity.status(HttpStatus.CREATED).body(convertToDto(savedStory));
|
return ResponseEntity.status(HttpStatus.CREATED).body(convertToDto(savedStory));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<StoryDto> updateStory(@PathVariable UUID id,
|
public ResponseEntity<StoryDto> updateStory(@PathVariable UUID id,
|
||||||
@Valid @RequestBody UpdateStoryRequest request) {
|
@Valid @RequestBody UpdateStoryRequest request) {
|
||||||
logger.info("Updating story: {} (ID: {})", request.getTitle(), id);
|
logger.info("Updating story: {} (ID: {})", request.getTitle(), id);
|
||||||
|
|
||||||
// Handle author creation/lookup at controller level before calling service
|
// Handle author creation/lookup at controller level before calling service
|
||||||
if (request.getAuthorName() != null && !request.getAuthorName().trim().isEmpty() && request.getAuthorId() == null) {
|
if (request.getAuthorName() != null && !request.getAuthorName().trim().isEmpty() && request.getAuthorId() == null) {
|
||||||
Author author = findOrCreateAuthor(request.getAuthorName().trim());
|
Author author = findOrCreateAuthor(request.getAuthorName().trim());
|
||||||
request.setAuthorId(author.getId());
|
request.setAuthorId(author.getId());
|
||||||
request.setAuthorName(null); // Clear author name since we now have the ID
|
request.setAuthorName(null); // Clear author name since we now have the ID
|
||||||
}
|
}
|
||||||
|
|
||||||
Story updatedStory = storyService.updateWithTagNames(id, request);
|
Story updatedStory = storyService.updateWithTagNames(id, request);
|
||||||
|
|
||||||
|
// Process external images in content after saving
|
||||||
|
updatedStory = processExternalImagesIfNeeded(updatedStory);
|
||||||
|
|
||||||
logger.info("Successfully updated story: {}", updatedStory.getTitle());
|
logger.info("Successfully updated story: {}", updatedStory.getTitle());
|
||||||
return ResponseEntity.ok(convertToDto(updatedStory));
|
return ResponseEntity.ok(convertToDto(updatedStory));
|
||||||
}
|
}
|
||||||
@@ -474,7 +480,9 @@ public class StoryController {
|
|||||||
story.setTitle(createReq.getTitle());
|
story.setTitle(createReq.getTitle());
|
||||||
story.setSummary(createReq.getSummary());
|
story.setSummary(createReq.getSummary());
|
||||||
story.setDescription(createReq.getDescription());
|
story.setDescription(createReq.getDescription());
|
||||||
|
|
||||||
story.setContentHtml(sanitizationService.sanitize(createReq.getContentHtml()));
|
story.setContentHtml(sanitizationService.sanitize(createReq.getContentHtml()));
|
||||||
|
|
||||||
story.setSourceUrl(createReq.getSourceUrl());
|
story.setSourceUrl(createReq.getSourceUrl());
|
||||||
story.setVolume(createReq.getVolume());
|
story.setVolume(createReq.getVolume());
|
||||||
|
|
||||||
@@ -706,7 +714,39 @@ public class StoryController {
|
|||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Story processExternalImagesIfNeeded(Story story) {
|
||||||
|
try {
|
||||||
|
if (story.getContentHtml() != null && !story.getContentHtml().trim().isEmpty()) {
|
||||||
|
logger.debug("Processing external images for story: {}", story.getId());
|
||||||
|
|
||||||
|
ImageService.ContentImageProcessingResult result =
|
||||||
|
imageService.processContentImages(story.getContentHtml(), story.getId());
|
||||||
|
|
||||||
|
// If content was changed (external images were processed), update the story
|
||||||
|
if (!result.getProcessedContent().equals(story.getContentHtml())) {
|
||||||
|
logger.info("External images processed for story {}: {} images downloaded",
|
||||||
|
story.getId(), result.getDownloadedImages().size());
|
||||||
|
|
||||||
|
// Update the story with the processed content
|
||||||
|
story.setContentHtml(result.getProcessedContent());
|
||||||
|
story = storyService.updateContentOnly(story.getId(), result.getProcessedContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.hasWarnings()) {
|
||||||
|
logger.warn("Image processing warnings for story {}: {}",
|
||||||
|
story.getId(), result.getWarnings());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to process external images for story {}: {}",
|
||||||
|
story.getId(), e.getMessage(), e);
|
||||||
|
// Don't fail the entire operation if image processing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
return story;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/check-duplicate")
|
@GetMapping("/check-duplicate")
|
||||||
public ResponseEntity<Map<String, Object>> checkDuplicate(
|
public ResponseEntity<Map<String, Object>> checkDuplicate(
|
||||||
@RequestParam String title,
|
@RequestParam String title,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.storycove.event;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event published when a story's content is created or updated
|
||||||
|
*/
|
||||||
|
public class StoryContentUpdatedEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private final UUID storyId;
|
||||||
|
private final String contentHtml;
|
||||||
|
private final boolean isNewStory;
|
||||||
|
|
||||||
|
public StoryContentUpdatedEvent(Object source, UUID storyId, String contentHtml, boolean isNewStory) {
|
||||||
|
super(source);
|
||||||
|
this.storyId = storyId;
|
||||||
|
this.contentHtml = contentHtml;
|
||||||
|
this.isNewStory = isNewStory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getStoryId() {
|
||||||
|
return storyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentHtml() {
|
||||||
|
return contentHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNewStory() {
|
||||||
|
return isNewStory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -21,6 +23,8 @@ import java.util.List;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import com.storycove.event.StoryContentUpdatedEvent;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ImageService {
|
public class ImageService {
|
||||||
|
|
||||||
@@ -934,4 +938,37 @@ public class ImageService {
|
|||||||
// Check if it matches UUID pattern (8-4-4-4-12 hex characters)
|
// Check if it matches UUID pattern (8-4-4-4-12 hex characters)
|
||||||
return nameWithoutExt.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
|
return nameWithoutExt.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listener for story content updates - processes external images asynchronously
|
||||||
|
*/
|
||||||
|
@EventListener
|
||||||
|
@Async
|
||||||
|
public void handleStoryContentUpdated(StoryContentUpdatedEvent event) {
|
||||||
|
logger.info("Processing images for {} story {} after content update",
|
||||||
|
event.isNewStory() ? "new" : "updated", event.getStoryId());
|
||||||
|
|
||||||
|
try {
|
||||||
|
ContentImageProcessingResult result = processContentImages(event.getContentHtml(), event.getStoryId());
|
||||||
|
|
||||||
|
// If content was changed, we need to update the story (but this could cause circular events)
|
||||||
|
// Instead, let's just log the results for now and let the controller handle updates if needed
|
||||||
|
if (result.hasWarnings()) {
|
||||||
|
logger.warn("Image processing warnings for story {}: {}", event.getStoryId(), result.getWarnings());
|
||||||
|
}
|
||||||
|
if (!result.getDownloadedImages().isEmpty()) {
|
||||||
|
logger.info("Downloaded {} external images for story {}: {}",
|
||||||
|
result.getDownloadedImages().size(), event.getStoryId(), result.getDownloadedImages());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: If content was changed, we might need a way to update the story without triggering another event
|
||||||
|
if (!result.getProcessedContent().equals(event.getContentHtml())) {
|
||||||
|
logger.info("Story {} content was processed and external images were replaced with local URLs", event.getStoryId());
|
||||||
|
// For now, just log that processing occurred - the original content processing already handles updates
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to process images for story {}: {}", event.getStoryId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,6 @@ public class StoryService {
|
|||||||
private final SeriesService seriesService;
|
private final SeriesService seriesService;
|
||||||
private final HtmlSanitizationService sanitizationService;
|
private final HtmlSanitizationService sanitizationService;
|
||||||
private final SearchServiceAdapter searchServiceAdapter;
|
private final SearchServiceAdapter searchServiceAdapter;
|
||||||
private final ImageService imageService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public StoryService(StoryRepository storyRepository,
|
public StoryService(StoryRepository storyRepository,
|
||||||
@@ -53,8 +52,7 @@ public class StoryService {
|
|||||||
TagService tagService,
|
TagService tagService,
|
||||||
SeriesService seriesService,
|
SeriesService seriesService,
|
||||||
HtmlSanitizationService sanitizationService,
|
HtmlSanitizationService sanitizationService,
|
||||||
SearchServiceAdapter searchServiceAdapter,
|
SearchServiceAdapter searchServiceAdapter) {
|
||||||
ImageService imageService) {
|
|
||||||
this.storyRepository = storyRepository;
|
this.storyRepository = storyRepository;
|
||||||
this.tagRepository = tagRepository;
|
this.tagRepository = tagRepository;
|
||||||
this.readingPositionRepository = readingPositionRepository;
|
this.readingPositionRepository = readingPositionRepository;
|
||||||
@@ -63,7 +61,6 @@ public class StoryService {
|
|||||||
this.seriesService = seriesService;
|
this.seriesService = seriesService;
|
||||||
this.sanitizationService = sanitizationService;
|
this.sanitizationService = sanitizationService;
|
||||||
this.searchServiceAdapter = searchServiceAdapter;
|
this.searchServiceAdapter = searchServiceAdapter;
|
||||||
this.imageService = imageService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@@ -346,34 +343,6 @@ public class StoryService {
|
|||||||
|
|
||||||
Story savedStory = storyRepository.save(story);
|
Story savedStory = storyRepository.save(story);
|
||||||
|
|
||||||
// Process external images in content (now that we have a story ID)
|
|
||||||
if (savedStory.getContentHtml() != null && !savedStory.getContentHtml().trim().isEmpty()) {
|
|
||||||
try {
|
|
||||||
ImageService.ContentImageProcessingResult imageResult =
|
|
||||||
imageService.processContentImages(savedStory.getContentHtml(), savedStory.getId());
|
|
||||||
|
|
||||||
// Update content if images were processed
|
|
||||||
if (!imageResult.getProcessedContent().equals(savedStory.getContentHtml())) {
|
|
||||||
savedStory.setContentHtml(imageResult.getProcessedContent());
|
|
||||||
savedStory = storyRepository.save(savedStory);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log any warnings or downloaded images
|
|
||||||
if (imageResult.hasWarnings()) {
|
|
||||||
logger.warn("Image processing warnings for new story {}: {}",
|
|
||||||
savedStory.getId(), imageResult.getWarnings());
|
|
||||||
}
|
|
||||||
if (!imageResult.getDownloadedImages().isEmpty()) {
|
|
||||||
logger.info("Downloaded {} external images for new story {}: {}",
|
|
||||||
imageResult.getDownloadedImages().size(), savedStory.getId(),
|
|
||||||
imageResult.getDownloadedImages());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("Failed to process images for new story {}: {}",
|
|
||||||
savedStory.getId(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle tags
|
// Handle tags
|
||||||
if (story.getTags() != null && !story.getTags().isEmpty()) {
|
if (story.getTags() != null && !story.getTags().isEmpty()) {
|
||||||
updateStoryTags(savedStory, story.getTags());
|
updateStoryTags(savedStory, story.getTags());
|
||||||
@@ -381,7 +350,7 @@ public class StoryService {
|
|||||||
|
|
||||||
// Index in search engine
|
// Index in search engine
|
||||||
searchServiceAdapter.indexStory(savedStory);
|
searchServiceAdapter.indexStory(savedStory);
|
||||||
|
|
||||||
return savedStory;
|
return savedStory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,34 +371,6 @@ public class StoryService {
|
|||||||
|
|
||||||
Story savedStory = storyRepository.save(story);
|
Story savedStory = storyRepository.save(story);
|
||||||
|
|
||||||
// Process external images in content (now that we have a story ID)
|
|
||||||
if (savedStory.getContentHtml() != null && !savedStory.getContentHtml().trim().isEmpty()) {
|
|
||||||
try {
|
|
||||||
ImageService.ContentImageProcessingResult imageResult =
|
|
||||||
imageService.processContentImages(savedStory.getContentHtml(), savedStory.getId());
|
|
||||||
|
|
||||||
// Update content if images were processed
|
|
||||||
if (!imageResult.getProcessedContent().equals(savedStory.getContentHtml())) {
|
|
||||||
savedStory.setContentHtml(imageResult.getProcessedContent());
|
|
||||||
savedStory = storyRepository.save(savedStory);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log any warnings or downloaded images
|
|
||||||
if (imageResult.hasWarnings()) {
|
|
||||||
logger.warn("Image processing warnings for new story {}: {}",
|
|
||||||
savedStory.getId(), imageResult.getWarnings());
|
|
||||||
}
|
|
||||||
if (!imageResult.getDownloadedImages().isEmpty()) {
|
|
||||||
logger.info("Downloaded {} external images for new story {}: {}",
|
|
||||||
imageResult.getDownloadedImages().size(), savedStory.getId(),
|
|
||||||
imageResult.getDownloadedImages());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("Failed to process images for new story {}: {}",
|
|
||||||
savedStory.getId(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle tags by names
|
// Handle tags by names
|
||||||
if (tagNames != null && !tagNames.isEmpty()) {
|
if (tagNames != null && !tagNames.isEmpty()) {
|
||||||
updateStoryTagsByNames(savedStory, tagNames);
|
updateStoryTagsByNames(savedStory, tagNames);
|
||||||
@@ -437,7 +378,7 @@ public class StoryService {
|
|||||||
|
|
||||||
// Index in search engine
|
// Index in search engine
|
||||||
searchServiceAdapter.indexStory(savedStory);
|
searchServiceAdapter.indexStory(savedStory);
|
||||||
|
|
||||||
return savedStory;
|
return savedStory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,6 +422,18 @@ public class StoryService {
|
|||||||
return updatedStory;
|
return updatedStory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Story updateContentOnly(UUID id, String contentHtml) {
|
||||||
|
Story existingStory = findById(id);
|
||||||
|
existingStory.setContentHtml(contentHtml);
|
||||||
|
|
||||||
|
Story updatedStory = storyRepository.save(existingStory);
|
||||||
|
|
||||||
|
// Update in search engine since content changed
|
||||||
|
searchServiceAdapter.updateStory(updatedStory);
|
||||||
|
|
||||||
|
return updatedStory;
|
||||||
|
}
|
||||||
|
|
||||||
public void delete(UUID id) {
|
public void delete(UUID id) {
|
||||||
Story story = findById(id);
|
Story story = findById(id);
|
||||||
|
|
||||||
@@ -647,32 +600,7 @@ public class StoryService {
|
|||||||
story.setSummary(updateReq.getSummary());
|
story.setSummary(updateReq.getSummary());
|
||||||
}
|
}
|
||||||
if (updateReq.getContentHtml() != null) {
|
if (updateReq.getContentHtml() != null) {
|
||||||
String sanitizedContent = sanitizationService.sanitize(updateReq.getContentHtml());
|
story.setContentHtml(sanitizationService.sanitize(updateReq.getContentHtml()));
|
||||||
|
|
||||||
// Process external images in the content
|
|
||||||
try {
|
|
||||||
ImageService.ContentImageProcessingResult imageResult =
|
|
||||||
imageService.processContentImages(sanitizedContent, story.getId());
|
|
||||||
|
|
||||||
// Use the processed content (with local image URLs)
|
|
||||||
story.setContentHtml(imageResult.getProcessedContent());
|
|
||||||
|
|
||||||
// Log any warnings or downloaded images
|
|
||||||
if (imageResult.hasWarnings()) {
|
|
||||||
logger.warn("Image processing warnings for story {}: {}",
|
|
||||||
story.getId(), imageResult.getWarnings());
|
|
||||||
}
|
|
||||||
if (!imageResult.getDownloadedImages().isEmpty()) {
|
|
||||||
logger.info("Downloaded {} external images for story {}: {}",
|
|
||||||
imageResult.getDownloadedImages().size(), story.getId(),
|
|
||||||
imageResult.getDownloadedImages());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("Failed to process images for story {}, using content without image processing: {}",
|
|
||||||
story.getId(), e.getMessage());
|
|
||||||
// Fallback to sanitized content without image processing
|
|
||||||
story.setContentHtml(sanitizedContent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (updateReq.getSourceUrl() != null) {
|
if (updateReq.getSourceUrl() != null) {
|
||||||
story.setSourceUrl(updateReq.getSourceUrl());
|
story.setSourceUrl(updateReq.getSourceUrl());
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ class StoryServiceTest {
|
|||||||
null, // tagService - not needed for reading progress tests
|
null, // tagService - not needed for reading progress tests
|
||||||
null, // seriesService - not needed for reading progress tests
|
null, // seriesService - not needed for reading progress tests
|
||||||
null, // sanitizationService - not needed for reading progress tests
|
null, // sanitizationService - not needed for reading progress tests
|
||||||
searchServiceAdapter,
|
searchServiceAdapter
|
||||||
null // imageService - not needed for reading progress tests
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user