diff --git a/postify/pom.xml b/postify/pom.xml
index 7cea5f7..abf0fba 100644
--- a/postify/pom.xml
+++ b/postify/pom.xml
@@ -34,12 +34,6 @@
org.springframework.boot
spring-boot-starter-webmvc
-
-
- org.postgresql
- postgresql
- runtime
-
org.projectlombok
lombok
@@ -50,6 +44,25 @@
spring-boot-starter-webmvc-test
test
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java
new file mode 100644
index 0000000..f213438
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java
@@ -0,0 +1,14 @@
+package net.hackyourfuture.backend.week6.postify.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestClient;
+
+@Configuration
+public class RestClientConfig {
+
+ @Bean
+ public RestClient restClient() {
+ return RestClient.create();
+ }
+}
\ No newline at end of file
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackController.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackController.java
new file mode 100644
index 0000000..c4b375c
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackController.java
@@ -0,0 +1,23 @@
+package net.hackyourfuture.backend.week6.postify.controller;
+
+
+import lombok.AllArgsConstructor;
+import net.hackyourfuture.backend.week6.postify.dto.TrackLyricsResponse;
+import net.hackyourfuture.backend.week6.postify.service.TrackService;
+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;
+
+@AllArgsConstructor
+@RestController
+@RequestMapping("/tracks")
+public class TrackController {
+
+ private final TrackService trackService;
+
+ @GetMapping("/{id}/lyrics")
+ public TrackLyricsResponse getTrackLyrics(@PathVariable Long id) {
+ return trackService.getTrackLyrics(id);
+ }
+}
\ No newline at end of file
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserController.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserController.java
new file mode 100644
index 0000000..6f1b34c
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserController.java
@@ -0,0 +1,34 @@
+package net.hackyourfuture.backend.week6.postify.controller;
+
+
+import lombok.AllArgsConstructor;
+import net.hackyourfuture.backend.week6.postify.dto.UserStatsResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import net.hackyourfuture.backend.week6.postify.repository.UserRepository;
+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;
+
+
+@AllArgsConstructor
+@RestController
+@RequestMapping("/users")
+public class UserController {
+
+ private final UserRepository userRepository;
+
+ @GetMapping("/{id}/stats")
+ public ResponseEntity> getUserStatsById(@PathVariable Long id) {
+ UserStatsResponse stats = userRepository.getUserStatsById(id);
+
+ return stats != null ? ResponseEntity.ok(stats)
+ : ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found");
+
+ }
+
+
+}
+
+
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/ExternalLyricsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/ExternalLyricsResponse.java
new file mode 100644
index 0000000..2d56f43
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/ExternalLyricsResponse.java
@@ -0,0 +1,10 @@
+package net.hackyourfuture.backend.week6.postify.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class ExternalLyricsResponse {
+ private String lyrics;
+}
\ No newline at end of file
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java
new file mode 100644
index 0000000..e86c363
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java
@@ -0,0 +1,15 @@
+package net.hackyourfuture.backend.week6.postify.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class TrackLyricsResponse {
+ private Long trackId;
+ private String trackTitle;
+ private String artistName;
+ private String lyrics;
+}
\ No newline at end of file
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/UserStatsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/UserStatsResponse.java
new file mode 100644
index 0000000..8d342b0
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/UserStatsResponse.java
@@ -0,0 +1,19 @@
+package net.hackyourfuture.backend.week6.postify.dto;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+@Getter
+@AllArgsConstructor
+public class UserStatsResponse {
+ private final Long userId;
+ private final String userName;
+ private final String userCountry;
+ private final Long totalStreams;
+ private final Long uniqueTracksStreamed;
+ private final Long uniqueArtistsStreamed;
+ private final String favoriteGenre;
+ private final Long totalListeningTimeSeconds;
+}
\ No newline at end of file
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepository.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepository.java
new file mode 100644
index 0000000..2e147a1
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepository.java
@@ -0,0 +1,41 @@
+package net.hackyourfuture.backend.week6.postify.repository;
+
+
+import net.hackyourfuture.backend.week6.postify.dto.TrackLyricsResponse;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class TrackRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public TrackRepository(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public TrackLyricsResponse getTrackAndArtistById(Long trackId) {
+ String sql = "SELECT " +
+ " tracks.track_id AS trackId, " +
+ " tracks.track_title AS trackTitle, " +
+ " artists.artist_name AS artistName " +
+ "FROM tracks " +
+ "JOIN albums ON tracks.album_id = albums.album_id " +
+ "JOIN artists ON albums.artist_id = artists.artist_id " +
+ "WHERE tracks.track_id = ?;";
+
+ try {
+ return jdbcTemplate.queryForObject(sql, (rs, rowNum) ->
+ new TrackLyricsResponse(
+ rs.getLong("trackId"),
+ rs.getString("trackTitle"),
+ rs.getString("artistName"),
+ null // from external API
+ ),
+ trackId
+ );
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserRepository.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserRepository.java
new file mode 100644
index 0000000..595dd02
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserRepository.java
@@ -0,0 +1,72 @@
+package net.hackyourfuture.backend.week6.postify.repository;
+
+import net.hackyourfuture.backend.week6.postify.dto.UserStatsResponse;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class UserRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public UserRepository(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public UserStatsResponse getUserStatsById(Long id) {
+
+ String mainSql = "SELECT " +
+ " users.user_id AS userId, " +
+ " users.user_name AS userName, " +
+ " users.user_country AS userCountry, " +
+ " COUNT(streams.stream_id) AS totalStreams, " +
+ " COUNT(DISTINCT streams.track_id) AS uniqueTracksStreamed, " +
+ " COUNT(DISTINCT albums.artist_id) AS uniqueArtistsStreamed, " +
+ " COALESCE(SUM(tracks.track_duration_s), 0) AS totalListeningTimeSeconds " +
+ "FROM users " +
+ "LEFT JOIN streams ON users.user_id = streams.user_id " +
+ "LEFT JOIN tracks ON streams.track_id = tracks.track_id " +
+ "LEFT JOIN albums ON tracks.album_id = albums.album_id " +
+ "WHERE users.user_id = ? " +
+ "GROUP BY users.user_id, users.user_name, users.user_country;";
+
+ String genreSql = "SELECT tracks.genre " +
+ "FROM streams " +
+ "JOIN tracks ON streams.track_id = tracks.track_id " +
+ "WHERE streams.user_id = ? " +
+ "GROUP BY tracks.genre " +
+ "ORDER BY COUNT(streams.stream_id) DESC " +
+ "LIMIT 1;";
+
+ try {
+ String favoriteGenre;
+ try {
+ favoriteGenre = jdbcTemplate.queryForObject(genreSql, String.class, id);
+ if (favoriteGenre == null) {
+ favoriteGenre = "None";
+ }
+ } catch (Exception e) {
+ favoriteGenre = "None";
+ }
+
+ final String finalGenre = favoriteGenre;
+
+ return jdbcTemplate.queryForObject(mainSql, (rs, rowNum) ->
+ new UserStatsResponse(
+ rs.getLong("userId"),
+ rs.getString("userName"),
+ rs.getString("userCountry"),
+ rs.getLong("totalStreams"),
+ rs.getLong("uniqueTracksStreamed"),
+ rs.getLong("uniqueArtistsStreamed"),
+ finalGenre,
+ rs.getLong("totalListeningTimeSeconds")
+ ),
+ id
+ );
+
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackService.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackService.java
new file mode 100644
index 0000000..fab1b56
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackService.java
@@ -0,0 +1,58 @@
+package net.hackyourfuture.backend.week6.postify.service;
+
+
+import net.hackyourfuture.backend.week6.postify.dto.ExternalLyricsResponse;
+import net.hackyourfuture.backend.week6.postify.dto.TrackLyricsResponse;
+import net.hackyourfuture.backend.week6.postify.repository.TrackRepository;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.server.ResponseStatusException;
+
+@Service
+public class TrackService {
+
+ private final TrackRepository trackRepository;
+ private final RestClient restClient;
+
+ public TrackService(TrackRepository trackRepository, RestClient restClient) {
+ this.trackRepository = trackRepository;
+ this.restClient = restClient;
+ }
+
+ public TrackLyricsResponse getTrackLyrics(Long trackId) {
+
+ TrackLyricsResponse trackInfo = trackRepository.getTrackAndArtistById(trackId);
+
+ if (trackInfo == null) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Track not found in our database");
+ }
+
+
+ String url = "https://api.lyrics.ovh/v1/" + trackInfo.getArtistName() + "/" + trackInfo.getTrackTitle();
+
+ try {
+
+ ExternalLyricsResponse externalResponse = restClient.get()
+ .uri(url)
+ .retrieve()
+ .body(ExternalLyricsResponse.class);
+
+
+ if (externalResponse != null && externalResponse.getLyrics() != null) {
+ return new TrackLyricsResponse(
+ trackInfo.getTrackId(),
+ trackInfo.getTrackTitle(),
+ trackInfo.getArtistName(),
+ externalResponse.getLyrics()
+ );
+ } else {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Lyrics not found for this track");
+ }
+
+ } catch (Exception e) {
+
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "The lyrics API has no lyrics for this track.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/postify/src/main/resources/application.properties b/postify/src/main/resources/application.properties
index 8f4842f..fc3ab0d 100644
--- a/postify/src/main/resources/application.properties
+++ b/postify/src/main/resources/application.properties
@@ -1 +1,4 @@
-spring.application.name=postify
+spring.datasource.url=jdbc:postgresql://localhost:5432/postify
+spring.datasource.username=hyfuser
+spring.datasource.password=hyfpassword
+spring.datasource.driver-class-name=org.postgresql.Driver
\ No newline at end of file
diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackControllerTest.java
new file mode 100644
index 0000000..89791c5
--- /dev/null
+++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackControllerTest.java
@@ -0,0 +1,74 @@
+package net.hackyourfuture.backend.week6.postify.controller;
+
+import net.hackyourfuture.backend.week6.postify.dto.TrackLyricsResponse;
+import net.hackyourfuture.backend.week6.postify.service.TrackService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
+import org.springframework.http.HttpStatus;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.web.server.ResponseStatusException;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+@AutoConfigureMockMvc
+class TrackControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+
+ @MockitoBean
+ private TrackService trackService;
+
+
+ @Test
+ void shouldReturnLyricsForExistingTrack() throws Exception {
+
+ TrackLyricsResponse fakeResponse = new TrackLyricsResponse(
+ 67L,
+ "the 1",
+ "Taylor Swift",
+ "I'm doing good, I'm on some new shi..."
+ );
+
+
+ when(trackService.getTrackLyrics(67L)).thenReturn(fakeResponse);
+
+ mockMvc.perform(get("/tracks/67/lyrics"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.trackId").value(67L))
+ .andExpect(jsonPath("$.trackTitle").value("the 1"))
+ .andExpect(jsonPath("$.artistName").value("Taylor Swift"))
+ .andExpect(jsonPath("$.lyrics").value("I'm doing good, I'm on some new shi..."));
+ }
+
+
+ @Test
+ void shouldReturn404WhenTrackNotFound() throws Exception {
+
+ when(trackService.getTrackLyrics(99999L))
+ .thenThrow(new ResponseStatusException(HttpStatus.NOT_FOUND));
+
+ mockMvc.perform(get("/tracks/99999/lyrics"))
+ .andExpect(status().isNotFound());
+ }
+
+
+ @Test
+ void shouldReturn404WhenLyricsNotAvailable() throws Exception {
+
+ when(trackService.getTrackLyrics(90L))
+ .thenThrow(new ResponseStatusException(HttpStatus.NOT_FOUND));
+
+ mockMvc.perform(get("/tracks/90/lyrics"))
+ .andExpect(status().isNotFound());
+ }
+}
\ No newline at end of file
diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserControllerTest.java
new file mode 100644
index 0000000..4f1f86f
--- /dev/null
+++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserControllerTest.java
@@ -0,0 +1,36 @@
+package net.hackyourfuture.backend.week6.postify.controller;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+@AutoConfigureMockMvc
+class UserControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void shouldReturnUserStats() throws Exception {
+
+ mockMvc.perform(get("/users/1/stats"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.userId").value(1))
+ .andExpect(jsonPath("$.userName").value("lena_v"))
+ .andExpect(jsonPath("$.userCountry").value("NL"));
+ }
+
+ @Test
+ void shouldReturn404WhenUserNotFound() throws Exception {
+
+ mockMvc.perform(get("/users/99999/stats"))
+ .andExpect(status().isNotFound());
+ }
+}
\ No newline at end of file