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: post recommendation API #508

Merged
merged 17 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9692bd6
:heavy_plus_sign: config(api): add spring cloud dependency
siyeonSon Sep 8, 2024
8ba6e7c
:sparkles: feat(api): get 50 popular songs using Apple Music API
siyeonSon Sep 8, 2024
77ceeb9
:sparkles: chore(api): change songs limit 50 -> 30
siyeonSon Sep 8, 2024
c4be97f
Merge branch 'dev' into feat/drop-recommend
siyeonSon Sep 8, 2024
3621126
:sparkles: feat(api): get recent posted songs
siyeonSon Sep 11, 2024
818872f
:sparkles: feat(api): recommend artists
siyeonSon Sep 11, 2024
397c293
:sparkles: feat(api): recommend artists
siyeonSon Sep 11, 2024
0aaa7c4
:sparkles: feat(api): recommend recent posted songs and popular songs…
siyeonSon Sep 11, 2024
23e0767
:sparkles: refactor(api): manage constants in RecommendType enum class
siyeonSon Sep 11, 2024
41da990
:sparkles: refactor(api): refactor the getCategoryChart()
siyeonSon Sep 18, 2024
a178ae0
:sparkles: refactor(api): refactor query of findRecentSongs()
siyeonSon Sep 22, 2024
3b72796
:sparkles: refactor(api): refactor query of findRecentSongs() with JPQL
siyeonSon Sep 22, 2024
bcac59c
:sparkles: refactor(api): get only one duplicate music
siyeonSon Sep 23, 2024
8e5c57c
:sparkles: refactor(api): change RecommendType name CHART_SONGS -> PO…
siyeonSon Sep 23, 2024
5438995
:sparkles: refactor(api): change RecommendType title according to the…
siyeonSon Sep 23, 2024
4836bd6
:sparkles: refactor(api): disable logging
siyeonSon Sep 23, 2024
70e439a
:recycle: refactor(api): refactor if-else -> switch case
siyeonSon Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/streetdrop-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ jar {
enabled = false
}

ext {
set('springCloudVersion', "2022.0.3")
}

