Skip to content

Commit da20227

Browse files
committed
feat: 아티스트 목록 저장
1 parent 12aebe0 commit da20227

3 files changed

Lines changed: 1007 additions & 0 deletions

File tree

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package com.back.web7_9_codecrete_be.domain.artists.service;
2+
3+
import com.back.web7_9_codecrete_be.domain.artists.entity.Artist;
4+
import com.back.web7_9_codecrete_be.domain.artists.repository.ArtistRepository;
5+
import com.back.web7_9_codecrete_be.global.musicbrainz.MusicBrainzClient;
6+
import com.back.web7_9_codecrete_be.global.wikidata.WikidataClient;
7+
import com.fasterxml.jackson.databind.JsonNode;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.data.domain.PageRequest;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Propagation;
13+
import org.springframework.transaction.annotation.Transactional;
14+
15+
import java.util.List;
16+
import java.util.Optional;
17+
18+
@Slf4j
19+
@Service
20+
@RequiredArgsConstructor
21+
public class ArtistEnrichService {
22+
23+
private final ArtistRepository artistRepository;
24+
private final MusicBrainzClient musicBrainzClient;
25+
private final WikidataClient wikidataClient;
26+
27+
28+
// Wikidata + Wikipedia + MusicBrainz를 통합하여 아티스트 정보를 가져와 enrich를 수행
29+
public int enrichArtist(int limit) {
30+
int actualLimit = limit > 0 ? Math.min(limit, 300) : 100;
31+
List<Artist> targets = artistRepository.findByNameKoIsNullOrderByIdAsc(
32+
PageRequest.of(0, actualLimit)
33+
);
34+
log.info("통합 enrich 시작 (Wikidata + Wikipedia + MusicBrainz): 요청 limit={}, 실제 limit={}, 대상 {}명",
35+
limit, actualLimit, targets.size());
36+
37+
if (targets.isEmpty()) {
38+
log.warn("⚠️ enrich할 대상 아티스트가 없습니다. (모두 이미 enrich되었거나 DB에 아티스트가 없습니다)");
39+
return 0;
40+
}
41+
int updated = 0;
42+
int failedNotFound = 0;
43+
int failedException = 0;
44+
45+
for (Artist artist : targets) {
46+
try {
47+
// 각 아티스트마다 별도 트랜잭션으로 처리하여 즉시 커밋
48+
enrichSingleArtist(artist);
49+
updated++;
50+
51+
// API rate limit 고려 (가장 느린 MusicBrainz 기준)
52+
// InterruptedException을 안전하게 처리
53+
try {
54+
Thread.sleep(1100);
55+
} catch (InterruptedException e) {
56+
Thread.currentThread().interrupt();
57+
log.warn("⚠️ Enrich 중 sleep 중단됨 (서버 종료 가능성): 처리된 개수={}", updated);
58+
// 이미 처리된 것은 저장되었으므로 break
59+
break;
60+
}
61+
62+
} catch (RuntimeException e) {
63+
// 아티스트 정보를 찾을 수 없는 경우
64+
if (e.getMessage() != null && e.getMessage().contains("아티스트 정보를 찾을 수 없음")) {
65+
failedNotFound++;
66+
} else {
67+
log.error("❌ Enrich 예외 발생: artistId={}, name={}, spotifyId={}, error={}",
68+
artist.getId(), artist.getArtistName(), artist.getSpotifyArtistId(), e.getMessage(), e);
69+
failedException++;
70+
}
71+
} catch (Exception e) {
72+
log.error("❌ Enrich 예외 발생: artistId={}, name={}, spotifyId={}, error={}",
73+
artist.getId(), artist.getArtistName(), artist.getSpotifyArtistId(), e.getMessage(), e);
74+
failedException++;
75+
}
76+
}
77+
78+
int totalFailed = failedNotFound + failedException;
79+
log.info("📊 통합 enrich 완료: 성공={}, 실패={} (정보없음={}, 예외={}), 총={}",
80+
updated, totalFailed, failedNotFound, failedException, targets.size());
81+
return updated;
82+
}
83+
84+
@Transactional(propagation = Propagation.REQUIRES_NEW)
85+
protected void enrichSingleArtist(Artist artist) {
86+
log.debug("Enrich 처리 중: artistId={}, name={}, spotifyId={}",
87+
artist.getId(), artist.getArtistName(), artist.getSpotifyArtistId());
88+
89+
EnrichResult result = enrichArtist(artist);
90+
91+
if (result == null) {
92+
log.warn("❌ 아티스트 정보를 찾을 수 없음: artistId={}, name={}, spotifyId={}",
93+
artist.getId(), artist.getArtistName(), artist.getSpotifyArtistId());
94+
throw new RuntimeException("아티스트 정보를 찾을 수 없음");
95+
}
96+
97+
// 기존 artistType이 있으면 유지, 없으면 가져온 값 사용
98+
String artistType = result.artistType != null ? result.artistType : artist.getArtistType();
99+
100+
// ✅ 기존 row를 "보강"
101+
artist.updateProfile(result.nameKo, result.artistGroup, artistType);
102+
// 명시적으로 save하여 변경사항을 DB에 즉시 반영
103+
artistRepository.save(artist);
104+
log.info("✅ Enrich 성공: artistId={}, name={}, nameKo={}, group={}, type={}, source={}",
105+
artist.getId(), artist.getArtistName(), result.nameKo,
106+
result.artistGroup, artistType, result.source);
107+
}
108+
109+
private EnrichResult enrichArtist(Artist artist) {
110+
String nameKo = null;
111+
String artistGroup = null;
112+
String artistType = null;
113+
String source = "";
114+
115+
// 1단계: Spotify ID로 Wikidata 찾기 (가장 정확)
116+
Optional<String> qidOpt = wikidataClient.searchWikidataIdBySpotifyId(artist.getSpotifyArtistId());
117+
if (qidOpt.isEmpty()) {
118+
// Spotify ID로 못 찾으면 이름으로 시도
119+
qidOpt = wikidataClient.searchWikidataId(artist.getArtistName());
120+
}
121+
122+
if (qidOpt.isPresent()) {
123+
String qid = qidOpt.get();
124+
Optional<JsonNode> entityOpt = wikidataClient.getEntityInfo(qid);
125+
126+
if (entityOpt.isPresent()) {
127+
JsonNode entity = entityOpt.get();
128+
129+
// Wikipedia에서 한국어 이름 가져오기
130+
Optional<String> nameKoOpt = wikidataClient.getKoreanNameFromWikipedia(entity);
131+
if (nameKoOpt.isPresent()) {
132+
nameKo = nameKoOpt.get();
133+
source += "Wikipedia ";
134+
}
135+
136+
// Wikidata에서 아티스트 타입 추출
137+
artistType = inferArtistTypeFromWikidata(entity);
138+
if (artistType != null) {
139+
source += "Wikidata ";
140+
}
141+
142+
// Wikidata에서 소속 그룹 추출
143+
artistGroup = resolveGroupNameFromWikidata(entity);
144+
if (artistGroup != null) {
145+
source += "Wikidata ";
146+
}
147+
}
148+
}
149+
150+
// 2단계: Wikipedia에서 직접 검색 (Wikidata 실패 시)
151+
if (nameKo == null) {
152+
Optional<String> nameKoOpt = wikidataClient.searchKoreanNameFromWikipedia(artist.getArtistName());
153+
if (nameKoOpt.isPresent()) {
154+
nameKo = nameKoOpt.get();
155+
source += "Wikipedia ";
156+
}
157+
}
158+
159+
// 3단계: MusicBrainz에서 추가 정보 가져오기 (보완, 실패해도 계속 진행)
160+
try {
161+
Optional<MusicBrainzClient.ArtistInfo> mbInfoOpt = musicBrainzClient.searchArtist(artist.getArtistName());
162+
if (mbInfoOpt.isPresent()) {
163+
MusicBrainzClient.ArtistInfo mbInfo = mbInfoOpt.get();
164+
165+
// 한국어 이름이 없으면 MusicBrainz에서 가져오기
166+
if (nameKo == null && mbInfo.getNameKo() != null) {
167+
nameKo = mbInfo.getNameKo();
168+
source += "MusicBrainz ";
169+
}
170+
171+
// 소속 그룹이 없으면 MusicBrainz에서 가져오기
172+
if (artistGroup == null && mbInfo.getArtistGroup() != null) {
173+
artistGroup = mbInfo.getArtistGroup();
174+
source += "MusicBrainz ";
175+
}
176+
177+
// 아티스트 타입이 없으면 MusicBrainz에서 가져오기
178+
if (artistType == null && mbInfo.getArtistType() != null) {
179+
artistType = mbInfo.getArtistType();
180+
source += "MusicBrainz ";
181+
}
182+
}
183+
} catch (Exception e) {
184+
// MusicBrainz 실패해도 Wikidata/Wikipedia 정보로는 계속 진행
185+
log.debug("MusicBrainz 정보 가져오기 실패 (무시하고 계속 진행): name={}, error={}",
186+
artist.getArtistName(), e.getMessage());
187+
}
188+
189+
// 최소한 한국어 이름은 있어야 성공으로 간주
190+
if (nameKo == null) {
191+
return null;
192+
}
193+
194+
return new EnrichResult(nameKo, artistGroup, artistType, source.trim());
195+
}
196+
197+
//Wikidata 엔티티에서 아티스트 타입 추출
198+
private String inferArtistTypeFromWikidata(JsonNode entity) {
199+
// P31 instance of: human(Q5), musical group(Q215380)
200+
List<String> instanceOfList = wikidataClient.getAllEntityIdClaims(entity, "P31");
201+
202+
// Q215380 (musical group)이 있으면 GROUP
203+
if (instanceOfList.contains("Q215380")) {
204+
return "GROUP";
205+
}
206+
207+
// P463 (member of) 속성이 있으면 그룹 멤버이므로 SOLO
208+
Optional<String> memberOf = wikidataClient.getEntityIdClaim(entity, "P463");
209+
if (memberOf.isPresent()) {
210+
return "SOLO";
211+
}
212+
213+
// Q5 (human)만 있으면 SOLO
214+
if (instanceOfList.contains("Q5") && instanceOfList.size() == 1) {
215+
return "SOLO";
216+
}
217+
218+
return null;
219+
}
220+
221+
// Wikidata 엔티티에서 소속 그룹 이름 추출
222+
private String resolveGroupNameFromWikidata(JsonNode artistEntity) {
223+
// P463 member of
224+
Optional<String> groupQid = wikidataClient.getEntityIdClaim(artistEntity, "P463");
225+
if (groupQid.isEmpty()) return null;
226+
227+
Optional<JsonNode> groupEntityOpt = wikidataClient.getEntityInfo(groupQid.get());
228+
if (groupEntityOpt.isEmpty()) return null;
229+
230+
// Wikipedia에서 그룹 이름 가져오기
231+
return wikidataClient.getKoreanNameFromWikipedia(groupEntityOpt.get()).orElse(null);
232+
}
233+
234+
private static class EnrichResult {
235+
final String nameKo;
236+
final String artistGroup;
237+
final String artistType;
238+
final String source;
239+
240+
EnrichResult(String nameKo, String artistGroup, String artistType, String source) {
241+
this.nameKo = nameKo;
242+
this.artistGroup = artistGroup;
243+
this.artistType = artistType;
244+
this.source = source;
245+
}
246+
}
247+
248+
}

0 commit comments

Comments
 (0)