From 424b8e28b217ea84177b4070d722c838f17a327f Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Wed, 24 Jun 2026 08:59:37 -0300 Subject: [PATCH] feat(manga): enhance ingest process with error handling --- .../catalog/service/MangaIngestService.java | 28 ++++++- .../service/MangaResolutionService.java | 80 +++++++++++++++++-- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/magamochi/catalog/service/MangaIngestService.java b/src/main/java/com/magamochi/catalog/service/MangaIngestService.java index b2b0196..8931999 100644 --- a/src/main/java/com/magamochi/catalog/service/MangaIngestService.java +++ b/src/main/java/com/magamochi/catalog/service/MangaIngestService.java @@ -27,6 +27,20 @@ public class MangaIngestService { log.info( "Ingesting manga with mangaTitle '{}' from provider {}", mangaTitle, contentProviderId); + try { + doIngestManga(contentProviderId, mangaTitle, url); + } catch (Exception e) { + log.error( + "Unexpected error ingesting manga '{}' from provider {} at url '{}'", + mangaTitle, + contentProviderId, + url, + e); + throw e; + } + } + + private void doIngestManga(long contentProviderId, String mangaTitle, String url) { if (mangaContentProviderRepository.existsByMangaTitleIgnoreCaseAndContentProvider_Id( mangaTitle, contentProviderId)) { log.info( @@ -51,6 +65,17 @@ public class MangaIngestService { return; } + if (mangaContentProviderRepository + .findByManga_IdAndContentProvider_Id(manga.getId(), contentProviderId) + .isPresent()) { + log.info( + "MangaContentProvider already exists for manga '{}' (ID: {}) and provider '{}', skipping ingest", + manga.getTitle(), + manga.getId(), + contentProviderId); + return; + } + try { var contentProvider = contentProviderService.find(contentProviderId); @@ -63,10 +88,11 @@ public class MangaIngestService { .build()); } catch (Exception e) { log.error( - "Failed to ingest manga with mangaTitle '{}' from provider '{}'", + "Failed to save manga content provider for manga '{}' from provider '{}'", mangaTitle, contentProviderId, e); + return; } log.info( diff --git a/src/main/java/com/magamochi/catalog/service/MangaResolutionService.java b/src/main/java/com/magamochi/catalog/service/MangaResolutionService.java index 1f6332c..ee3e487 100644 --- a/src/main/java/com/magamochi/catalog/service/MangaResolutionService.java +++ b/src/main/java/com/magamochi/catalog/service/MangaResolutionService.java @@ -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.entity.Manga; @@ -9,6 +10,7 @@ import com.magamochi.catalog.queue.producer.MangaUpdateProducer; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; @Log4j2 @@ -116,21 +118,21 @@ public class MangaResolutionService { if (nonNull(aniListId)) { var existingByAniList = mangaRepository.findByAniListId(aniListId); if (existingByAniList.isPresent()) { - return existingByAniList.get(); + return mergeExternalIds(existingByAniList.get(), canonicalTitle, aniListId, malId); } } if (nonNull(malId)) { var existingByMalId = mangaRepository.findByMalId(malId); if (existingByMalId.isPresent()) { - return existingByMalId.get(); + return mergeExternalIds(existingByMalId.get(), canonicalTitle, aniListId, malId); } } if (nonNull(canonicalTitle)) { var existingByTitle = mangaRepository.findByTitleIgnoreCase(canonicalTitle); if (existingByTitle.isPresent()) { - return existingByTitle.get(); + return mergeExternalIds(existingByTitle.get(), canonicalTitle, aniListId, malId); } } @@ -138,13 +140,75 @@ public class MangaResolutionService { } private Manga createAndNotifyManga(String title, Long aniListId, Long malId) { - var manga = - mangaRepository.save( - Manga.builder().title(title).aniListId(aniListId).malId(malId).build()); + try { + var manga = + mangaRepository.save( + Manga.builder().title(title).aniListId(aniListId).malId(malId).build()); - mangaUpdateProducer.sendMangaUpdateCommand(new MangaUpdateCommand(manga.getId())); + 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; + return manga; + } catch (DataIntegrityViolationException e) { + log.warn( + "Race condition while creating manga '{}' (aniListId={}, malId={}), re-querying existing", + title, + aniListId, + malId); + + if (nonNull(aniListId)) { + var existing = mangaRepository.findByAniListId(aniListId); + if (existing.isPresent()) { + return mergeExternalIds(existing.get(), title, aniListId, malId); + } + } + if (nonNull(malId)) { + var existing = mangaRepository.findByMalId(malId); + if (existing.isPresent()) { + return mergeExternalIds(existing.get(), title, aniListId, malId); + } + } + if (nonNull(title)) { + var existing = mangaRepository.findByTitleIgnoreCase(title); + if (existing.isPresent()) { + return mergeExternalIds(existing.get(), title, aniListId, malId); + } + } + + throw e; + } + } + + private Manga mergeExternalIds(Manga existing, String canonicalTitle, Long aniListId, Long malId) { + boolean updated = false; + + if (isNull(existing.getAniListId()) && nonNull(aniListId)) { + existing.setAniListId(aniListId); + updated = true; + } + if (isNull(existing.getMalId()) && nonNull(malId)) { + existing.setMalId(malId); + updated = true; + } + if (isNull(existing.getTitle()) && nonNull(canonicalTitle)) { + existing.setTitle(canonicalTitle); + updated = true; + } + + if (updated) { + log.info( + "Merged external IDs into manga '{}' (ID: {}): aniListId={}, malId={}", + existing.getTitle(), + existing.getId(), + existing.getAniListId(), + existing.getMalId()); + return mangaRepository.save(existing); + } + + return existing; } private record ProviderResult(String title, Long externalId) {}