Skip to content

Commit

Permalink
merge: 애플뮤직 유사음악 검색
Browse files Browse the repository at this point in the history
Feature/#88 애플뮤직 유사음악 검색
  • Loading branch information
hong-sile authored Aug 15, 2024
2 parents c6f60dc + b6f2918 commit 6c8b589
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 31 deletions.
9 changes: 9 additions & 0 deletions src/main/java/play/pluv/music/controller/MusicController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package play.pluv.music.controller;

import static play.pluv.playlist.domain.MusicStreaming.APPLE;
import static play.pluv.playlist.domain.MusicStreaming.SPOTIFY;
import static play.pluv.playlist.domain.MusicStreaming.YOUTUBE;

Expand Down Expand Up @@ -39,6 +40,14 @@ public BaseResponse<List<MusicSearchResponse>> searchYoutubeMusics(
return BaseResponse.ok(responses);
}

@PostMapping("/apple/search")
public BaseResponse<List<MusicSearchResponse>> searchAppleMusics(
@Valid @RequestBody final MusicSearchRequest musicSearchRequest
) {
final var responses = musicService.searchMusics(APPLE, musicSearchRequest);
return BaseResponse.ok(responses);
}

@PostMapping("/spotify/add")
public BaseResponse<String> transferSpotifyMusics(
@Valid @RequestBody final MusicAddRequest request
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/play/pluv/music/domain/DestinationMusics.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import play.pluv.playlist.domain.PlayListMusic;

@Getter
@RequiredArgsConstructor
@ToString
public class DestinationMusics {

private final List<DestinationMusic> destinationMusics;
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/play/pluv/oauth/apple/AppleApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.PostExchange;
import play.pluv.oauth.apple.dto.AppleMusicSongs;
import play.pluv.oauth.apple.dto.ApplePlayListMusicResponses;
import play.pluv.oauth.apple.dto.ApplePlayListResponses;
import play.pluv.oauth.apple.dto.AppleSearchMusicResponses;
import play.pluv.oauth.apple.dto.AppleTokenResponse;

public interface AppleApiClient {
Expand All @@ -29,4 +31,16 @@ ApplePlayListMusicResponses getMusics(
@RequestHeader("Music-User-Token") final String musicUserToken,
@PathVariable final String id
);

@GetExchange("https://api.music.apple.com/v1/catalog/kr/search?types=songs&l=ko")
AppleSearchMusicResponses searchMusicByNameAndArtists(
@RequestHeader("Authorization") final String developerToken,
@RequestParam final String term
);

@GetExchange("https://api.music.apple.com/v1/catalog/kr/songs")
AppleMusicSongs searchMusicByIsrc(
@RequestHeader("Authorization") final String developerToken,
@RequestParam("filter[isrc]") final String isrc
);
}
49 changes: 38 additions & 11 deletions src/main/java/play/pluv/oauth/apple/AppleConnector.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import play.pluv.login.exception.LoginException;
import play.pluv.music.application.MusicExplorer;
import play.pluv.music.domain.DestinationMusics;
import play.pluv.music.domain.MusicId;
import play.pluv.oauth.apple.dto.AppleTokenResponse;
import play.pluv.oauth.application.SocialLoginClient;
import play.pluv.oauth.domain.OAuthMemberInfo;
Expand All @@ -35,19 +36,26 @@
import play.pluv.playlist.domain.PlayListMusic;

@Component
@RequiredArgsConstructor
public class AppleConnector implements SocialLoginClient, PlayListConnector {
public class AppleConnector implements SocialLoginClient, PlayListConnector, MusicExplorer {

private static final String AUDIENCE = "https://appleid.apple.com";
private static final Long EXP = MILLISECONDS.convert(30, MINUTES);
private static final String AUTHORIZATION_FORMAT = "Bearer %s";
private static final Function<String, String> CREATE_AUTH_HEADER
= (token) -> String.format(AUTHORIZATION_FORMAT, token);

private final String developerAuthorization;
private final ObjectMapper objectMapper;
private final AppleApiClient appleApiClient;
private final AppleConfigProperty appleConfigProperty;

public AppleConnector(
final ObjectMapper objectMapper, final AppleApiClient appleApiClient,
final AppleConfigProperty appleConfigProperty
) {
this.objectMapper = objectMapper;
this.appleApiClient = appleApiClient;
this.appleConfigProperty = appleConfigProperty;
this.developerAuthorization = String.format("Bearer %s", appleConfigProperty.developerToken());
}

@Override
public OAuthMemberInfo fetchMember(final String idToken) {
final String userIdentifier = extractSub(idToken);
Expand Down Expand Up @@ -75,16 +83,14 @@ private String extractSub(final String idToken) {
@Override
public List<PlayList> getPlayList(final String musicUserToken) {
return appleApiClient.getPlayList(
CREATE_AUTH_HEADER.apply(appleConfigProperty.developerToken())
, musicUserToken
developerAuthorization, musicUserToken
).toPlayLists();
}

@Override
public List<PlayListMusic> getMusics(final String playListId, final String musicUserToken) {
return appleApiClient.getMusics(
CREATE_AUTH_HEADER.apply(appleConfigProperty.developerToken())
, musicUserToken, playListId
developerAuthorization, musicUserToken, playListId
).toPlayListMusics();
}

Expand All @@ -93,6 +99,27 @@ public PlayListId createPlayList(final String musicUserToken, final String name)
return null;
}

@Override
public DestinationMusics searchMusic(final String musicUserToken, final PlayListMusic source) {
return source.getIsrcCode()
.map(isrc -> appleApiClient.searchMusicByIsrc(developerAuthorization, isrc)
.toDestinationMusics()
)
.orElseGet(() -> searchByNameAndArtists(source));
}

private DestinationMusics searchByNameAndArtists(final PlayListMusic source) {
final String term = source.getTitle() + source.getArtistNames();
return appleApiClient.searchMusicByNameAndArtists(developerAuthorization, term)
.toDestinationMusics();
}

@Override
public void transferMusics(
final String accessToken, final List<MusicId> musicIds, final String playlistName
) {
}

@Override
public MusicStreaming supportedType() {
return APPLE;
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/play/pluv/oauth/apple/dto/AppleMusicSongs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package play.pluv.oauth.apple.dto;

import java.util.List;
import play.pluv.music.domain.DestinationMusic;
import play.pluv.music.domain.DestinationMusics;
import play.pluv.music.domain.MusicId;
import play.pluv.playlist.domain.MusicStreaming;

public record AppleMusicSongs(
List<AppleMusicSong> data
) {

public DestinationMusics toDestinationMusics() {
return new DestinationMusics(data.stream()
.map(AppleMusicSong::toDestinationMusic)
.toList());
}

private record AppleMusicSong(
AppleMusicSong.AppleMusicAttributes attributes, String id
) {

private DestinationMusic toDestinationMusic() {
return DestinationMusic.builder()
.musicId(new MusicId(MusicStreaming.APPLE, id))
.title(attributes.name)
.artistNames(List.of(attributes.artistName))
.isrcCode(attributes.isrc)
.imageUrl(attributes().artwork.url())
.build();
}

private record AppleMusicAttributes(
String isrc, Artwork artwork, String name, String artistName
) {

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package play.pluv.oauth.apple.dto;

import play.pluv.music.domain.DestinationMusics;

public record AppleSearchMusicResponses(
AppleMusicSearchResult results
) {

public DestinationMusics toDestinationMusics() {
return results.songs().toDestinationMusics();
}

private record AppleMusicSearchResult(
AppleMusicSongs songs
) {

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static play.pluv.playlist.domain.MusicStreaming.YOUTUBE;

import java.util.List;
import java.util.Optional;
import play.pluv.music.domain.DestinationMusic;
import play.pluv.music.domain.DestinationMusics;
import play.pluv.music.domain.MusicId;
Expand All @@ -12,22 +11,6 @@ public record YoutubeSearchMusicResponses(
List<YoutubeMusicVideo> items
) {

public Optional<DestinationMusic> toDestinationMusic() {
if (items.isEmpty()) {
return Optional.empty();
}
final YoutubeMusicVideo youtubeMusicVideo = items.get(0);

return Optional.of(DestinationMusic.builder()
.musicId(new MusicId(YOUTUBE, youtubeMusicVideo.getId()))
.imageUrl(youtubeMusicVideo.snippet().thumbnails().getUrl())
.artistNames(List.of())
.title(youtubeMusicVideo.snippet().title())
.isrcCode(null)
.build()
);
}

public DestinationMusics toDestinationMusics() {
return new DestinationMusics(items.stream()
.map(YoutubeMusicVideo::toDestinationMusic)
Expand Down
21 changes: 20 additions & 1 deletion src/test/java/play/pluv/api/MusicApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static play.pluv.api.fixture.MusicFixture.스포티파이_음악_검색_결과;
import static play.pluv.api.fixture.MusicFixture.애플_음악_검색_결과;
import static play.pluv.api.fixture.MusicFixture.유튜브_음악_검색_결과;
import static play.pluv.api.fixture.MusicFixture.음악_검색_요청;
import static play.pluv.api.fixture.MusicFixture.음악_가_요청;
Expand All @@ -31,7 +32,7 @@ public class MusicApiTest extends ApiTest {
private static final Snippet[] MUSIC_SEARCH_SNIPPETS = {
requestFields(
fieldWithPath("destinationAccessToken").type(STRING)
.description("플레이리스트 제공자의 accessToken"),
.description("플레이리스트 제공자의 accessToken(애플의 경우엔 musicUserToken)"),
fieldWithPath("musics[].title").type(STRING).description("음악 이름"),
fieldWithPath("musics[].artistName").type(STRING).description("가수 이름들"),
fieldWithPath("musics[].imageUrl").type(STRING).description("음악 image url"),
Expand Down Expand Up @@ -104,6 +105,24 @@ public class MusicApiTest extends ApiTest {
));
}

@Test
void 애플뮤직_음악을_읽어서_반환해준다() throws Exception {
final MusicSearchRequest 검색_요청 = 음악_검색_요청();
final List<MusicSearchResponse> 검색_결과 = 애플_음악_검색_결과();

final String requestBody = objectMapper.writeValueAsString(검색_요청);

when(musicService.searchMusics(any(), any())).thenReturn(검색_결과);

mockMvc.perform(post("/music/apple/search")
.contentType(APPLICATION_JSON_VALUE)
.content(requestBody))
.andExpect(status().isOk())
.andDo(document("search-apple-music",
MUSIC_SEARCH_SNIPPETS
));
}

@Test
void 음악들을_스포티파이의_새로운_플리에_한다() throws Exception {
final MusicAddRequest 음악_가_요청 = 음악_가_요청();
Expand Down
20 changes: 18 additions & 2 deletions src/test/java/play/pluv/api/fixture/MusicFixture.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ true, true, new SourceMusicResponse("좋은 날", "아이유", "imageUrl"),
"https://i.scdn.co/image/ab67616d00001e0215cf3110f19687b1a24943d1")
)
),
new MusicSearchResponse(true, true, new SourceMusicResponse("ㅈㅣㅂ", "hanro", "imageUrl"),
new MusicSearchResponse(false, true, new SourceMusicResponse("ㅈㅣㅂ", "hanro", "imageUrl"),
List.of(new DestinationMusicResponse("uo890df1", "SPOT!", "제니,지코",
"https://i.scdn.co/image/ab67616d00001e024930dc9d8cdc7f5f33282538"))),
new MusicSearchResponse(false, false,
Expand All @@ -40,7 +40,23 @@ true, true, new SourceMusicResponse("좋은 날", "아이유", "imageUrl"),
List.of(new DestinationMusicResponse("124nkd3fh", "Good Day - MV", "",
"https://i.scdn.co/image/ab67616d00001e0215cf3110f19687b1a24943d1"))
),
new MusicSearchResponse(true, true, new SourceMusicResponse("ㅈㅣㅂ", "hanro", "imageUrl"),
new MusicSearchResponse(false, true, new SourceMusicResponse("ㅈㅣㅂ", "hanro", "imageUrl"),
List.of(new DestinationMusicResponse("uo890df1", "SPOT! - MV (제니,지코)", "",
"https://i.scdn.co/image/ab67616d00001e024930dc9d8cdc7f5f33282538"))),
new MusicSearchResponse(false, false,
new SourceMusicResponse("세상에 존재하지 않는 음악", "세상에 존재하지 않는 가수", "imageUrl"), List.of()
)
);
}

public static List<MusicSearchResponse> 애플_음악_검색_결과() {
return List.of(
new MusicSearchResponse(
true, true, new SourceMusicResponse("좋은 날", "아이유", "imageUrl"),
List.of(new DestinationMusicResponse("534dfdf.dfe", "좋은 날", "아이유",
"https://i.scdn.co/image/ab67616d00001e0215cf3110f19687b1a24943d1"))
),
new MusicSearchResponse(false, true, new SourceMusicResponse("ㅈㅣㅂ", "hanro", "imageUrl"),
List.of(new DestinationMusicResponse("uo890df1", "SPOT! - MV (제니,지코)", "",
"https://i.scdn.co/image/ab67616d00001e024930dc9d8cdc7f5f33282538"))),
new MusicSearchResponse(false, false,
Expand Down

0 comments on commit 6c8b589

Please sign in to comment.