fix image processing

This commit is contained in:
Stefan Hardegger
2025-09-27 09:29:40 +02:00
parent df5e124115
commit 622cf9ac76
5 changed files with 135 additions and 97 deletions

View File

@@ -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;
@@ -146,6 +144,10 @@ public class StoryController {
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));
} }
@@ -163,6 +165,10 @@ public class StoryController {
} }
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());
@@ -707,6 +715,38 @@ 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,

View File

@@ -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;
}
}

View File

@@ -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);
}
}
} }

View File

@@ -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());
@@ -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);
@@ -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());

View File

@@ -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
); );
} }