Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 30 additions & 5 deletions postify/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.6</version>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>net.hackyourfuture.backend.week6</groupId>
Expand All @@ -27,14 +27,28 @@
<url/>
</scm>
<properties>
<java.version>25</java.version>
<java.version>21</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.19.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
Expand All @@ -45,11 +59,22 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a useful startup check to verify that the application can connect to the database when it becomes ready. Consider using a logger instead of System.out.println, and make sure this class is registered as a Spring bean, for example with @Component.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package net.hackyourfuture.backend.week6.postify;

import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.jdbc.core.JdbcTemplate;

public class DBConnectionCheck {

private final JdbcTemplate jdbcTemplate;

public DBConnectionCheck(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}

@EventListener(ApplicationReadyEvent.class)
public void checkConnection(){
String result = this.jdbcTemplate.queryForObject("SELECT 'connected'", String.class);
System.out.println("Database status: " + result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package net.hackyourfuture.backend.week6.postify.client;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestClient;

@Configuration
public class LyricsApiConfiguration {

@Bean
public RestClient.Builder restClientBuilder() {
return RestClient.builder();
}

@Bean
public RestClient lyricsRestClient(RestClient.Builder builder){

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely configured RestClient. Static request information, such as the base URL and default Accept header, is centralized in the configuration instead of being repeated across individual requests. This keeps the client code cleaner and easier to maintain.

return builder.baseUrl("https://api.lyrics.ovh")
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea to wrap the external REST API calls in a dedicated service. This keeps the integration logic isolated and makes the code easier to maintain and test.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package net.hackyourfuture.backend.week6.postify.client;

import net.hackyourfuture.backend.week6.postify.dto.response.lyric.LyricApiResponse;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClient;

@Service
public class LyricsClient {
private final RestClient lyricsRestClient;

public LyricsClient(@Qualifier("lyricsRestClient") RestClient lyricsRestClient){
this.lyricsRestClient = lyricsRestClient;
}

public LyricApiResponse getLyrics(String artistName, String trackTitle){
try {
return lyricsRestClient.get()
.uri(uriBuilder -> uriBuilder
.path("/v1/{artist}/{title}")
.build(artistName, trackTitle))
.retrieve()
.body(LyricApiResponse.class);
} catch(HttpClientErrorException.NotFound ex){
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning null here should be clearly documented, but using Optional would make the absence of a result more explicit and easier for callers to handle safely.

}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package net.hackyourfuture.backend.week6.postify.controllers;

import net.hackyourfuture.backend.week6.postify.dto.response.lyric.TrackLyricResponse;
import net.hackyourfuture.backend.week6.postify.services.TrackLyricsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/tracks")
public class TrackLyricsController {
private final TrackLyricsService service;

public TrackLyricsController(TrackLyricsService service){
this.service = service;
}

@GetMapping("/{id}/lyrics")
public TrackLyricResponse getLyrics(@PathVariable("id") int trackId){
return service.getLyricsForTrack(trackId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.hackyourfuture.backend.week6.postify.controllers;

import net.hackyourfuture.backend.week6.postify.dto.response.user.UserStatisticsResponse;
import net.hackyourfuture.backend.week6.postify.services.UserStatisticsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class UserStatisticsController {

private final UserStatisticsService service;

public UserStatisticsController(UserStatisticsService service){
this.service = service;
}

@GetMapping("/{id}/stats")
public UserStatisticsResponse getUserStats(@PathVariable int id){
return service.getStatisticsForUser(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package net.hackyourfuture.backend.week6.postify.dto.response.lyric;

public record LyricApiResponse(String lyrics) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package net.hackyourfuture.backend.week6.postify.dto.response.lyric;

public record TrackBasicInfo (int trackId, String trackTitle, String artistName){
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.hackyourfuture.backend.week6.postify.dto.response.lyric;

import java.util.List;

public record TrackLyricResponse (int trackId, String trackTitle, String artistName, List<String> lyrics){
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package net.hackyourfuture.backend.week6.postify.dto.response.user;

public record UserBasicInfoResponse(int id, String name, String country) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.hackyourfuture.backend.week6.postify.dto.response.user;

public record UserStatisticsResponse(
int userId,
String userName,
String userCountry,
int totalStreams,
int uniqueTracksStreamed,
int uniqueArtistsStreamed,
String favoriteGenre,
int totalListeningTimeSeconds
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.hackyourfuture.backend.week6.postify.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}

@ExceptionHandler(TrackNotFoundException.class)
public ResponseEntity<String> handleTrackNotFound(TrackNotFoundException ex){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}

@ExceptionHandler(LyricsNotFoundException.class)
public ResponseEntity<String> handleLyricsNotFound(LyricsNotFoundException ex){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.hackyourfuture.backend.week6.postify.exceptions;

public class LyricsNotFoundException extends RuntimeException{
public LyricsNotFoundException(String trackTitle, String artistName){
super("Lyrics not found for track '" + trackTitle + "' by '" + artistName + "'");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.hackyourfuture.backend.week6.postify.exceptions;

public class TrackNotFoundException extends RuntimeException{
public TrackNotFoundException(int id){
super("Track with id " + id + " not found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.hackyourfuture.backend.week6.postify.exceptions;

public class UserNotFoundException extends RuntimeException{
public UserNotFoundException(int id){
super("User with id " + id + " not found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package net.hackyourfuture.backend.week6.postify.repository;

import net.hackyourfuture.backend.week6.postify.dto.response.lyric.TrackBasicInfo;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public class TracksLyricsRepository {
private final JdbcTemplate jdbcTemplate;

public TracksLyricsRepository(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}

public Optional<TrackBasicInfo> findTrackWithArtist(int trackId){
String sql = """
SELECT t.track_id, t.track_title, a.artist_name
FROM tracks t
JOIN albums al ON t.album_id = al.album_id
JOIN artists a ON al.artist_id = a.artist_id
WHERE t.track_id = ?
""";

return jdbcTemplate.query(sql,rs -> {
if(rs.next()){
return Optional.of(new TrackBasicInfo(
rs.getInt("track_id"),
rs.getString("track_title"),
rs.getString("artist_name")
));
} return Optional.empty();
}, trackId);
}
}
Loading