feat/image-exclusion-list #57
@ -3,6 +3,7 @@ package com.magamochi.catalog.model.repository;
|
|||||||
import com.magamochi.catalog.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
@ -15,4 +16,6 @@ public interface MangaRepository
|
|||||||
Optional<Manga> findByMalId(Long malId);
|
Optional<Manga> findByMalId(Long malId);
|
||||||
|
|
||||||
Optional<Manga> findByAniListId(Long aniListId);
|
Optional<Manga> findByAniListId(Long aniListId);
|
||||||
|
|
||||||
|
List<Manga> findAllByCoverImage_Id(UUID imageId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
package com.magamochi.content.model.repository;
|
package com.magamochi.content.model.repository;
|
||||||
|
|
||||||
import com.magamochi.content.model.entity.MangaContent;
|
|
||||||
import com.magamochi.content.model.entity.MangaContentImage;
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface MangaContentImageRepository extends JpaRepository<MangaContentImage, Long> {
|
public interface MangaContentImageRepository extends JpaRepository<MangaContentImage, Long> {
|
||||||
List<MangaContentImage> findAllByMangaContent(MangaContent mangaContent);
|
|
||||||
|
|
||||||
boolean existsByMangaContent_IdAndPosition(Long mangaContentId, int position);
|
boolean existsByMangaContent_IdAndPosition(Long mangaContentId, int position);
|
||||||
|
|
||||||
|
List<MangaContentImage> findAllByImage_Id(UUID imageId);
|
||||||
|
|
||||||
|
List<MangaContentImage> findAllByMangaContent_IdOrderByPositionAsc(Long mangaContentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.magamochi.controller;
|
|||||||
|
|
||||||
import com.magamochi.client.NtfyClient;
|
import com.magamochi.client.NtfyClient;
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
|
import com.magamochi.image.service.ImageExclusionService;
|
||||||
import com.magamochi.image.task.ImageCleanupTask;
|
import com.magamochi.image.task.ImageCleanupTask;
|
||||||
import com.magamochi.user.repository.UserRepository;
|
import com.magamochi.user.repository.UserRepository;
|
||||||
import com.magamochi.userinteraction.task.MangaFollowUpdateTask;
|
import com.magamochi.userinteraction.task.MangaFollowUpdateTask;
|
||||||
@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ManagementController {
|
public class ManagementController {
|
||||||
private final ImageCleanupTask imageCleanupTask;
|
private final ImageCleanupTask imageCleanupTask;
|
||||||
|
private final ImageExclusionService imageExclusionService;
|
||||||
private final MangaFollowUpdateTask mangaFollowUpdateTask;
|
private final MangaFollowUpdateTask mangaFollowUpdateTask;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final NtfyClient ntfyClient;
|
private final NtfyClient ntfyClient;
|
||||||
@ -30,6 +32,18 @@ public class ManagementController {
|
|||||||
return DefaultResponseDTO.ok().build();
|
return DefaultResponseDTO.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Cleanup excluded images",
|
||||||
|
description = "Triggers the cleanup of images that are in the exclusion list",
|
||||||
|
tags = {"Management"},
|
||||||
|
operationId = "imageExclusionCleanup")
|
||||||
|
@PostMapping("image-exclusion-cleanup")
|
||||||
|
public DefaultResponseDTO<Void> imageExclusionCleanup() {
|
||||||
|
imageExclusionService.cleanupExcludedImages();
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Trigger user follow update",
|
summary = "Trigger user follow update",
|
||||||
description = "Trigger user follow update",
|
description = "Trigger user follow update",
|
||||||
|
|||||||
@ -2,17 +2,22 @@ package com.magamochi.image.service;
|
|||||||
|
|
||||||
import static java.util.Objects.isNull;
|
import static java.util.Objects.isNull;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.repository.MangaRepository;
|
||||||
import com.magamochi.content.model.entity.MangaContentImage;
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
|
import com.magamochi.content.model.repository.MangaContentImageRepository;
|
||||||
import com.magamochi.image.model.entity.ImageExclusion;
|
import com.magamochi.image.model.entity.ImageExclusion;
|
||||||
import com.magamochi.image.model.repository.ImageExclusionRepository;
|
import com.magamochi.image.model.repository.ImageExclusionRepository;
|
||||||
|
import com.magamochi.image.model.repository.ImageRepository;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Log4j2
|
@Log4j2
|
||||||
@ -20,8 +25,12 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ImageExclusionService {
|
public class ImageExclusionService {
|
||||||
private final ImageService imageService;
|
private final ImageService imageService;
|
||||||
|
private final S3Service s3Service;
|
||||||
|
|
||||||
private final ImageExclusionRepository exclusionRepository;
|
private final ImageExclusionRepository exclusionRepository;
|
||||||
|
private final ImageRepository imageRepository;
|
||||||
|
private final MangaRepository mangaRepository;
|
||||||
|
private final MangaContentImageRepository mangaContentImageRepository;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void excludeImageByUuid(UUID imageId) {
|
public void excludeImageByUuid(UUID imageId) {
|
||||||
@ -55,4 +64,80 @@ public class ImageExclusionService {
|
|||||||
.filter(img -> !excludedHashes.contains(img.getImage().getFileHash()))
|
.filter(img -> !excludedHashes.contains(img.getImage().getFileHash()))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void cleanupExcludedImages() {
|
||||||
|
log.info("Starting cleanup of excluded images...");
|
||||||
|
|
||||||
|
var exclusions = exclusionRepository.findAll();
|
||||||
|
log.info("Found {} excluded hashes to check", exclusions.size());
|
||||||
|
|
||||||
|
var keysToDelete =
|
||||||
|
exclusions.stream()
|
||||||
|
.map(exclusion -> processSingleExclusion(exclusion.getFileHash()))
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (!keysToDelete.isEmpty()) {
|
||||||
|
log.info("Deleting {} objects from S3", keysToDelete.size());
|
||||||
|
s3Service.deleteObjects(keysToDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Finished cleanup of excluded images.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public Optional<String> processSingleExclusion(String fileHash) {
|
||||||
|
var imageOpt = imageRepository.findByFileHash(fileHash);
|
||||||
|
if (imageOpt.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
var image = imageOpt.get();
|
||||||
|
var imageId = image.getId();
|
||||||
|
var objectKey = image.getObjectKey();
|
||||||
|
|
||||||
|
log.info("Processing exclusion for image {} (hash: {})", imageId, fileHash);
|
||||||
|
|
||||||
|
// 1. Remove from Manga covers
|
||||||
|
var mangasWithCover = mangaRepository.findAllByCoverImage_Id(imageId);
|
||||||
|
if (!mangasWithCover.isEmpty()) {
|
||||||
|
log.info("Removing image from {} manga covers", mangasWithCover.size());
|
||||||
|
mangasWithCover.forEach(
|
||||||
|
manga -> {
|
||||||
|
manga.setCoverImage(null);
|
||||||
|
mangaRepository.save(manga);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Remove from Manga content and reindex
|
||||||
|
var contentImages = mangaContentImageRepository.findAllByImage_Id(imageId);
|
||||||
|
if (!contentImages.isEmpty()) {
|
||||||
|
log.info("Removing image from {} manga content entries", contentImages.size());
|
||||||
|
contentImages.forEach(
|
||||||
|
mci -> {
|
||||||
|
var mangaContent = mci.getMangaContent();
|
||||||
|
mangaContentImageRepository.delete(mci);
|
||||||
|
reindexMangaContent(mangaContent.getId());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
imageRepository.delete(image);
|
||||||
|
|
||||||
|
return Optional.of(objectKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reindexMangaContent(Long mangaContentId) {
|
||||||
|
var remainingImages =
|
||||||
|
mangaContentImageRepository.findAllByMangaContent_IdOrderByPositionAsc(mangaContentId);
|
||||||
|
|
||||||
|
for (var i = 0; i < remainingImages.size(); i++) {
|
||||||
|
var img = remainingImages.get(i);
|
||||||
|
if (img.getPosition() != i) {
|
||||||
|
img.setPosition(i);
|
||||||
|
mangaContentImageRepository.save(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.magamochi.image.task;
|
||||||
|
|
||||||
|
import com.magamochi.image.service.ImageExclusionService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ExclusionCleanupTask {
|
||||||
|
@Value("${image-service.exclusion-cleanup-enabled:true}")
|
||||||
|
private Boolean cleanupEnabled;
|
||||||
|
|
||||||
|
private final ImageExclusionService exclusionService;
|
||||||
|
|
||||||
|
@Scheduled(cron = "${image-service.exclusion-cleanup-cron:@weekly}")
|
||||||
|
public void cleanupExcludedImagesScheduled() {
|
||||||
|
if (!cleanupEnabled) {
|
||||||
|
log.info("Excluded image cleanup is disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exclusionService.cleanupExcludedImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -90,6 +90,8 @@ routing-key:
|
|||||||
image-service:
|
image-service:
|
||||||
clean-up-enabled: ${IMAGE_SERVICE_CLEAN_UP_ENABLED:false}
|
clean-up-enabled: ${IMAGE_SERVICE_CLEAN_UP_ENABLED:false}
|
||||||
cron-expression: "@weekly"
|
cron-expression: "@weekly"
|
||||||
|
exclusion-cleanup-enabled: ${IMAGE_SERVICE_EXCLUSION_CLEANUP_ENABLED:true}
|
||||||
|
exclusion-cleanup-cron: "@weekly"
|
||||||
|
|
||||||
content-providers:
|
content-providers:
|
||||||
update-enabled: ${CONTENT_PROVIDER_UPDATE_ENABLED:false}
|
update-enabled: ${CONTENT_PROVIDER_UPDATE_ENABLED:false}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user