From 622cf9ac76204887d170e196dfb1ff51b7995414 Mon Sep 17 00:00:00 2001 From: Stefan Hardegger Date: Sat, 27 Sep 2025 09:29:40 +0200 Subject: [PATCH] fix image processing --- .../storycove/controller/StoryController.java | 54 +++++++-- .../event/StoryContentUpdatedEvent.java | 34 ++++++ .../com/storycove/service/ImageService.java | 37 +++++++ .../com/storycove/service/StoryService.java | 104 +++--------------- .../storycove/service/StoryServiceTest.java | 3 +- 5 files changed, 135 insertions(+), 97 deletions(-) create mode 100644 backend/src/main/java/com/storycove/event/StoryContentUpdatedEvent.java diff --git a/backend/src/main/java/com/storycove/controller/StoryController.java b/backend/src/main/java/com/storycove/controller/StoryController.java index b20e3e0..84bdf54 100644 --- a/backend/src/main/java/com/storycove/controller/StoryController.java +++ b/backend/src/main/java/com/storycove/controller/StoryController.java @@ -12,9 +12,7 @@ import com.storycove.service.*; import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -144,25 +142,33 @@ public class StoryController { logger.info("Creating new story: {}", request.getTitle()); Story story = new Story(); updateStoryFromRequest(story, request); - + 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()); return ResponseEntity.status(HttpStatus.CREATED).body(convertToDto(savedStory)); } @PutMapping("/{id}") - public ResponseEntity updateStory(@PathVariable UUID id, + public ResponseEntity updateStory(@PathVariable UUID id, @Valid @RequestBody UpdateStoryRequest request) { logger.info("Updating story: {} (ID: {})", request.getTitle(), id); - + // Handle author creation/lookup at controller level before calling service if (request.getAuthorName() != null && !request.getAuthorName().trim().isEmpty() && request.getAuthorId() == null) { Author author = findOrCreateAuthor(request.getAuthorName().trim()); request.setAuthorId(author.getId()); request.setAuthorName(null); // Clear author name since we now have the ID } - + Story updatedStory = storyService.updateWithTagNames(id, request); + + // Process external images in content after saving + updatedStory = processExternalImagesIfNeeded(updatedStory); + logger.info("Successfully updated story: {}", updatedStory.getTitle()); return ResponseEntity.ok(convertToDto(updatedStory)); } @@ -474,7 +480,9 @@ public class StoryController { story.setTitle(createReq.getTitle()); story.setSummary(createReq.getSummary()); story.setDescription(createReq.getDescription()); + story.setContentHtml(sanitizationService.sanitize(createReq.getContentHtml())); + story.setSourceUrl(createReq.getSourceUrl()); story.setVolume(createReq.getVolume()); @@ -706,7 +714,39 @@ public class StoryController { 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") public ResponseEntity> checkDuplicate( @RequestParam String title, diff --git a/backend/src/main/java/com/storycove/event/StoryContentUpdatedEvent.java b/backend/src/main/java/com/storycove/event/StoryContentUpdatedEvent.java new file mode 100644 index 0000000..1cd7793 --- /dev/null +++ b/backend/src/main/java/com/storycove/event/StoryContentUpdatedEvent.java @@ -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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/storycove/service/ImageService.java b/backend/src/main/java/com/storycove/service/ImageService.java index 435f41f..d98bb75 100644 --- a/backend/src/main/java/com/storycove/service/ImageService.java +++ b/backend/src/main/java/com/storycove/service/ImageService.java @@ -4,6 +4,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; 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.web.multipart.MultipartFile; @@ -21,6 +23,8 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.storycove.event.StoryContentUpdatedEvent; + @Service public class ImageService { @@ -934,4 +938,37 @@ public class ImageService { // 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}"); } + + /** + * 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); + } + } } \ No newline at end of file diff --git a/backend/src/main/java/com/storycove/service/StoryService.java b/backend/src/main/java/com/storycove/service/StoryService.java index 4a59690..4c16db7 100644 --- a/backend/src/main/java/com/storycove/service/StoryService.java +++ b/backend/src/main/java/com/storycove/service/StoryService.java @@ -43,7 +43,6 @@ public class StoryService { private final SeriesService seriesService; private final HtmlSanitizationService sanitizationService; private final SearchServiceAdapter searchServiceAdapter; - private final ImageService imageService; @Autowired public StoryService(StoryRepository storyRepository, @@ -53,8 +52,7 @@ public class StoryService { TagService tagService, SeriesService seriesService, HtmlSanitizationService sanitizationService, - SearchServiceAdapter searchServiceAdapter, - ImageService imageService) { + SearchServiceAdapter searchServiceAdapter) { this.storyRepository = storyRepository; this.tagRepository = tagRepository; this.readingPositionRepository = readingPositionRepository; @@ -63,7 +61,6 @@ public class StoryService { this.seriesService = seriesService; this.sanitizationService = sanitizationService; this.searchServiceAdapter = searchServiceAdapter; - this.imageService = imageService; } @Transactional(readOnly = true) @@ -346,34 +343,6 @@ public class StoryService { 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 if (story.getTags() != null && !story.getTags().isEmpty()) { updateStoryTags(savedStory, story.getTags()); @@ -381,7 +350,7 @@ public class StoryService { // Index in search engine searchServiceAdapter.indexStory(savedStory); - + return savedStory; } @@ -402,34 +371,6 @@ public class StoryService { 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 if (tagNames != null && !tagNames.isEmpty()) { updateStoryTagsByNames(savedStory, tagNames); @@ -437,7 +378,7 @@ public class StoryService { // Index in search engine searchServiceAdapter.indexStory(savedStory); - + return savedStory; } @@ -481,6 +422,18 @@ public class StoryService { 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) { Story story = findById(id); @@ -647,32 +600,7 @@ public class StoryService { story.setSummary(updateReq.getSummary()); } if (updateReq.getContentHtml() != null) { - String sanitizedContent = 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); - } + story.setContentHtml(sanitizationService.sanitize(updateReq.getContentHtml())); } if (updateReq.getSourceUrl() != null) { story.setSourceUrl(updateReq.getSourceUrl()); diff --git a/backend/src/test/java/com/storycove/service/StoryServiceTest.java b/backend/src/test/java/com/storycove/service/StoryServiceTest.java index 377c359..b49996f 100644 --- a/backend/src/test/java/com/storycove/service/StoryServiceTest.java +++ b/backend/src/test/java/com/storycove/service/StoryServiceTest.java @@ -56,8 +56,7 @@ class StoryServiceTest { null, // tagService - not needed for reading progress tests null, // seriesService - not needed for reading progress tests null, // sanitizationService - not needed for reading progress tests - searchServiceAdapter, - null // imageService - not needed for reading progress tests + searchServiceAdapter ); }