Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#127 ManiaDB API 를 사용하여 등록되지 않은 노래 검색 기능 구현 #153

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
9 changes: 9 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ dependencies {
//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

// WebClient Dependency
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// XML parsing Dependency
implementation 'org.glassfish.jaxb:jaxb-runtime'

compileOnly 'org.projectlombok:lombok'

runtimeOnly 'com.h2database:h2'
Expand All @@ -43,6 +49,9 @@ dependencies {

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'

// WebClient Test Dependencies
testImplementation 'com.squareup.okhttp3:mockwebserver'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package shook.shook.song.application;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import shook.shook.song.application.dto.UnregisteredSongResponse;
import shook.shook.song.application.dto.maniadb.ManiaDBAPISearchResponse;
import shook.shook.song.application.dto.maniadb.SearchedSongFromManiaDBApiResponses;
import shook.shook.song.exception.ExternalApiException;
import shook.shook.song.exception.UnregisteredSongException;
import shook.shook.util.StringChecker;

@RequiredArgsConstructor
@Service
public class ManiaDBSearchService {

private static final String MANIA_DB_API_URI = "/%s/?sr=song&display=%d&key=example&v=0.5";
private static final int SEARCH_SIZE = 100;
private static final String SPECIAL_MARK_REGEX = "[^ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9,. ]";

private final WebClient webClient;

public List<UnregisteredSongResponse> searchSongs(final String searchWord) {
validateSearchWord(searchWord);

final String parsedSearchWord = replaceSpecialMark(searchWord);
final SearchedSongFromManiaDBApiResponses searchResult = getSearchResult(parsedSearchWord);

if (Objects.isNull(searchResult.getSongs())) {
return Collections.emptyList();
}

return searchResult.getSongs().stream()
.map(UnregisteredSongResponse::from)
.toList();
}

private void validateSearchWord(final String searchWord) {
if (StringChecker.isNullOrBlank(searchWord)) {
throw new UnregisteredSongException.NullOrBlankSearchWordException();
}
}

private String replaceSpecialMark(final String rawSearchWord) {
return rawSearchWord.replaceAll(SPECIAL_MARK_REGEX, "");
}

private SearchedSongFromManiaDBApiResponses getSearchResult(final String searchWord) {
final String searchUrl = String.format(MANIA_DB_API_URI, searchWord, SEARCH_SIZE);
final ManiaDBAPISearchResponse result = getResultFromManiaDB(searchUrl);

if (Objects.isNull(result)) {
throw new ExternalApiException.EmptyResultException();
}

return result.getSongs();
}

private ManiaDBAPISearchResponse getResultFromManiaDB(final String searchUrl) {
return webClient.get()
.uri(searchUrl)
.accept(MediaType.TEXT_XML)
.acceptCharset(StandardCharsets.UTF_8)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (clientResponse) -> {
throw new ExternalApiException.ManiaDBClientException();
})
.onStatus(HttpStatusCode::is5xxServerError, (clientResponse) -> {
throw new ExternalApiException.ManiaDBServerException();
})
.bodyToMono(ManiaDBAPISearchResponse.class)
.block();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package shook.shook.song.application.dto;

import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import shook.shook.song.application.dto.maniadb.SearchedSongFromManiaDBApiResponse;
import shook.shook.song.application.dto.maniadb.SongArtistResponse;

@AllArgsConstructor
@Getter
public class UnregisteredSongResponse {

private static final String EMPTY_SINGER = "";
private static final String SINGER_DELIMITER = ", ";

private String title;
private String singer;
private String albumImageUrl;

public static UnregisteredSongResponse from(
final SearchedSongFromManiaDBApiResponse searchedSongFromManiaDBApiResponse) {
if (isEmptyArtists(searchedSongFromManiaDBApiResponse)) {
return new UnregisteredSongResponse(
searchedSongFromManiaDBApiResponse.getTitle().trim(),
EMPTY_SINGER,
searchedSongFromManiaDBApiResponse.getAlbum().getImage().trim()
);
}

final String singers = collectToString(searchedSongFromManiaDBApiResponse);

return new UnregisteredSongResponse(
searchedSongFromManiaDBApiResponse.getTitle().trim(),
singers,
searchedSongFromManiaDBApiResponse.getAlbum().getImage().trim()
);
}

private static boolean isEmptyArtists(
final SearchedSongFromManiaDBApiResponse searchedSongFromManiaDBApiResponse) {
return searchedSongFromManiaDBApiResponse.getTrackArtists() == null
|| searchedSongFromManiaDBApiResponse.getTrackArtists().getArtists() == null;
}

private static String collectToString(
final SearchedSongFromManiaDBApiResponse searchedSongFromManiaDBApiResponse) {
return searchedSongFromManiaDBApiResponse.getTrackArtists().getArtists().stream()
.map(SongArtistResponse::getName)
.map(String::trim)
.collect(Collectors.joining(SINGER_DELIMITER));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package shook.shook.song.application.dto.maniadb;

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Getter;

@Getter
@XmlRootElement(name = "rss")
public class ManiaDBAPISearchResponse {

@XmlElement(name = "channel")
private SearchedSongFromManiaDBApiResponses songs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package shook.shook.song.application.dto.maniadb;

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Getter;

@Getter
@XmlRootElement(name = "item")
public class SearchedSongFromManiaDBApiResponse {

@XmlElement(name = "title")
private String title;

@XmlElement(name = "trackartists", namespace = "http://www.maniadb.com/api")
private SongTrackArtistsResponse trackArtists;

@XmlElement(name = "album", namespace = "http://www.maniadb.com/api")
private SongAlbumResponse album;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package shook.shook.song.application.dto.maniadb;

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import java.util.List;
import lombok.Getter;

@Getter
@XmlRootElement(name = "channel")
public class SearchedSongFromManiaDBApiResponses {

@XmlElement(name = "item")
private List<SearchedSongFromManiaDBApiResponse> songs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package shook.shook.song.application.dto.maniadb;

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Getter;

@Getter
@XmlRootElement(name = "album", namespace = "http://www.maniadb.com/api")
public class SongAlbumResponse {

@XmlElement(name = "image")
private String image;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package shook.shook.song.application.dto.maniadb;

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Getter;

@Getter
@XmlRootElement(name = "artist", namespace = "http://www.maniadb.com/api")
public class SongArtistResponse {

@XmlElement(name = "name")
private String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package shook.shook.song.application.dto.maniadb;

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import java.util.List;
import lombok.Getter;

@Getter
@XmlRootElement(name = "trackartists", namespace = "http://www.maniadb.com/api")
public class SongTrackArtistsResponse {

@XmlElement(name = "artist", namespace = "http://www.maniadb.com/api")
private List<SongArtistResponse> artists;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package shook.shook.song.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class ManiaDBConfiguration {

private static final String MANIA_DB_BASE_URL = "http://www.maniadb.com/api/search";

@Bean
public WebClient getWebClient() {
return WebClient.builder()
.baseUrl(MANIA_DB_BASE_URL)
.exchangeStrategies(
ExchangeStrategies.builder()
.codecs(configurer ->
configurer.defaultCodecs().jaxb2Decoder(new Jaxb2XmlDecoder())
)
.codecs(configurer ->
configurer.defaultCodecs().maxInMemorySize(4 * 1024 * 1024)
)
.build()
)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package shook.shook.song.exception;

public class ExternalApiException extends RuntimeException {

public static class EmptyResultException extends ExternalApiException {

public EmptyResultException() {
super();
}
}

public static class ManiaDBServerException extends ExternalApiException {

public ManiaDBServerException() {
super();
}
}

public static class ManiaDBClientException extends ExternalApiException {

public ManiaDBClientException() {
super();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package shook.shook.song.exception;

public class UnregisteredSongException extends RuntimeException {

public static class NullOrBlankSearchWordException extends UnregisteredSongException {

public NullOrBlankSearchWordException() {
super();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package shook.shook.song.ui;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import shook.shook.song.application.ManiaDBSearchService;
import shook.shook.song.application.dto.UnregisteredSongResponse;

@RequiredArgsConstructor
@RequestMapping("/songs/unregistered/search")
@RestController
public class UnregisteredSongSearchController {

private final ManiaDBSearchService maniaDBSearchService;

@GetMapping
public ResponseEntity<List<UnregisteredSongResponse>> searchUnregisteredSong(
final @RequestParam("keyword") String searchWord
) {
final List<UnregisteredSongResponse> songs = maniaDBSearchService.searchSongs(
searchWord);

return ResponseEntity.ok(songs);
}
}
1 change: 1 addition & 0 deletions backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
spring:
profiles:
active: local

config:
import: classpath:shook-security/application.yml

Expand Down
Loading