dependencies {
implementation project(':streetdrop-domain')
implementation project(':streetdrop-common')
Expand All @@ -39,6 +43,13 @@ dependencies {

implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public enum CommonErrorCode implements ErrorCodeInterface {
* Basic Server Error
*/
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON_INTERNAL_SERVER_ERROR", "Internal Server Error", "An unexpected error occurred"),
NOT_IMPLEMENTED(HttpStatus.NOT_IMPLEMENTED, "COMMON_NOT_IMPLEMENTED", "Not Implemented", "The server does not support the functionality required to fulfill the request.");

NOT_IMPLEMENTED(HttpStatus.NOT_IMPLEMENTED, "COMMON_NOT_IMPLEMENTED", "Not Implemented", "The server does not support the functionality required to fulfill the request."),
UNSUPPORTED_TYPE(HttpStatus.BAD_REQUEST, "COMMON_UNSUPPORTED_TYPE", "Unsupported Type", "The type specified is not supported.");
siyeonSon marked this conversation as resolved.
Show resolved Hide resolved

private final HttpStatus status;
private final String errorResponseCode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.depromeet.domains.music.dto.response.MusicResponseDto;
import com.depromeet.domains.music.event.CreateSongGenreEvent;
import com.depromeet.domains.music.song.repository.SongRepository;
import com.depromeet.domains.recommend.dto.response.RecommendCategoryDto;
import com.depromeet.domains.recommend.constant.RecommendType;
import com.depromeet.music.album.Album;
import com.depromeet.music.album.AlbumCover;
import com.depromeet.music.artist.Artist;
Expand Down Expand Up @@ -120,4 +122,10 @@ public MusicResponseDto getMusic(Long songId) {
.map(MusicResponseDto::new)
.orElseThrow(() -> new NotFoundException(CommonErrorCode.NOT_FOUND, songId));
}

@Transactional(readOnly = true)
public RecommendCategoryDto getRecentMusic(RecommendType recommendType) {
var recentSongs = songRepository.findRecentSongs(recommendType.getLimit());
return RecommendCategoryDto.ofMusicInfoResponseDto(recommendType, recentSongs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.depromeet.domains.music.song.repository;

import com.depromeet.domains.recommend.dto.response.MusicInfoResponseDto;

import java.util.List;

public interface QueryDslSongRepository {
List<MusicInfoResponseDto> findRecentSongs(int count);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import java.util.Optional;

public interface SongRepository extends JpaRepository<Song, Long> {
public interface SongRepository extends JpaRepository<Song, Long>, QueryDslSongRepository {
Optional<Song> findSongByNameAndAlbum(String name, Album album);

@Query("SELECT s FROM Song s JOIN FETCH s.album " +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.depromeet.domains.music.song.repository;

import com.depromeet.domains.recommend.dto.response.MusicInfoResponseDto;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.depromeet.item.QItem.item;
import static com.depromeet.music.album.QAlbum.album;
import static com.depromeet.music.album.QAlbumCover.albumCover;
import static com.depromeet.music.artist.QArtist.artist;
import static com.depromeet.music.song.QSong.song;

@Repository
@RequiredArgsConstructor
public class SongRepositoryImpl implements QueryDslSongRepository {

private final JPAQueryFactory queryFactory;

@Override
public List<MusicInfoResponseDto> findRecentSongs(int count) {
return queryFactory.select(
Projections.fields(
MusicInfoResponseDto.class,
album.name,
artist.name,
song.name,
albumCover.albumImage,
albumCover.albumThumbnail,
song.genres
))
.from(item)
.join(item.song, song)
.on(item.song.id.eq(song.id))
.join(song.album, album)
.on(song.album.id.eq(album.id))
.join(album.artist, artist)
.on(album.artist.id.eq(artist.id))
.join(album.albumCover, albumCover)
.on(album.albumCover.id.eq(albumCover.id))
.orderBy(item.createdAt.desc())
.limit(count)
.fetch();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.depromeet.domains.recommend.constant;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum RecommendType {
CHART_SONGS("인기 있는 음악", 30, true),
siyeonSon marked this conversation as resolved.
Show resolved Hide resolved
RECENT_SONGS("많이 드랍된 음악", 15, true),
CHART_ARTIST("아티스트", 10, false);

private final String title;
private final int limit;
private final boolean nextPage;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.depromeet.domains.recommend.controller;

import com.depromeet.common.dto.ResponseDto;
import com.depromeet.domains.recommend.dto.response.RecommendResponseDto;
import com.depromeet.domains.recommend.dto.response.SearchTermRecommendResponseDto;
import com.depromeet.domains.recommend.service.SearchRecommendService;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -12,17 +13,25 @@
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/search-term/recommend")
@RequestMapping
@RequiredArgsConstructor
@Tag(name = "💁Search Recommend", description = "Search Recommend API")
public class SearchRecommendController {

private final SearchRecommendService searchRecommendService;

@Operation(summary = "검색어 추천")
@GetMapping
@GetMapping("/search-term/recommend")
siyeonSon marked this conversation as resolved.
Show resolved Hide resolved
public ResponseEntity<SearchTermRecommendResponseDto> recommendSearchTerm() {
var response = searchRecommendService.recommendSearchTerm();
return ResponseDto.ok(response);
}

@Operation(summary = "검색어 추천 v2")
@GetMapping("/v2/search-term/recommend")
public ResponseEntity<RecommendResponseDto> recommendSearchTerm2() {
var response = searchRecommendService.recommendSearchSongs();
return ResponseDto.ok(response);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.depromeet.domains.recommend.dto.response;

import com.depromeet.external.applemusic.dto.response.catalogchart.AppleMusicAlbumChartResponseDto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ArtistInfoResponseDto {

private static final int MINUTES_PER_HOUR = 60;
private static final int TO_SEC = 1000;
private static final int ALBUM_IMAGE_SIZE = 500;
private static final int ALBUM_THUMBNAIL_IMAGE_SIZE = 100;

private String artistName;
private String albumImage;
private String albumThumbnailImage;

private static String fillSize(String url, int size) {
return url.replace("{w}", String.valueOf(size)).replace("{h}", String.valueOf(size));
}

private static String convertToTimeFormat(int totalMilliseconds) {
int totalSeconds = totalMilliseconds / TO_SEC;
int minutes = totalSeconds / MINUTES_PER_HOUR;
int seconds = totalSeconds % MINUTES_PER_HOUR;
return String.format("%d:%02d", minutes, seconds);
}

public static ArtistInfoResponseDto fromAppleMusicResponse(AppleMusicAlbumChartResponseDto.AlbumData data) {
return new ArtistInfoResponseDto(
data.attributes.artistName,
fillSize(data.attributes.artwork.url, ALBUM_IMAGE_SIZE),
fillSize(data.attributes.artwork.url, ALBUM_THUMBNAIL_IMAGE_SIZE)
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.depromeet.domains.recommend.dto.response;

import com.depromeet.external.applemusic.dto.response.catalogchart.AppleMusicSongChartResponseDto;
import com.depromeet.music.genre.Genre;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

@Getter
@NoArgsConstructor
public class MusicInfoResponseDto {
private String albumName;
private String artistName;
private String songName;
private String durationTime;
private String albumImage;
private String albumThumbnailImage;
private List<String> genre;

public MusicInfoResponseDto(String albumName, String artistName, String songName, String albumImage, String albumThumbnailImage, List<Genre> genre) {
this.albumName = albumName;
this.artistName = artistName;
this.songName = songName;
this.durationTime = ""; // TODO: DB에서 가져오는 음악 데이터는 durationTime이 없음
siyeonSon marked this conversation as resolved.
Show resolved Hide resolved
this.albumImage = albumImage;
this.albumThumbnailImage = albumThumbnailImage;
this.genre = genre.stream().map(Genre::getName).toList();
}

public static MusicInfoResponseDto fromAppleMusicResponse(AppleMusicSongChartResponseDto.SongData data) {
final int MINUTES_PER_HOUR = 60;
final int TO_SEC = 1000;
final int ALBUM_IMAGE_SIZE = 500;
final int ALBUM_THUMBNAIL_IMAGE_SIZE = 100;

BiFunction<String, Integer, String> fillSize = (s, size) ->
s.replace("{w}", String.valueOf(size)).replace("{h}", String.valueOf(size));

Function<Integer, String> totalSecondsToTime = totalSeconds -> {
totalSeconds = totalSeconds / TO_SEC;
int minutes = totalSeconds / MINUTES_PER_HOUR;
int seconds = totalSeconds % MINUTES_PER_HOUR;
return String.format("%d:%02d", minutes, seconds);
};

MusicInfoResponseDto musicInfo = new MusicInfoResponseDto();
musicInfo.albumName = data.attributes.albumName;
musicInfo.artistName = data.attributes.artistName;
musicInfo.songName = data.attributes.name;
musicInfo.durationTime = totalSecondsToTime.apply(data.attributes.durationInMillis);
musicInfo.albumImage = fillSize.apply(data.attributes.artwork.url, ALBUM_IMAGE_SIZE);
musicInfo.albumThumbnailImage = fillSize.apply(data.attributes.artwork.url, ALBUM_THUMBNAIL_IMAGE_SIZE);
musicInfo.genre = data.attributes.genreNames;
return musicInfo;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.depromeet.domains.recommend.dto.response;

import com.depromeet.domains.recommend.constant.RecommendType;
import com.depromeet.external.applemusic.dto.response.catalogchart.AppleMusicAlbumChartResponseDto;
import com.depromeet.external.applemusic.dto.response.catalogchart.AppleMusicSongChartResponseDto;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

@Getter
@AllArgsConstructor
public class RecommendCategoryDto {
private String title;
private List<?> content;
private boolean nextPage;

public static RecommendCategoryDto ofMusicInfoResponseDto(RecommendType recommendType, List<MusicInfoResponseDto> musicInfoResponseDto) {
return new RecommendCategoryDto(recommendType.getTitle(), musicInfoResponseDto, recommendType.isNextPage());
}


public static RecommendCategoryDto ofAppleMusicResponseDto(RecommendType recommendType, AppleMusicSongChartResponseDto appleMusicSongChartResponseDto) {
List<MusicInfoResponseDto> musicInfoList = Optional.ofNullable(appleMusicSongChartResponseDto.results.songs)
.filter(songs -> !songs.isEmpty())
.map(songs -> songs.get(0).data.stream()
.map(MusicInfoResponseDto::fromAppleMusicResponse)
.toList()
)
.orElse(Collections.emptyList());
return new RecommendCategoryDto(recommendType.getTitle(), musicInfoList, recommendType.isNextPage());
}

public static RecommendCategoryDto ofAppleMusicResponseDto(RecommendType recommendType, AppleMusicAlbumChartResponseDto appleMusicAlbumChartResponseDto) {
List<ArtistInfoResponseDto> artistInfoList =
Optional.ofNullable(appleMusicAlbumChartResponseDto.results.albums)
.filter(albums -> !albums.isEmpty())
.map(albums -> albums.get(0).data.stream()
.map(ArtistInfoResponseDto::fromAppleMusicResponse)
.toList()
)
.orElse(Collections.emptyList());
return new RecommendCategoryDto(recommendType.getTitle(), artistInfoList, recommendType.isNextPage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.depromeet.domains.recommend.dto.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor
public class RecommendResponseDto {
private List<RecommendCategoryDto> data;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@

import com.depromeet.common.error.dto.CommonErrorCode;
import com.depromeet.common.error.exception.internal.BusinessException;
import com.depromeet.domains.recommend.dto.response.SearchTermRecommendResponseDto;
import com.depromeet.domains.recommend.dto.response.TextColorDto;
import com.depromeet.domains.music.service.MusicService;
import com.depromeet.domains.recommend.constant.RecommendType;
import com.depromeet.domains.recommend.dto.response.*;
import com.depromeet.domains.recommend.repository.SearchRecommendTermRepository;
import com.depromeet.external.applemusic.service.AppleMusicService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
siyeonSon marked this conversation as resolved.
Show resolved Hide resolved
@Service
@RequiredArgsConstructor
public class SearchRecommendService {

private final MusicService musicService;
private final AppleMusicService appleMusicService;
private final SearchRecommendTermRepository searchRecommendTermRepository;

public SearchTermRecommendResponseDto recommendSearchTerm() {
Expand All @@ -30,4 +38,15 @@ public SearchTermRecommendResponseDto recommendSearchTerm() {

return new SearchTermRecommendResponseDto(description, termList);
}

public RecommendResponseDto recommendSearchSongs() {
return new RecommendResponseDto(
List.of(
siyeonSon marked this conversation as resolved.
Show resolved Hide resolved
appleMusicService.getCategoryChart(RecommendType.CHART_SONGS),
musicService.getRecentMusic(RecommendType.RECENT_SONGS),
appleMusicService.getCategoryChart(RecommendType.CHART_ARTIST)
)
);
}

}
Loading
Loading