fix image processing
This commit is contained in:
@@ -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;
|
||||
@@ -146,6 +144,10 @@ public class StoryController {
|
||||
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));
|
||||
}
|
||||
@@ -163,6 +165,10 @@ public class StoryController {
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
@@ -707,6 +715,38 @@ 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<Map<String, Object>> checkDuplicate(
|
||||
@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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user