Compare commits
4 Commits
cc4c8105ef
...
d398ac8f06
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d398ac8f06 | ||
|
|
331df77d28 | ||
|
|
7d8cdd2be8 | ||
|
|
9938d4649b |
@ -1,12 +1,16 @@
|
||||
package com.magamochi.catalog.controller;
|
||||
|
||||
import com.magamochi.catalog.model.dto.MangaIngestReviewDTO;
|
||||
import com.magamochi.catalog.model.dto.ManualMangaIngestRequestDTO;
|
||||
import com.magamochi.catalog.service.MangaIngestReviewService;
|
||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/catalog")
|
||||
@ -50,4 +54,20 @@ public class MangaIngestReviewController {
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Resolve manga ingest review manually",
|
||||
description =
|
||||
"Resolve a manga ingest review by manually providing all manga data and a cover image.",
|
||||
tags = {"Manga Ingest Review"},
|
||||
operationId = "resolveMangaIngestReviewManually")
|
||||
@PostMapping(value = "/ingest-reviews/manual", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public DefaultResponseDTO<Void> resolveMangaIngestReviewManually(
|
||||
@RequestParam Long id,
|
||||
@RequestPart MultipartFile coverImage,
|
||||
@RequestPart @Valid ManualMangaIngestRequestDTO request) {
|
||||
mangaIngestReviewService.resolveIngestReviewManually(id, coverImage, request);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package com.magamochi.catalog.model.dto;
|
||||
|
||||
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
|
||||
@Builder
|
||||
public record ManualMangaIngestRequestDTO(
|
||||
@NotBlank String title,
|
||||
String synopsis,
|
||||
MangaStatus status,
|
||||
Double score,
|
||||
OffsetDateTime publishedFrom,
|
||||
OffsetDateTime publishedTo,
|
||||
Integer chapterCount,
|
||||
Boolean adult,
|
||||
List<String> authors,
|
||||
List<String> genres,
|
||||
List<String> alternativeTitles) {}
|
||||
@ -8,6 +8,7 @@ import com.magamochi.catalog.client.AniListClient;
|
||||
import com.magamochi.catalog.model.dto.MangaDataDTO;
|
||||
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
||||
import com.magamochi.catalog.util.DoubleUtil;
|
||||
import com.magamochi.common.config.ExternalApiCircuitBreaker;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
@ -26,50 +27,77 @@ import org.springframework.stereotype.Service;
|
||||
public class AniListService {
|
||||
private final AniListClient aniListClient;
|
||||
private final RateLimiter aniListRateLimiter;
|
||||
private final ExternalApiCircuitBreaker circuitBreaker;
|
||||
|
||||
public Map<String, AniListClient.Manga> searchMangaByTitle(String title) {
|
||||
var request = getSearchGraphQLRequest(title);
|
||||
return circuitBreaker.execute(
|
||||
"aniList",
|
||||
() -> {
|
||||
var request = getSearchGraphQLRequest(title);
|
||||
|
||||
aniListRateLimiter.acquire();
|
||||
var response = aniListClient.searchManga(request);
|
||||
aniListRateLimiter.acquire();
|
||||
var response = aniListClient.searchManga(request);
|
||||
|
||||
if (nonNull(response) && nonNull(response.data()) && nonNull(response.data().page())) {
|
||||
return response.data().page().media().stream()
|
||||
.flatMap(
|
||||
manga ->
|
||||
Stream.of(
|
||||
manga.title().romaji(),
|
||||
manga.title().english(),
|
||||
manga.title().nativeTitle())
|
||||
.filter(Objects::nonNull)
|
||||
.filter(t -> !t.isBlank())
|
||||
.map(titleString -> Map.entry(titleString, manga)))
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
Map.Entry::getKey, Map.Entry::getValue, (existingManga, manga) -> existingManga));
|
||||
}
|
||||
if (nonNull(response) && nonNull(response.data()) && nonNull(response.data().page())) {
|
||||
return response.data().page().media().stream()
|
||||
.flatMap(
|
||||
manga ->
|
||||
Stream.of(
|
||||
manga.title().romaji(),
|
||||
manga.title().english(),
|
||||
manga.title().nativeTitle())
|
||||
.filter(Objects::nonNull)
|
||||
.filter(t -> !t.isBlank())
|
||||
.map(titleString -> Map.entry(titleString, manga)))
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
Map.Entry::getValue,
|
||||
(existingManga, manga) -> existingManga));
|
||||
}
|
||||
|
||||
return Map.of();
|
||||
return Map.of();
|
||||
},
|
||||
throwable -> {
|
||||
log.error("AniList circuit breaker open for search '{}'", title);
|
||||
return Map.of();
|
||||
});
|
||||
}
|
||||
|
||||
public Map<Long, MangaDataDTO> getMangasByTitle(String title) {
|
||||
var request = getSearchGraphQLRequest(title);
|
||||
return circuitBreaker.execute(
|
||||
"aniList",
|
||||
() -> {
|
||||
var request = getSearchGraphQLRequest(title);
|
||||
|
||||
aniListRateLimiter.acquire();
|
||||
var response = aniListClient.searchManga(request);
|
||||
aniListRateLimiter.acquire();
|
||||
var response = aniListClient.searchManga(request);
|
||||
|
||||
return response.data().page().media().stream()
|
||||
.map(this::parseMangaData)
|
||||
.flatMap(mangaDataMap -> mangaDataMap.entrySet().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
return response.data().page().media().stream()
|
||||
.map(this::parseMangaData)
|
||||
.flatMap(mangaDataMap -> mangaDataMap.entrySet().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
},
|
||||
throwable -> {
|
||||
log.error("AniList circuit breaker open for search '{}'", title);
|
||||
return Map.of();
|
||||
});
|
||||
}
|
||||
|
||||
public MangaDataDTO getMangaDataById(Long aniListId) {
|
||||
var request = getGraphQLRequest(aniListId);
|
||||
return circuitBreaker.execute(
|
||||
"aniList",
|
||||
() -> {
|
||||
var request = getGraphQLRequest(aniListId);
|
||||
|
||||
aniListRateLimiter.acquire();
|
||||
var manga = aniListClient.getManga(request).data().Media();
|
||||
return parseMangaData(manga).get(aniListId);
|
||||
aniListRateLimiter.acquire();
|
||||
var manga = aniListClient.getManga(request).data().Media();
|
||||
return parseMangaData(manga).get(aniListId);
|
||||
},
|
||||
throwable -> {
|
||||
log.error("AniList circuit breaker open for manga ID {}", aniListId);
|
||||
throw new RuntimeException("AniList API unavailable");
|
||||
});
|
||||
}
|
||||
|
||||
private Map<Long, MangaDataDTO> parseMangaData(AniListClient.Manga manga) {
|
||||
|
||||
@ -1,18 +1,35 @@
|
||||
package com.magamochi.catalog.service;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.magamochi.catalog.model.dto.MangaIngestReviewDTO;
|
||||
import com.magamochi.catalog.model.dto.ManualMangaIngestRequestDTO;
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import com.magamochi.catalog.model.entity.MangaAlternativeTitle;
|
||||
import com.magamochi.catalog.model.entity.MangaAuthor;
|
||||
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||
import com.magamochi.catalog.model.entity.MangaGenre;
|
||||
import com.magamochi.catalog.model.entity.MangaIngestReview;
|
||||
import com.magamochi.catalog.model.enumeration.MangaState;
|
||||
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
||||
import com.magamochi.catalog.model.repository.MangaIngestReviewRepository;
|
||||
import com.magamochi.catalog.model.repository.MangaRepository;
|
||||
import com.magamochi.common.exception.NotFoundException;
|
||||
import com.magamochi.image.model.entity.Image;
|
||||
import com.magamochi.image.service.ImageService;
|
||||
import com.magamochi.image.util.ImageHashUtil;
|
||||
import com.magamochi.ingestion.service.ContentProviderService;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.apache.tika.Tika;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Log4j2
|
||||
@Service
|
||||
@ -20,9 +37,15 @@ import org.springframework.stereotype.Service;
|
||||
public class MangaIngestReviewService {
|
||||
private final MangaResolutionService mangaResolutionService;
|
||||
private final ContentProviderService contentProviderService;
|
||||
private final ImageService imageService;
|
||||
private final GenreService genreService;
|
||||
private final AuthorService authorService;
|
||||
|
||||
private final MangaIngestReviewRepository mangaIngestReviewRepository;
|
||||
private final MangaContentProviderRepository mangaContentProviderRepository;
|
||||
private final MangaRepository mangaRepository;
|
||||
|
||||
private final Tika tika = new Tika();
|
||||
|
||||
public List<MangaIngestReviewDTO> get() {
|
||||
return mangaIngestReviewRepository.findAll().stream().map(MangaIngestReviewDTO::from).toList();
|
||||
@ -90,6 +113,121 @@ public class MangaIngestReviewService {
|
||||
contentProviderId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void resolveIngestReviewManually(
|
||||
Long reviewId, MultipartFile coverImage, ManualMangaIngestRequestDTO dto) {
|
||||
if (coverImage.isEmpty()) {
|
||||
throw new IllegalArgumentException("Cover image is required");
|
||||
}
|
||||
|
||||
var ingestReview = get(reviewId);
|
||||
var contentProviderId = ingestReview.getContentProvider().getId();
|
||||
|
||||
if (mangaContentProviderRepository.existsByMangaTitleIgnoreCaseAndContentProvider_Id(
|
||||
dto.title(), contentProviderId)) {
|
||||
log.info(
|
||||
"Manga with title '{}' already exists for provider '{}', skipping ingest",
|
||||
dto.title(),
|
||||
contentProviderId);
|
||||
return;
|
||||
}
|
||||
|
||||
var contentProvider = contentProviderService.find(contentProviderId);
|
||||
|
||||
var image = uploadCoverImage(coverImage);
|
||||
|
||||
var unsavedManga = buildManga(dto, image);
|
||||
var manga = mangaRepository.save(unsavedManga);
|
||||
|
||||
if (nonNull(dto.genres())) {
|
||||
var genres =
|
||||
dto.genres().stream()
|
||||
.map(name -> MangaGenre.builder().manga(manga).genre(genreService.findOrCreateGenre(name)).build())
|
||||
.toList();
|
||||
manga.setMangaGenres(genres);
|
||||
}
|
||||
|
||||
if (nonNull(dto.authors())) {
|
||||
var authors =
|
||||
dto.authors().stream()
|
||||
.map(
|
||||
name ->
|
||||
MangaAuthor.builder().manga(manga).author(authorService.findOrCreateAuthor(name)).build())
|
||||
.toList();
|
||||
manga.setMangaAuthors(authors);
|
||||
}
|
||||
|
||||
if (nonNull(dto.alternativeTitles())) {
|
||||
var titles =
|
||||
dto.alternativeTitles().stream()
|
||||
.map(title -> MangaAlternativeTitle.builder().title(title).manga(manga).build())
|
||||
.toList();
|
||||
manga.setAlternativeTitles(titles);
|
||||
}
|
||||
|
||||
mangaContentProviderRepository.save(
|
||||
MangaContentProvider.builder()
|
||||
.manga(manga)
|
||||
.mangaTitle(dto.title())
|
||||
.contentProvider(contentProvider)
|
||||
.url(ingestReview.getUrl())
|
||||
.build());
|
||||
|
||||
mangaIngestReviewRepository.delete(ingestReview);
|
||||
|
||||
log.info(
|
||||
"Successfully ingested manga '{}' manually from provider {}",
|
||||
dto.title(),
|
||||
contentProviderId);
|
||||
}
|
||||
|
||||
private Image uploadCoverImage(MultipartFile coverImage) {
|
||||
try {
|
||||
var bytes = coverImage.getBytes();
|
||||
var contentType = resolveContentType(coverImage.getContentType(), bytes);
|
||||
var hash = ImageHashUtil.computeSha256(bytes);
|
||||
var imageId = imageService.upload(bytes, contentType, "cover", hash);
|
||||
return imageService.find(imageId);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to read cover image bytes", e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 algorithm not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveContentType(String headerContentType, byte[] fileBytes) {
|
||||
if (nonNull(headerContentType) && headerContentType.startsWith("image/")) {
|
||||
return headerContentType;
|
||||
}
|
||||
|
||||
return tika.detect(fileBytes);
|
||||
}
|
||||
|
||||
private Manga buildManga(ManualMangaIngestRequestDTO dto, Image coverImage) {
|
||||
var builder = Manga.builder()
|
||||
.title(dto.title())
|
||||
.synopsis(dto.synopsis())
|
||||
.status(dto.status())
|
||||
.score(dto.score())
|
||||
.publishedFrom(dto.publishedFrom())
|
||||
.publishedTo(dto.publishedTo())
|
||||
.coverImage(coverImage)
|
||||
.state(MangaState.AVAILABLE)
|
||||
.mangaAuthors(Collections.emptyList())
|
||||
.mangaGenres(Collections.emptyList())
|
||||
.alternativeTitles(Collections.emptyList());
|
||||
|
||||
if (nonNull(dto.chapterCount())) {
|
||||
builder.chapterCount(dto.chapterCount());
|
||||
}
|
||||
|
||||
if (nonNull(dto.adult())) {
|
||||
builder.adult(dto.adult());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private MangaIngestReview get(Long id) {
|
||||
return mangaIngestReviewRepository
|
||||
.findById(id)
|
||||
|
||||
@ -157,11 +157,13 @@ public class MangaResolutionService {
|
||||
mangaRepository.save(
|
||||
Manga.builder().title(title).aniListId(aniListId).malId(malId).build());
|
||||
|
||||
try {
|
||||
mangaUpdateProducer.sendMangaUpdateCommand(new MangaUpdateCommand(manga.getId()));
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to send manga update command for manga '{}' (ID: {})", title, manga.getId(), e);
|
||||
if (nonNull(aniListId) || nonNull(malId)) {
|
||||
try {
|
||||
mangaUpdateProducer.sendMangaUpdateCommand(new MangaUpdateCommand(manga.getId()));
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to send manga update command for manga '{}' (ID: {})", title, manga.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return manga;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.magamochi.catalog.service;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.magamochi.catalog.model.dto.MangaDataDTO;
|
||||
@ -49,6 +50,12 @@ public class MangaUpdateService {
|
||||
log.info("Updating manga with ID {}", mangaId);
|
||||
|
||||
var manga = mangaService.find(mangaId);
|
||||
|
||||
if (isNull(manga.getAniListId()) && isNull(manga.getMalId())) {
|
||||
log.info("Skipping update for manga {}: no external IDs available", mangaId);
|
||||
return;
|
||||
}
|
||||
|
||||
var mangaData = fetchExternalMangaData(manga);
|
||||
applyUpdatesToDatabase(manga, mangaData);
|
||||
|
||||
@ -95,8 +102,7 @@ public class MangaUpdateService {
|
||||
throw (RuntimeException) fetchException;
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"Cannot update manga: No external provider IDs found for Manga " + manga.getId());
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyUpdatesToDatabase(Manga manga, MangaDataDTO mangaData) {
|
||||
|
||||
@ -8,6 +8,7 @@ import com.magamochi.catalog.client.JikanClient;
|
||||
import com.magamochi.catalog.model.dto.MangaDataDTO;
|
||||
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
||||
import com.magamochi.catalog.util.DoubleUtil;
|
||||
import com.magamochi.common.config.ExternalApiCircuitBreaker;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -20,45 +21,70 @@ import org.springframework.stereotype.Service;
|
||||
public class MyAnimeListService {
|
||||
private final JikanClient jikanClient;
|
||||
private final RateLimiter jikanRateLimiter;
|
||||
private final ExternalApiCircuitBreaker circuitBreaker;
|
||||
|
||||
/// Searches for manga titles on MyAnimeList using the Jikan API and returns a map of title to MAL
|
||||
// ID.
|
||||
public Map<String, Long> searchMangaByTitle(String titleToSearch) {
|
||||
jikanRateLimiter.acquire();
|
||||
var results = jikanClient.mangaSearch(titleToSearch).data();
|
||||
if (results.isEmpty()) {
|
||||
log.warn("No manga found with title {}", titleToSearch);
|
||||
return Map.of();
|
||||
}
|
||||
return circuitBreaker.execute(
|
||||
"myAnimeList",
|
||||
() -> {
|
||||
jikanRateLimiter.acquire();
|
||||
var results = jikanClient.mangaSearch(titleToSearch).data();
|
||||
if (results.isEmpty()) {
|
||||
log.warn("No manga found with title {}", titleToSearch);
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
return results.stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
JikanClient.MangaData::title,
|
||||
JikanClient.MangaData::mal_id,
|
||||
(existing, second) -> existing));
|
||||
return results.stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
JikanClient.MangaData::title,
|
||||
JikanClient.MangaData::mal_id,
|
||||
(existing, second) -> existing));
|
||||
},
|
||||
throwable -> {
|
||||
log.error("MyAnimeList circuit breaker open for search '{}'", titleToSearch);
|
||||
return Map.of();
|
||||
});
|
||||
}
|
||||
|
||||
public Map<Long, MangaDataDTO> getMangasByTitle(String title) {
|
||||
jikanRateLimiter.acquire();
|
||||
var response = jikanClient.mangaSearch(title).data();
|
||||
return circuitBreaker.execute(
|
||||
"myAnimeList",
|
||||
() -> {
|
||||
jikanRateLimiter.acquire();
|
||||
var response = jikanClient.mangaSearch(title).data();
|
||||
|
||||
return response.stream()
|
||||
.map(this::parseMangaData)
|
||||
.flatMap(mangaDataMap -> mangaDataMap.entrySet().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
return response.stream()
|
||||
.map(this::parseMangaData)
|
||||
.flatMap(mangaDataMap -> mangaDataMap.entrySet().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
},
|
||||
throwable -> {
|
||||
log.error("MyAnimeList circuit breaker open for search '{}'", title);
|
||||
return Map.of();
|
||||
});
|
||||
}
|
||||
|
||||
public MangaDataDTO getMangaDataById(Long malId) {
|
||||
jikanRateLimiter.acquire();
|
||||
var response = jikanClient.getMangaById(malId);
|
||||
if (isNull(response) || isNull(response.data())) {
|
||||
log.warn("No manga found with MAL ID {}", malId);
|
||||
return null;
|
||||
}
|
||||
return circuitBreaker.execute(
|
||||
"myAnimeList",
|
||||
() -> {
|
||||
jikanRateLimiter.acquire();
|
||||
var response = jikanClient.getMangaById(malId);
|
||||
if (isNull(response) || isNull(response.data())) {
|
||||
log.warn("No manga found with MAL ID {}", malId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var responseData = response.data();
|
||||
return parseMangaData(responseData).get(malId);
|
||||
var responseData = response.data();
|
||||
return parseMangaData(responseData).get(malId);
|
||||
},
|
||||
throwable -> {
|
||||
log.error("MyAnimeList circuit breaker open for manga ID {}", malId);
|
||||
throw new RuntimeException("MyAnimeList API unavailable");
|
||||
});
|
||||
}
|
||||
|
||||
private Map<Long, MangaDataDTO> parseMangaData(JikanClient.MangaData data) {
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package com.magamochi.common.config;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Log4j2
|
||||
@Component
|
||||
public class ExternalApiCircuitBreaker {
|
||||
|
||||
private static final long OPEN_DURATION_SECONDS = 30;
|
||||
|
||||
private final ConcurrentHashMap<String, AtomicReference<Instant>> circuits =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
public <T> T execute(String name, Callable<T> callable, Function<Throwable, T> fallback) {
|
||||
var circuit = circuits.computeIfAbsent(name, k -> new AtomicReference<>(Instant.MIN));
|
||||
var now = Instant.now();
|
||||
var openUntil = circuit.get();
|
||||
|
||||
if (now.isBefore(openUntil)) {
|
||||
log.warn("Circuit '{}' OPEN — failing fast", name);
|
||||
return fallback.apply(null);
|
||||
}
|
||||
|
||||
try {
|
||||
T result = callable.call();
|
||||
circuit.set(Instant.MIN);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
circuit.set(now.plusSeconds(OPEN_DURATION_SECONDS));
|
||||
log.error("Circuit '{}' OPEN for {}s: {}", name, OPEN_DURATION_SECONDS, e.getMessage());
|
||||
return fallback.apply(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,12 +4,12 @@ import static java.util.Objects.nonNull;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.magamochi.common.model.enumeration.ContentType;
|
||||
import com.magamochi.image.util.ImageHashUtil;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -54,7 +54,7 @@ public class ImageFetchService {
|
||||
throws NoSuchAlgorithmException {
|
||||
var fileContentType = resolveContentType(httpResponse, imageBytes);
|
||||
|
||||
var fileHash = computeHash(imageBytes);
|
||||
var fileHash = ImageHashUtil.computeSha256(imageBytes);
|
||||
|
||||
return imageManagerService.upload(
|
||||
imageBytes, fileContentType, contentType.name().toLowerCase(), fileHash);
|
||||
@ -76,22 +76,4 @@ public class ImageFetchService {
|
||||
|
||||
return tika.detect(fileBytes);
|
||||
}
|
||||
|
||||
private String computeHash(byte[] content) throws NoSuchAlgorithmException {
|
||||
var digest = MessageDigest.getInstance("SHA-256");
|
||||
var hashBytes = digest.digest(content);
|
||||
var hexString = new StringBuilder(2 * hashBytes.length);
|
||||
|
||||
for (byte b : hashBytes) {
|
||||
var hex = Integer.toHexString(0xff & b);
|
||||
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
|
||||
hexString.append(hex);
|
||||
}
|
||||
|
||||
return hexString.toString();
|
||||
}
|
||||
}
|
||||
|
||||
26
src/main/java/com/magamochi/image/util/ImageHashUtil.java
Normal file
26
src/main/java/com/magamochi/image/util/ImageHashUtil.java
Normal file
@ -0,0 +1,26 @@
|
||||
package com.magamochi.image.util;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
public class ImageHashUtil {
|
||||
public static String computeSha256(byte[] content) throws NoSuchAlgorithmException {
|
||||
var digest = MessageDigest.getInstance("SHA-256");
|
||||
var hashBytes = digest.digest(content);
|
||||
var hexString = new StringBuilder(2 * hashBytes.length);
|
||||
|
||||
for (byte b : hashBytes) {
|
||||
var hex = Integer.toHexString(0xff & b);
|
||||
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
|
||||
hexString.append(hex);
|
||||
}
|
||||
|
||||
return hexString.toString();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user