Files
myListBridgeAPI/src/main/java/com/domain/service/VibeService.java
2025-11-28 15:34:48 +09:00

518 lines
22 KiB
Java

package com.domain.service;
import com.domain.request.VibeLoginRequest;
import com.domain.response.VibePlaylistResponse;
import com.domain.response.VibeTrackResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.playwright.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class VibeService {
private final ObjectMapper objectMapper;
private final VibeCookieService vibeCookieService;
private final com.global.util.EncryptionUtil encryptionUtil;
private static final String NAVER_LOGIN_URL = "https://nid.naver.com/nidlogin.login";
private static final String VIBE_PLAYLIST_API_URL = "https://apis.naver.com/vibeWeb/musicapiweb/vibe/v2/library/PLAYLIST/libraries?start=1&display=50";
private static final String VIBE_TRACK_API_URL = "https://apis.naver.com/vibeWeb/musicapiweb/myMusic/myAlbum/%s/tracks?start=1&display=500";
/**
* 네이버 로그인을 통해 Vibe 플레이리스트를 조회합니다.
* Redis에 저장된 쿠키를 우선 사용하고, 만료된 경우에만 재로그인합니다.
*
* @param request 네이버 아이디/비밀번호 (암호화됨)
* @return Vibe 플레이리스트 응답 (sessionId 포함)
*/
public VibePlaylistResponse getVibePlaylistsWithLogin(VibeLoginRequest request) {
try {
// 0. 아이디/비밀번호 복호화
String userId = encryptionUtil.decrypt(request.getId());
String password = encryptionUtil.decrypt(request.getPassword());
log.info("Vibe 플레이리스트 조회 요청 - userId: {}", userId);
// 1. Redis에서 기존 쿠키 조회
Map<String, String> cookies = vibeCookieService.getCookieByUserId(userId);
// 2. 쿠키가 있으면 유효성 검사
if (cookies != null && !cookies.isEmpty()) {
log.info("Redis에서 쿠키 발견 - 유효성 검사 수행");
if (validateVibeCookie(cookies)) {
log.info("쿠키 유효 - Redis 쿠키 사용");
} else {
log.warn("쿠키 만료됨 - 재로그인 필요");
cookies = null;
}
}
// 3. 쿠키가 없거나 만료된 경우 재로그인
if (cookies == null || cookies.isEmpty()) {
log.info("재로그인 시작 - userId: {}", userId);
cookies = loginToNaver(userId, password);
if (cookies.isEmpty()) {
return VibePlaylistResponse.builder()
.success(false)
.message("네이버 로그인에 실패했습니다.")
.build();
}
// 4. 로그인 성공 - Redis에 쿠키 저장
vibeCookieService.saveCookieByUserId(userId, cookies);
log.info("Redis에 쿠키 저장 완료 - userId: {}", userId);
}
// 5. 쿠키를 사용하여 Vibe 플레이리스트 조회
List<VibePlaylistResponse.Playlist> playlists = fetchVibePlaylists(cookies);
// 6. userId를 암호화하여 sessionId로 반환
String sessionId = encryptionUtil.encrypt(userId);
log.info("세션 ID 생성 완료 - userId: {}", userId);
return VibePlaylistResponse.builder()
.success(true)
.message("플레이리스트 조회 성공")
.sessionId(sessionId)
.playlists(playlists)
.build();
} catch (Exception e) {
log.error("Vibe 플레이리스트 조회 중 오류 발생", e);
return VibePlaylistResponse.builder()
.success(false)
.message("플레이리스트 조회 중 오류가 발생했습니다: " + e.getMessage())
.build();
}
}
/**
* Playwright를 사용하여 네이버 로그인을 수행하고 쿠키를 반환합니다.
*
* @param naverId 네이버 아이디
* @param naverPassword 네이버 비밀번호
* @return 쿠키 맵 (name -> value)
*/
private Map<String, String> loginToNaver(String naverId, String naverPassword) {
Map<String, String> cookieMap = new HashMap<>();
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setHeadless(true) // 브라우저를 백그라운드에서 실행
.setTimeout(60000)); // 60초 타임아웃
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.setViewportSize(1920, 1080));
Page page = context.newPage();
try {
log.info("네이버 로그인 페이지 접속");
page.navigate(NAVER_LOGIN_URL);
// 아이디 입력
page.fill("input#id", naverId);
log.debug("네이버 아이디 입력 완료");
// 비밀번호 입력
page.fill("input#pw", naverPassword);
log.debug("네이버 비밀번호 입력 완료");
// 로그인 버튼 클릭
page.click("button[type='submit']");
log.info("로그인 버튼 클릭");
// 로그인 완료 대기 (URL 변경 또는 특정 요소 확인)
try {
page.waitForURL("https://www.naver.com/**", new Page.WaitForURLOptions().setTimeout(10000));
log.info("네이버 로그인 성공");
} catch (TimeoutError e) {
// URL이 변경되지 않으면 로그인 실패 가능성
String currentUrl = page.url();
log.warn("로그인 후 URL: {}", currentUrl);
if (currentUrl.contains("nidlogin")) {
log.error("네이버 로그인 실패 - CAPTCHA 또는 인증 오류 가능성");
return cookieMap;
}
}
// 쿠키 추출
List<com.microsoft.playwright.options.Cookie> cookies = context.cookies();
for (com.microsoft.playwright.options.Cookie cookie : cookies) {
cookieMap.put(cookie.name, cookie.value);
log.debug("쿠키 획득: {} = {}", cookie.name, cookie.value);
}
log.info("총 {} 개의 쿠키 획득", cookieMap.size());
} catch (Exception e) {
log.error("네이버 로그인 중 오류 발생", e);
} finally {
page.close();
context.close();
browser.close();
}
} catch (Exception e) {
log.error("Playwright 초기화 중 오류 발생", e);
}
return cookieMap;
}
/**
* 쿠키의 유효성을 검사합니다.
* Vibe API를 호출하여 401/403 응답이 나오지 않으면 유효한 것으로 판단합니다.
*
* @param cookies 검사할 쿠키 맵
* @return 유효하면 true, 만료되었으면 false
*/
private boolean validateVibeCookie(Map<String, String> cookies) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(10, TimeUnit.SECONDS)
.setResponseTimeout(10, TimeUnit.SECONDS)
.build();
try (CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.build()) {
HttpGet getRequest = new HttpGet(VIBE_PLAYLIST_API_URL);
// 헤더 설정
getRequest.setHeader("Accept", "application/json");
getRequest.setHeader("Content-Type", "application/json");
// 쿠키 설정
StringBuilder cookieHeader = new StringBuilder();
for (Map.Entry<String, String> entry : cookies.entrySet()) {
if (cookieHeader.length() > 0) {
cookieHeader.append("; ");
}
cookieHeader.append(entry.getKey()).append("=").append(entry.getValue());
}
getRequest.setHeader("Cookie", cookieHeader.toString());
try (CloseableHttpResponse response = httpClient.execute(getRequest)) {
int statusCode = response.getCode();
// 401 Unauthorized 또는 403 Forbidden이면 쿠키 만료
if (statusCode == 401 || statusCode == 403) {
log.warn("쿠키 만료 감지 - 상태 코드: {}", statusCode);
return false;
}
// 200 OK 또는 기타 성공 응답이면 유효
if (statusCode >= 200 && statusCode < 300) {
log.debug("쿠키 유효성 검사 성공 - 상태 코드: {}", statusCode);
return true;
}
// 기타 에러는 유효하지 않은 것으로 처리
log.warn("쿠키 유효성 검사 실패 - 상태 코드: {}", statusCode);
return false;
}
} catch (Exception e) {
log.error("쿠키 유효성 검사 중 오류 발생", e);
return false; // 오류 발생 시 재로그인 유도
}
}
/**
* Vibe API를 호출하여 플레이리스트를 조회합니다.
*
* @param cookies 네이버 로그인 쿠키
* @return 플레이리스트 목록
*/
private List<VibePlaylistResponse.Playlist> fetchVibePlaylists(Map<String, String> cookies) {
List<VibePlaylistResponse.Playlist> playlists = new ArrayList<>();
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(30, TimeUnit.SECONDS)
.setResponseTimeout(30, TimeUnit.SECONDS)
.build();
try (CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.build()) {
HttpGet getRequest = new HttpGet(VIBE_PLAYLIST_API_URL);
getRequest.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
getRequest.setHeader("Referer", "https://vibe.naver.com/");
// 쿠키 헤더 추가
StringBuilder cookieHeader = new StringBuilder();
for (Map.Entry<String, String> entry : cookies.entrySet()) {
if (cookieHeader.length() > 0) {
cookieHeader.append("; ");
}
cookieHeader.append(entry.getKey()).append("=").append(entry.getValue());
}
getRequest.setHeader("Cookie", cookieHeader.toString());
try (CloseableHttpResponse response = httpClient.execute(getRequest)) {
int statusCode = response.getCode();
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
log.info("Vibe API 응답 코드: {}", statusCode);
log.info("Vibe API 응답 본문 길이: {} bytes", responseBody.length());
log.info("Vibe API 응답 본문 (처음 500자): {}", responseBody.length() > 500 ? responseBody.substring(0, 500) : responseBody);
if (statusCode == 200) {
// JSON 응답 파싱
playlists = parseVibePlaylistResponse(responseBody);
log.info("파싱된 플레이리스트 개수: {}", playlists.size());
} else {
log.error("Vibe API 호출 실패. 상태 코드: {}, 응답: {}", statusCode, responseBody);
}
}
} catch (Exception e) {
log.error("Vibe 플레이리스트 조회 중 오류 발생", e);
}
return playlists;
}
/**
* Vibe API 응답을 파싱합니다.
*
* @param responseBody JSON 응답 본문
* @return 플레이리스트 목록
*/
private List<VibePlaylistResponse.Playlist> parseVibePlaylistResponse(String responseBody) {
List<VibePlaylistResponse.Playlist> playlists = new ArrayList<>();
try {
JsonNode root = objectMapper.readTree(responseBody);
JsonNode resultNode = root.path("response").path("result");
if (resultNode.isObject()) {
JsonNode librariesNode = resultNode.path("libraries");
if (librariesNode.isArray()) {
for (JsonNode libraryNode : librariesNode) {
VibePlaylistResponse.Playlist playlist = VibePlaylistResponse.Playlist.builder()
.playlistId(libraryNode.path("id").asText())
.playlistName(libraryNode.path("title").asText())
.description(libraryNode.path("subtitle").asText(null))
.trackCount(libraryNode.path("trackCount").asInt(0))
.imageUrl(libraryNode.path("imageUrl").asText(null))
.createdAt(null) // API 응답에 없음
.updatedAt(null) // API 응답에 없음
.build();
playlists.add(playlist);
}
}
}
} catch (Exception e) {
log.error("Vibe 응답 파싱 중 오류 발생", e);
}
return playlists;
}
/**
* 세션 ID(암호화된 userId)를 사용하여 플레이리스트의 트랙 목록을 조회합니다.
*
* @param sessionId 세션 ID (암호화된 userId)
* @param playlistId 플레이리스트 ID
* @return Track 목록 응답
*/
public VibeTrackResponse getVibeTracks(String sessionId, String playlistId) {
try {
// 1. sessionId(암호화된 userId)를 복호화
String userId;
try {
userId = encryptionUtil.decrypt(sessionId);
log.info("세션 ID 복호화 성공 - userId: {}", userId);
} catch (Exception e) {
log.error("세션 ID 복호화 실패", e);
return VibeTrackResponse.builder()
.success(false)
.message("유효하지 않은 세션 ID입니다.")
.build();
}
// 2. Redis에서 userId로 쿠키 조회
Map<String, String> cookies = vibeCookieService.getCookieByUserId(userId);
if (cookies == null || cookies.isEmpty()) {
log.warn("쿠키를 찾을 수 없음 - userId: {}", userId);
return VibeTrackResponse.builder()
.success(false)
.message("세션이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요.")
.build();
}
// 3. Track API 호출
List<VibeTrackResponse.Track> tracks = fetchVibeTracks(cookies, playlistId);
return VibeTrackResponse.builder()
.success(true)
.message("트랙 목록 조회 성공")
.totalCount(tracks.size())
.tracks(tracks)
.build();
} catch (Exception e) {
log.error("Vibe 트랙 목록 조회 중 오류 발생", e);
return VibeTrackResponse.builder()
.success(false)
.message("트랙 목록 조회 중 오류가 발생했습니다: " + e.getMessage())
.build();
}
}
/**
* Vibe API를 호출하여 트랙 목록을 조회합니다.
*
* @param cookies 네이버 로그인 쿠키
* @param playlistId 플레이리스트 ID
* @return 트랙 목록
*/
private List<VibeTrackResponse.Track> fetchVibeTracks(Map<String, String> cookies, String playlistId) {
List<VibeTrackResponse.Track> tracks = new ArrayList<>();
String trackApiUrl = String.format(VIBE_TRACK_API_URL, playlistId);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(30, TimeUnit.SECONDS)
.setResponseTimeout(30, TimeUnit.SECONDS)
.build();
try (CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.build()) {
HttpGet getRequest = new HttpGet(trackApiUrl);
getRequest.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
getRequest.setHeader("Referer", "https://vibe.naver.com/");
getRequest.setHeader("Accept", "application/json");
// 쿠키 헤더 추가
StringBuilder cookieHeader = new StringBuilder();
for (Map.Entry<String, String> entry : cookies.entrySet()) {
if (cookieHeader.length() > 0) {
cookieHeader.append("; ");
}
cookieHeader.append(entry.getKey()).append("=").append(entry.getValue());
}
getRequest.setHeader("Cookie", cookieHeader.toString());
try (CloseableHttpResponse response = httpClient.execute(getRequest)) {
int statusCode = response.getCode();
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
log.info("Vibe Track API 응답 코드: {}", statusCode);
log.info("Vibe Track API 응답 본문 길이: {} bytes", responseBody.length());
if (statusCode == 200) {
// JSON 응답 파싱
tracks = parseVibeTrackResponse(responseBody);
log.info("파싱된 트랙 개수: {}", tracks.size());
} else {
log.error("Vibe Track API 호출 실패. 상태 코드: {}, 응답: {}", statusCode, responseBody);
}
}
} catch (Exception e) {
log.error("Vibe 트랙 목록 조회 중 오류 발생", e);
}
return tracks;
}
/**
* Vibe Track API 응답을 파싱합니다.
*
* @param responseBody JSON 응답 본문
* @return 트랙 목록
*/
private List<VibeTrackResponse.Track> parseVibeTrackResponse(String responseBody) {
List<VibeTrackResponse.Track> tracks = new ArrayList<>();
try {
JsonNode root = objectMapper.readTree(responseBody);
JsonNode resultNode = root.path("response").path("result");
if (resultNode.isObject()) {
JsonNode tracksNode = resultNode.path("tracks");
if (tracksNode.isArray()) {
for (JsonNode trackNode : tracksNode) {
// Artists 파싱
List<VibeTrackResponse.Artist> artists = new ArrayList<>();
JsonNode artistsNode = trackNode.path("artists");
if (artistsNode.isArray()) {
for (JsonNode artistNode : artistsNode) {
VibeTrackResponse.Artist artist = VibeTrackResponse.Artist.builder()
.artistId(artistNode.path("artistId").asLong())
.artistName(artistNode.path("artistName").asText())
.imageUrl(artistNode.path("imageUrl").asText(null))
.build();
artists.add(artist);
}
}
// Album 파싱
JsonNode albumNode = trackNode.path("album");
VibeTrackResponse.Album album = null;
if (albumNode.isObject()) {
album = VibeTrackResponse.Album.builder()
.albumId(albumNode.path("albumId").asLong())
.albumTitle(albumNode.path("albumTitle").asText())
.releaseDate(albumNode.path("releaseDate").asText(null))
.imageUrl(albumNode.path("imageUrl").asText(null))
.build();
}
// Track 생성
VibeTrackResponse.Track track = VibeTrackResponse.Track.builder()
.trackId(trackNode.path("trackId").asLong())
.trackTitle(trackNode.path("trackTitle").asText())
.playTime(trackNode.path("playTime").asText(null))
.artists(artists)
.album(album)
.hasLyric(trackNode.path("hasLyric").asBoolean(false))
.isStreaming(trackNode.path("isStreaming").asBoolean(false))
.build();
tracks.add(track);
}
}
}
} catch (Exception e) {
log.error("Vibe Track 응답 파싱 중 오류 발생", e);
}
return tracks;
}
}