From 9cff43c114aaab215550b2344e6da33b543a3626 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Thu, 17 Oct 2024 19:11:13 +0200 Subject: [PATCH 01/10] :triangular_ruler: Refactored insights controller and service --- .../controller/insights.controller.ts | 145 ++--- .../src/insights/services/insights.service.ts | 591 ++++++++---------- 2 files changed, 295 insertions(+), 441 deletions(-) diff --git a/Backend/src/insights/controller/insights.controller.ts b/Backend/src/insights/controller/insights.controller.ts index 6ad28fe7..331aadea 100644 --- a/Backend/src/insights/controller/insights.controller.ts +++ b/Backend/src/insights/controller/insights.controller.ts @@ -1,138 +1,79 @@ -import { Controller, Get, Query, HttpException, HttpStatus } from "@nestjs/common"; +import { Controller, Post, Body, HttpException, HttpStatus } from "@nestjs/common"; import { InsightsService } from "../services/insights.service"; @Controller("insights") -export class InsightsController -{ - constructor(private readonly insightsService: InsightsService) - { - } +export class InsightsController { + constructor(private readonly insightsService: InsightsService) {} - // Endpoint to get the top mood from recent tracks - @Get("top-mood") - async getTopMood( - @Query("userId") userId: string, - @Query("accessToken") accessToken: string, - @Query("providerToken") providerToken: string, - @Query("providerName") providerName: string - ) - { - if (!userId || !accessToken || !providerToken || !providerName) - { + @Post("top-mood") + async getTopMood(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } - return this.insightsService.getTopMood(userId, accessToken, providerToken, providerName); + return this.insightsService.getTopMood(accessToken, refreshToken, providerName); } - // Endpoint to get the total listening time - @Get("total-listening-time") - async getTotalListeningTime( - @Query("userId") userId: string, - @Query("accessToken") accessToken: string, - @Query("providerToken") providerToken: string, - @Query("providerName") providerName: string - ) - { - if (!userId || !accessToken || !providerToken || !providerName) - { + @Post("total-listening-time") + async getTotalListeningTime(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } - return this.insightsService.getTotalListeningTime(userId, accessToken, providerToken, providerName); + return this.insightsService.getTotalListeningTime(accessToken, refreshToken, providerName); } - // Endpoint to get the most listened artist - @Get("most-listened-artist") - async getMostListenedArtist( - @Query("userId") userId: string, - @Query("accessToken") accessToken: string, - @Query("providerToken") providerToken: string, - @Query("providerName") providerName: string - ) - { - if (!userId || !accessToken || !providerToken || !providerName) - { + @Post("most-listened-artist") + async getMostListenedArtist(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } - return this.insightsService.getMostListenedArtist(userId, accessToken, providerToken, providerName); + return this.insightsService.getMostListenedArtist(accessToken, refreshToken, providerName); } - // Endpoint to get the most played track - @Get("most-played-track") - async getMostPlayedTrack( - @Query("userId") userId: string, - @Query("accessToken") accessToken: string, - @Query("providerToken") providerToken: string, - @Query("providerName") providerName: string - ) - { - if (!userId || !accessToken || !providerToken || !providerName) - { + @Post("most-played-track") + async getMostPlayedTrack(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } - return this.insightsService.getMostPlayedTrack(userId, accessToken, providerToken, providerName); + return this.insightsService.getMostPlayedTrack(accessToken, refreshToken, providerName); } - // Endpoint to get the top genre from the user's listening history - @Get("top-genre") - async getTopGenre( - @Query("userId") userId: string, - @Query("accessToken") accessToken: string, - @Query("providerToken") providerToken: string, - @Query("providerName") providerName: string - ) - { - if (!userId || !accessToken || !providerToken || !providerName) - { + @Post("top-genre") + async getTopGenre(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } - return this.insightsService.getTopGenre(userId, accessToken, providerToken, providerName); + return this.insightsService.getTopGenre(accessToken, refreshToken, providerName); } - // Endpoint to get the average song duration - @Get("average-song-duration") - async getAverageSongDuration( - @Query("userId") userId: string, - @Query("accessToken") accessToken: string, - @Query("providerToken") providerToken: string, - @Query("providerName") providerName: string - ) - { - if (!userId || !accessToken || !providerToken || !providerName) - { + @Post("average-song-duration") + async getAverageSongDuration(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } - return this.insightsService.getAverageSongDuration(userId, accessToken, providerToken, providerName); + return this.insightsService.getAverageSongDuration(accessToken, refreshToken, providerName); } - // Endpoint to get the most active day of listening - @Get("most-active-day") - async getMostActiveDay( - @Query("userId") userId: string, - @Query("accessToken") accessToken: string, - @Query("providerToken") providerToken: string, - @Query("providerName") providerName: string - ) - { - if (!userId || !accessToken || !providerToken || !providerName) - { + @Post("most-active-day") + async getMostActiveDay(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } - return this.insightsService.getMostActiveDay(userId, accessToken, providerToken, providerName); + return this.insightsService.getMostActiveDay(accessToken, refreshToken, providerName); } - // Endpoint to get the number of unique artists listened to - @Get("unique-artists-listened") - async getUniqueArtistsListened( - @Query("userId") userId: string, - @Query("accessToken") accessToken: string, - @Query("providerToken") providerToken: string, - @Query("providerName") providerName: string - ) - { - if (!userId || !accessToken || !providerToken || !providerName) - { + @Post("unique-artists-listened") + async getUniqueArtistsListened(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } - return this.insightsService.getUniqueArtistsListened(userId, accessToken, providerToken, providerName); + return this.insightsService.getUniqueArtistsListened(accessToken, refreshToken, providerName); } } diff --git a/Backend/src/insights/services/insights.service.ts b/Backend/src/insights/services/insights.service.ts index 93a34b0f..0dd27f6c 100644 --- a/Backend/src/insights/services/insights.service.ts +++ b/Backend/src/insights/services/insights.service.ts @@ -1,19 +1,18 @@ import { Injectable, HttpException, HttpStatus } from "@nestjs/common"; import axios from "axios"; import { createSupabaseClient } from "../../supabase/services/supabaseClient"; +import { SupabaseService } from "../../supabase/services/supabase.service"; @Injectable() -export class InsightsService -{ +export class InsightsService { + constructor(private supabaseService: SupabaseService) {} // Helper to get user details from Supabase - private async getUserFromSupabase(accessToken: string) - { + private async getUserFromSupabase(accessToken: string): Promise { const supabase = createSupabaseClient(); const { data, error } = await supabase.auth.getUser(accessToken); - if (error) - { + if (error) { throw new HttpException("Invalid access token", HttpStatus.UNAUTHORIZED); } @@ -22,273 +21,231 @@ export class InsightsService // Get top mood based on user's listening history async getTopMood( - userId: string, accessToken: string, - providerToken: string, + refreshToken: string, providerName: string - ): Promise - { - const user = await this.getUserFromSupabase(accessToken); - if (!user || user.id !== userId) - { - throw new HttpException("User not authorized", HttpStatus.UNAUTHORIZED); - } + ): Promise { + await this.setSupabaseSession(accessToken, refreshToken); // Set session before retrieving tokens + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") - { + if (providerName === "spotify") { return this.getTopMoodFromSpotify(providerToken); - } - else if (providerName === "youtube") - { + } else if (providerName === "youtube") { return this.getTopMoodFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - // Get total listening time from Spotify or YouTube - async getTotalListeningTime( - userId: string, - accessToken: string, - providerToken: string, - providerName: string - ): Promise - { - const user = await this.getUserFromSupabase(accessToken); - if (!user || user.id !== userId) - { - throw new HttpException("User not authorized", HttpStatus.UNAUTHORIZED); - } + async getTopMoodFromSpotify(providerToken: string): Promise { + const url = "https://api.spotify.com/v1/me/top/tracks?limit=10"; - if (providerName === "spotify") - { - return this.getTotalListeningTimeFromSpotify(providerToken); - } - else if (providerName === "youtube") - { - return this.getTotalListeningTimeFromYouTube(providerToken); - } + try { + const response = await axios.get(url, { + headers: { + Authorization: `Bearer ${providerToken}`, + }, + }); + const topTracks = response.data.items; - throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); - } + const genres = new Set( + topTracks.flatMap((track) => track.artists.map((artist) => artist.genres)) + ); - // Get most played track for a user - async getMostPlayedTrack( - userId: string, - accessToken: string, - providerToken: string, - providerName: string - ): Promise - { - const user = await this.getUserFromSupabase(accessToken); - if (!user || user.id !== userId) - { - throw new HttpException("User not authorized", HttpStatus.UNAUTHORIZED); - } + let mood = "Happy"; // Default mood - if (providerName === "spotify") - { - return this.getMostPlayedTrackFromSpotify(providerToken); - } - else if (providerName === "youtube") - { - return this.getMostPlayedTrackFromYouTube(providerToken); - } + if (genres.has("pop")) { + mood = "Energetic"; + } else if (genres.has("classical")) { + mood = "Relaxed"; + } else if (genres.has("rock")) { + mood = "Energetic"; + } - throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); + return { mood }; + } catch (error) { + throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } } - // Get most listened artist for a user - async getMostListenedArtist( - userId: string, - accessToken: string, - providerToken: string, - providerName: string - ): Promise - { - const user = await this.getUserFromSupabase(accessToken); - if (!user || user.id !== userId) - { - throw new HttpException("User not authorized", HttpStatus.UNAUTHORIZED); - } + async getTopMoodFromYouTube(providerToken: string): Promise { + const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - if (providerName === "spotify") - { - return this.getMostListenedArtistFromSpotify(providerToken); - } - else if (providerName === "youtube") - { - return this.getMostListenedArtistFromYouTube(providerToken); - } + try { + const response = await axios.get(url); + const likedVideos = response.data.items; - throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); - } + let mood = "Energetic"; // Default mood + if (likedVideos.some((video) => video.snippet.categoryId === "Music")) { + mood = "Energetic"; + } else if (likedVideos.some((video) => video.snippet.categoryId === "Education")) { + mood = "Focused"; + } - async getTopMoodFromSpotify(providerToken: string): Promise - { - const url = "https://api.spotify.com/v1/me/top/tracks"; - try - { - const response = await axios.get(url, { - headers: { - Authorization: `Bearer ${providerToken}` - } - }); - const topTracks = response.data.items; - const mood = "Happy"; return { mood }; + } catch (error) { + throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } - catch (error) - { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + + // Get total listening time from Spotify or YouTube + async getTotalListeningTime( + accessToken: string, + refreshToken: string, + providerName: string + ): Promise { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); + + if (providerName === "spotify") { + return this.getTotalListeningTimeFromSpotify(providerToken); + } else if (providerName === "youtube") { + return this.getTotalListeningTimeFromYouTube(providerToken); } + + throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getTotalListeningTimeFromSpotify(providerToken: string): Promise - { + async getTotalListeningTimeFromSpotify(providerToken: string): Promise { const url = "https://api.spotify.com/v1/me/player/recently-played"; - try - { + try { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}` - } + Authorization: `Bearer ${providerToken}`, + }, }); const tracks = response.data.items; let totalListeningTime = 0; - tracks.forEach((track) => - { + tracks.forEach((track) => { totalListeningTime += track.track.duration_ms; }); const hours = totalListeningTime / (1000 * 60 * 60); return { totalListeningTime: `${hours.toFixed(2)} hours` }; - } - catch (error) - { + } catch (error) { throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); } } - async getMostPlayedTrackFromSpotify(providerToken: string): Promise - { - const url = "https://api.spotify.com/v1/me/top/tracks?limit=1"; - try - { - const response = await axios.get(url, { - headers: { - Authorization: `Bearer ${providerToken}` - } + async getTotalListeningTimeFromYouTube(providerToken: string): Promise { + const url = `https://www.googleapis.com/youtube/v3/videos?part=contentDetails&myRating=like&access_token=${providerToken}`; + try { + const response = await axios.get(url); + const likedVideos = response.data.items; + let totalListeningTime = 0; + + likedVideos.forEach((video) => { + const duration = video.contentDetails.duration; + const time = this.parseYouTubeDuration(duration); + totalListeningTime += time; }); - const track = response.data.items[0]; - return { - name: track.name, - artist: track.artists.map((artist) => artist.name).join(", ") - }; + + const hours = totalListeningTime / (60 * 60); + return { totalListeningTime: `${hours.toFixed(2)} hours` }; + } catch (error) { + throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } - catch (error) - { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + + // Get most played track for a user + async getMostPlayedTrack( + accessToken: string, + refreshToken: string, + providerName: string + ): Promise { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); + + if (providerName === "spotify") { + return this.getMostPlayedTrackFromSpotify(providerToken); + } else if (providerName === "youtube") { + return this.getMostPlayedTrackFromYouTube(providerToken); } + + throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getMostListenedArtistFromSpotify(providerToken: string): Promise - { - const url = "https://api.spotify.com/v1/me/top/artists?limit=1"; - try - { + async getMostPlayedTrackFromSpotify(providerToken: string): Promise { + const url = "https://api.spotify.com/v1/me/top/tracks?limit=1"; + try { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}` - } + Authorization: `Bearer ${providerToken}`, + }, }); - const artist = response.data.items[0]; - return { artist: artist.name }; - } - catch (error) - { + const track = response.data.items[0]; + return { + name: track.name, + artist: track.artists.map((artist) => artist.name).join(", "), + }; + } catch (error) { throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); } } - // --- YouTube-specific methods --- - - async getTopMoodFromYouTube(providerToken: string): Promise - { + async getMostPlayedTrackFromYouTube(providerToken: string): Promise { const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try - { + try { const response = await axios.get(url); - const likedVideos = response.data.items; - const mood = "Energetic"; // Simplified mood analysis logic - return { mood }; - } - catch (error) - { + const mostLikedVideo = response.data.items[0]; + return { name: mostLikedVideo.snippet.title }; + } catch (error) { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } - async getTotalListeningTimeFromYouTube(providerToken: string): Promise - { - const url = `https://www.googleapis.com/youtube/v3/videos?part=contentDetails&myRating=like&access_token=${providerToken}`; - try - { - const response = await axios.get(url); - const likedVideos = response.data.items; - let totalListeningTime = 0; - - likedVideos.forEach((video) => - { - const duration = video.contentDetails.duration; - const time = this.parseYouTubeDuration(duration); - totalListeningTime += time; - }); + // Get most listened artist for a user + async getMostListenedArtist( + accessToken: string, + refreshToken: string, + providerName: string + ): Promise { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); - const hours = totalListeningTime / (60 * 60); - return { totalListeningTime: `${hours.toFixed(2)} hours` }; - } - catch (error) - { - throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); + if (providerName === "spotify") { + return this.getMostListenedArtistFromSpotify(providerToken); + } else if (providerName === "youtube") { + return this.getMostListenedArtistFromYouTube(providerToken); } + + throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getMostPlayedTrackFromYouTube(providerToken: string): Promise - { - const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try - { - const response = await axios.get(url); - const mostLikedVideo = response.data.items[0]; - return { name: mostLikedVideo.snippet.title }; - } - catch (error) - { - throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); + async getMostListenedArtistFromSpotify(providerToken: string): Promise { + const url = "https://api.spotify.com/v1/me/top/artists?limit=1"; + try { + const response = await axios.get(url, { + headers: { + Authorization: `Bearer ${providerToken}`, + }, + }); + const artist = response.data.items[0]; + return { artist: artist.name }; + } catch (error) { + throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); } } - async getMostListenedArtistFromYouTube(providerToken: string): Promise - { + async getMostListenedArtistFromYouTube(providerToken: string): Promise { const url = `https://www.googleapis.com/youtube/v3/channels?part=snippet&mine=true&access_token=${providerToken}`; - try - { + try { const response = await axios.get(url); const channel = response.data.items[0]; return { artist: channel.snippet.title }; - } - catch (error) - { + } catch (error) { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } // Helper to parse YouTube ISO 8601 duration - parseYouTubeDuration(duration: string): number - { + parseYouTubeDuration(duration: string): number { const regex = /PT(\d+H)?(\d+M)?(\d+S)?/; const matches = regex.exec(duration); let totalSeconds = 0; @@ -302,52 +259,39 @@ export class InsightsService // Get top genre from Spotify or YouTube async getTopGenre( - userId: string, accessToken: string, - providerToken: string, + refreshToken: string, providerName: string - ): Promise - { - const user = await this.getUserFromSupabase(accessToken); - if (!user || user.id !== userId) - { - throw new HttpException("User not authorized", HttpStatus.UNAUTHORIZED); - } + ): Promise { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") - { + if (providerName === "spotify") { return this.getTopGenreFromSpotify(providerToken); - } - else if (providerName === "youtube") - { + } else if (providerName === "youtube") { return this.getTopGenreFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getTopGenreFromSpotify(providerToken: string): Promise - { + async getTopGenreFromSpotify(providerToken: string): Promise { const url = "https://api.spotify.com/v1/me/top/tracks"; - try - { + try { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}` - } + Authorization: `Bearer ${providerToken}`, + }, }); const topTracks = response.data.items; const genres = {}; // Accumulate genres from artists - topTracks.forEach((track) => - { - track.artists.forEach((artist) => - { - artist.genres.forEach((genre) => - { - if (!genres[genre]) - { + topTracks.forEach((track) => { + track.artists.forEach((artist) => { + artist.genres.forEach((genre) => { + if (!genres[genre]) { genres[genre] = 0; } genres[genre]++; @@ -358,70 +302,54 @@ export class InsightsService // Find the top genre const topGenre = Object.keys(genres).reduce((a, b) => (genres[a] > genres[b] ? a : b)); return { topGenre }; - } - catch (error) - { + } catch (error) { throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); } } - async getTopGenreFromYouTube(providerToken: string): Promise - { + async getTopGenreFromYouTube(providerToken: string): Promise { const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try - { + try { const response = await axios.get(url); const likedVideos = response.data.items; - const genre = "Pop"; + const genre = "Pop"; // You'll need to implement logic to determine genre from YouTube data return { topGenre: genre }; - } - catch (error) - { + } catch (error) { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } // Get average song duration async getAverageSongDuration( - userId: string, accessToken: string, - providerToken: string, + refreshToken: string, providerName: string - ): Promise - { - const user = await this.getUserFromSupabase(accessToken); - if (!user || user.id !== userId) - { - throw new HttpException("User not authorized", HttpStatus.UNAUTHORIZED); - } + ): Promise { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") - { + if (providerName === "spotify") { return this.getAverageSongDurationFromSpotify(providerToken); - } - else if (providerName === "youtube") - { + } else if (providerName === "youtube") { return this.getAverageSongDurationFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getAverageSongDurationFromSpotify(providerToken: string): Promise - { + async getAverageSongDurationFromSpotify(providerToken: string): Promise { const url = "https://api.spotify.com/v1/me/player/recently-played"; - try - { + try { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}` - } + Authorization: `Bearer ${providerToken}`, + }, }); const tracks = response.data.items; let totalDuration = 0; - tracks.forEach((track) => - { + tracks.forEach((track) => { totalDuration += track.track.duration_ms; }); @@ -429,24 +357,19 @@ export class InsightsService const minutes = Math.floor(averageDuration / 60000); const seconds = Math.floor((averageDuration % 60000) / 1000); return { averageDuration: `${minutes}:${seconds < 10 ? "0" : ""}${seconds}` }; - } - catch (error) - { + } catch (error) { throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); } } - async getAverageSongDurationFromYouTube(providerToken: string): Promise - { + async getAverageSongDurationFromYouTube(providerToken: string): Promise { const url = `https://www.googleapis.com/youtube/v3/videos?part=contentDetails&myRating=like&access_token=${providerToken}`; - try - { + try { const response = await axios.get(url); const likedVideos = response.data.items; let totalDuration = 0; - likedVideos.forEach((video) => - { + likedVideos.forEach((video) => { const duration = video.contentDetails.duration; const time = this.parseYouTubeDuration(duration); totalDuration += time; @@ -454,57 +377,45 @@ export class InsightsService const averageDuration = totalDuration / likedVideos.length; const minutes = Math.floor(averageDuration / 60); - const seconds = Math.floor((averageDuration % 60000) / 1000); + const seconds = Math.floor(averageDuration % 60); return { averageDuration: `${minutes}:${seconds < 10 ? "0" : ""}${seconds}` }; - } - catch (error) - { + } catch (error) { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } // Get most active day async getMostActiveDay( - userId: string, accessToken: string, - providerToken: string, + refreshToken: string, providerName: string - ): Promise - { - const user = await this.getUserFromSupabase(accessToken); - if (!user || user.id !== userId) - { - throw new HttpException("User not authorized", HttpStatus.UNAUTHORIZED); - } + ): Promise { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") - { + if (providerName === "spotify") { return this.getMostActiveDayFromSpotify(providerToken); - } - else if (providerName === "youtube") - { + } else if (providerName === "youtube") { return this.getMostActiveDayFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getMostActiveDayFromSpotify(providerToken: string): Promise - { + async getMostActiveDayFromSpotify(providerToken: string): Promise { const url = "https://api.spotify.com/v1/me/player/recently-played"; - try - { + try { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}` - } + Authorization: `Bearer ${providerToken}`, + }, }); const tracks = response.data.items; const days = {}; // Count tracks per day - tracks.forEach((track) => - { + tracks.forEach((track) => { const date = new Date(track.played_at).toLocaleDateString("en-US", { weekday: "long" }); days[date] = (days[date] || 0) + 1; }); @@ -512,96 +423,77 @@ export class InsightsService // Find the day with the most plays const mostActiveDay = Object.keys(days).reduce((a, b) => (days[a] > days[b] ? a : b)); return { mostActiveDay }; - } - catch (error) - { + } catch (error) { throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); } } - async getMostActiveDayFromYouTube(providerToken: string): Promise - { + async getMostActiveDayFromYouTube(providerToken: string): Promise { const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try - { + try { const response = await axios.get(url); const likedVideos = response.data.items; const days = {}; // Count videos per day - likedVideos.forEach((video) => - { - const date = new Date(video.snippet.publishedAt).toLocaleDateString("en-US", { weekday: "long" }); + likedVideos.forEach((video) => { + const date = new Date(video.snippet.publishedAt).toLocaleDateString("en-US", { + weekday: "long", + }); days[date] = (days[date] || 0) + 1; }); // Find the day with the most plays const mostActiveDay = Object.keys(days).reduce((a, b) => (days[a] > days[b] ? a : b)); return { mostActiveDay }; - } - catch (error) - { + } catch (error) { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } // Get unique artists listened async getUniqueArtistsListened( - userId: string, accessToken: string, - providerToken: string, + refreshToken: string, providerName: string - ): Promise - { - const user = await this.getUserFromSupabase(accessToken); - if (!user || user.id !== userId) - { - throw new HttpException("User not authorized", HttpStatus.UNAUTHORIZED); - } + ): Promise { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") - { + if (providerName === "spotify") { return this.getUniqueArtistsFromSpotify(providerToken); - } - else if (providerName === "youtube") - { + } else if (providerName === "youtube") { return this.getUniqueArtistsFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getUniqueArtistsFromSpotify(providerToken: string): Promise - { + async getUniqueArtistsFromSpotify(providerToken: string): Promise { const url = "https://api.spotify.com/v1/me/player/recently-played"; - try - { + try { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}` - } + Authorization: `Bearer ${providerToken}`, + }, }); const tracks = response.data.items; const uniqueArtists = new Set(); - tracks.forEach((track) => - { + tracks.forEach((track) => { track.track.artists.forEach((artist) => uniqueArtists.add(artist.name)); }); return { uniqueArtists: Array.from(uniqueArtists) }; - } - catch (error) - { + } catch (error) { throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); } } - async getUniqueArtistsFromYouTube(providerToken: string): Promise - { + async getUniqueArtistsFromYouTube(providerToken: string): Promise { const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try - { + try { const response = await axios.get(url); const likedVideos = response.data.items; const uniqueChannels = new Set(); @@ -609,11 +501,32 @@ export class InsightsService likedVideos.forEach((video) => uniqueChannels.add(video.snippet.channelTitle)); return { uniqueArtists: Array.from(uniqueChannels) }; - } - catch (error) - { + } catch (error) { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } -} + private async getUserIdFromAccessToken(accessToken: string): Promise { + const supabase = createSupabaseClient(); + const { data, error } = await supabase.auth.getUser(accessToken); + + if (error || !data || !data.user) { + throw new HttpException("Invalid access token", HttpStatus.UNAUTHORIZED); + } + + return data.user.id; + } + + private async setSupabaseSession(accessToken: string, refreshToken: string): Promise { + const supabase = createSupabaseClient(); + const { error } = await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }); + + if (error) { + console.error("Error setting Supabase session:", error); + throw new HttpException("Failed to set Supabase session", HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} \ No newline at end of file From 0ea6da57c06e685f4f43b61212e802095ff7090f Mon Sep 17 00:00:00 2001 From: 21797545 Date: Thu, 17 Oct 2024 19:11:45 +0200 Subject: [PATCH 02/10] :triangular_ruler: Added supabase as provider in insights module --- Backend/package.json | 2 +- Backend/src/insights/insights.module.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Backend/package.json b/Backend/package.json index b4c0bac8..7a9387fe 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -10,7 +10,7 @@ "jest --coverage": "jest", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "node dist/main.js", + "start": "nest start --watch", "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main.js", diff --git a/Backend/src/insights/insights.module.ts b/Backend/src/insights/insights.module.ts index a89ff62d..7fffdb89 100644 --- a/Backend/src/insights/insights.module.ts +++ b/Backend/src/insights/insights.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; import { InsightsService } from './services/insights.service'; import { InsightsController } from './controller/insights.controller'; +import { HttpModule } from "@nestjs/axios"; +import { SupabaseModule } from "../supabase/supabase.module"; +import { SupabaseService } from "../supabase/services/supabase.service"; @Module({ + imports: [HttpModule, SupabaseModule], controllers: [InsightsController], - providers: [InsightsService], + providers: [InsightsService, SupabaseService], }) export class InsightsModule {} From cabbbeb3bfc1da34bb5504d5412aa4692f52437f Mon Sep 17 00:00:00 2001 From: 21797545 Date: Thu, 17 Oct 2024 19:12:13 +0200 Subject: [PATCH 03/10] :triangular_ruler: Changed frontend insights service to use updated inisghts controller endpoints --- Frontend/src/app/services/insights.service.ts | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/Frontend/src/app/services/insights.service.ts b/Frontend/src/app/services/insights.service.ts index 096005d4..5daf485e 100644 --- a/Frontend/src/app/services/insights.service.ts +++ b/Frontend/src/app/services/insights.service.ts @@ -1,13 +1,13 @@ import { Injectable } from "@angular/core"; -import { HttpClient, HttpParams } from "@angular/common/http"; -import { Observable, of } from "rxjs"; +import { HttpClient } from "@angular/common/http"; +import { Observable, of, throwError } from "rxjs"; import { TokenService } from "./token.service"; import { ProviderService } from "./provider.service"; import { environment } from "../../environments/environment"; -import { catchError, switchMap } from 'rxjs/operators'; +import { catchError, switchMap } from "rxjs/operators"; @Injectable({ - providedIn: "root" + providedIn: "root", }) export class InsightsService { private apiUrl = `${environment.apiUrl}/insights`; @@ -18,40 +18,39 @@ export class InsightsService { private providerService: ProviderService ) {} - private getParams(): Observable { + private getParams(): Observable<{ accessToken: string; refreshToken: string; providerName: string }> { return this.tokenService.getAccessToken$().pipe( - switchMap(accessToken => { + switchMap((accessToken) => { if (!accessToken) { - throw new Error('No access token available'); + return throwError(() => new Error("No access token available")); } - const providerName = this.providerService.getProviderName(); - // Note: You'll need to implement a way to get the provider token - // This is just a placeholder. Replace with your actual implementation. - const providerToken = this.getProviderToken(providerName); - if (!providerToken) { - throw new Error(`No provider token available for ${providerName}`); + + const refreshToken = this.tokenService.getRefreshToken(); + if (!refreshToken) { + return throwError(() => new Error("No refresh token available")); } - return of(new HttpParams({ - fromObject: { - accessToken, - providerToken, - providerName - } - })); + + const providerName = this.providerService.getProviderName(); + + return of({ accessToken, refreshToken, providerName }); + }), + catchError((error) => { + console.error("Error getting parameters:", error); + return throwError(() => error); }) ); } - // Placeholder method. Implement this based on how you're storing provider tokens. - private getProviderToken(providerName: string): string | null { - // This is just an example. Replace with your actual implementation. - return localStorage.getItem(`${providerName}Token`); - } - private makeRequest(endpoint: string): Observable { return this.getParams().pipe( - switchMap(params => this.http.get(`${this.apiUrl}/${endpoint}`, { params })), - catchError(error => { + switchMap(({ accessToken, refreshToken, providerName }) => + this.http.post(`${this.apiUrl}/${endpoint}`, { + accessToken, + refreshToken, + providerName, + }) + ), + catchError((error) => { console.error(`Error fetching ${endpoint}:`, error); return of(null); }) @@ -59,34 +58,34 @@ export class InsightsService { } getTopMood(): Observable { - return this.makeRequest('top-mood'); + return this.makeRequest("top-mood"); } getTotalListeningTime(): Observable { - return this.makeRequest('total-listening-time'); + return this.makeRequest("total-listening-time"); } getMostListenedArtist(): Observable { - return this.makeRequest('most-listened-artist'); + return this.makeRequest("most-listened-artist"); } getMostPlayedTrack(): Observable { - return this.makeRequest('most-played-track'); + return this.makeRequest("most-played-track"); } getTopGenre(): Observable { - return this.makeRequest('top-genre'); + return this.makeRequest("top-genre"); } getAverageSongDuration(): Observable { - return this.makeRequest('average-song-duration'); + return this.makeRequest("average-song-duration"); } getMostActiveDay(): Observable { - return this.makeRequest('most-active-day'); + return this.makeRequest("most-active-day"); } getUniqueArtistsListened(): Observable { - return this.makeRequest('unique-artists-listened'); + return this.makeRequest("unique-artists-listened"); } } From e40b9091b72488855179c75120721d99d0146c50 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Thu, 17 Oct 2024 19:13:57 +0200 Subject: [PATCH 04/10] :tada: Integrated insights --- .../pages/insights/insights.component.html | 24 ++++---- .../app/pages/insights/insights.component.ts | 60 +++++++++++++++++-- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/Frontend/src/app/pages/insights/insights.component.html b/Frontend/src/app/pages/insights/insights.component.html index 28a00778..e69e5ed2 100644 --- a/Frontend/src/app/pages/insights/insights.component.html +++ b/Frontend/src/app/pages/insights/insights.component.html @@ -1,55 +1,55 @@

Listening Insights

-
+

Top Mood

-

Joy

+

{{ topMood }}

Total Listening Time

-

42 hrs

+

{{ totalListeningTime }}

Most Listened Artist

-

The Weeknd

+

{{ mostListenedArtist }}

Most Played Track

-

Blinding Lights

+

{{ mostPlayedTrack }}

Top Genre

-

Pop

+

{{ topGenre }}

Average Song Duration

-

3 min 45 sec

+

{{ averageSongDuration }}

Most Active Day

-

Saturday

+

{{ mostActiveDay }}

Unique Artists Listened

-

28

+

{{ uniqueArtistsListened }}

-
+

Mood Distribution

-
+

Listening by Service

-
+

Top Genres

diff --git a/Frontend/src/app/pages/insights/insights.component.ts b/Frontend/src/app/pages/insights/insights.component.ts index f3cb48e0..99fc95bc 100644 --- a/Frontend/src/app/pages/insights/insights.component.ts +++ b/Frontend/src/app/pages/insights/insights.component.ts @@ -3,6 +3,7 @@ import { isPlatformBrowser } from "@angular/common"; import Chart, { ChartType } from "chart.js/auto"; import { MoodService } from '../../services/mood-service.service'; import { NgClass, NgIf } from '@angular/common'; +import { InsightsService } from "../../services/insights.service"; @Component({ selector: "app-insights", @@ -19,13 +20,26 @@ export class InsightsComponent implements AfterViewInit, AfterViewChecked { public moodComponentClasses!: { [key: string]: string }; private chartInitialized: boolean = false; + topMood: string = ''; + totalListeningTime: string = ''; + mostListenedArtist: string = ''; + mostPlayedTrack: string = ''; + topGenre: string = ''; + averageSongDuration: string = ''; + mostActiveDay: string = ''; + uniqueArtistsListened: number = 0; + // ViewChild sections for smooth scrolling @ViewChild('widgets', { static: false }) widgetsSection!: ElementRef; @ViewChild('moodChart', { static: false }) moodChartSection!: ElementRef; @ViewChild('serviceChart', { static: false }) serviceChartSection!: ElementRef; @ViewChild('genreChart', { static: false }) genreChartSection!: ElementRef; - constructor(@Inject(PLATFORM_ID) private platformId: Object, public moodService: MoodService) { + constructor( + @Inject(PLATFORM_ID) private platformId: Object, + public moodService: MoodService, + private insightsService: InsightsService + ) { this.moodComponentClasses = { 'Joy': 'bg-yellow-400 text-black', 'Sadness': 'bg-blue-400 text-white', @@ -34,6 +48,9 @@ export class InsightsComponent implements AfterViewInit, AfterViewChecked { 'Fear': 'bg-gray-400 text-white', 'Optimism': 'bg-green-400 text-white' }; + + // Fetch insights data from backend + this.fetchInsights(); } ngAfterViewInit() { @@ -108,7 +125,7 @@ export class InsightsComponent implements AfterViewInit, AfterViewChecked { ], datasets: [{ label: 'Mood Distribution', - data: [30, 10, 5, 3, 7, 8, 25, 5, 2, 5], + data: [30, 10, 5, 3, 7, 8, 25, 5, 2, 5], // Replace with actual mood data if available backgroundColor: [ '#facc15', '#94a3b8', '#ef4444', '#a3e635', '#3b82f6', '#eab308', '#ec4899', '#10b981', '#fb923c', '#6b7280' @@ -123,7 +140,7 @@ export class InsightsComponent implements AfterViewInit, AfterViewChecked { labels: ['Spotify', 'YouTube'], datasets: [{ label: 'Listening Distribution', - data: [70, 30], + data: [70, 30], // Replace with actual service distribution data if available backgroundColor: ['#1DB954', '#FF0000'], }] }; @@ -134,11 +151,46 @@ export class InsightsComponent implements AfterViewInit, AfterViewChecked { labels: ['Pop', 'Rock', 'Hip-Hop', 'Electronic', 'Jazz', 'Classical', 'Indie', 'R&B'], datasets: [{ label: 'Top Genres', - data: [35, 20, 15, 10, 5, 5, 7, 3], + data: [35, 20, 15, 10, 5, 5, 7, 3], // Replace with actual genre data if available backgroundColor: [ '#f43f5e', '#3b82f6', '#22c55e', '#facc15', '#6366f1', '#8b5cf6', '#f59e0b', '#10b981' ] }] }; } + + fetchInsights(): void { + this.insightsService.getTopMood().subscribe(data => { + this.topMood = data?.mood || 'N/A'; + }); + + this.insightsService.getTotalListeningTime().subscribe(data => { + this.totalListeningTime = data?.totalListeningTime || 'N/A'; + }); + + this.insightsService.getMostListenedArtist().subscribe(data => { + this.mostListenedArtist = data?.artist || 'N/A'; + }); + + this.insightsService.getMostPlayedTrack().subscribe(data => { + this.mostPlayedTrack = data?.name || 'N/A'; + }); + + this.insightsService.getTopGenre().subscribe(data => { + this.topGenre = data?.topGenre || 'N/A'; + }); + + this.insightsService.getAverageSongDuration().subscribe(data => { + this.averageSongDuration = data?.averageDuration || 'N/A'; + }); + + this.insightsService.getMostActiveDay().subscribe(data => { + this.mostActiveDay = data?.mostActiveDay || 'N/A'; + }); + + this.insightsService.getUniqueArtistsListened().subscribe(data => { + this.uniqueArtistsListened = data?.uniqueArtists?.length || 0; + }); + } + } From 9ba481a801916f4456f0bd29de8c31cc4d0f4675 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Thu, 17 Oct 2024 20:48:46 +0200 Subject: [PATCH 05/10] :tada: New endpoints for insights --- .../controller/insights.controller.ts | 50 ++++ .../src/insights/services/insights.service.ts | 247 ++++++++++++++++++ 2 files changed, 297 insertions(+) diff --git a/Backend/src/insights/controller/insights.controller.ts b/Backend/src/insights/controller/insights.controller.ts index 331aadea..da53bb85 100644 --- a/Backend/src/insights/controller/insights.controller.ts +++ b/Backend/src/insights/controller/insights.controller.ts @@ -76,4 +76,54 @@ export class InsightsController { } return this.insightsService.getUniqueArtistsListened(accessToken, refreshToken, providerName); } + + @Post("listening-trends") + async getListeningTrends(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { + throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); + } + return this.insightsService.getListeningTrends(accessToken, refreshToken, providerName); + } + + @Post("weekly-playlist") + async getWeeklyPlaylist(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { + throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); + } + return this.insightsService.getWeeklyPlaylist(accessToken, refreshToken, providerName); + } + + @Post("most-listened-day") + async getMostListenedDay(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { + throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); + } + return this.insightsService.getMostListenedDay(accessToken, refreshToken, providerName); + } + + @Post("listening-over-time") + async getListeningOverTime(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + return this.validateAndCallService(body, this.insightsService.getListeningOverTime); + } + + @Post("artists-vs-tracks") + async getArtistsVsTracks(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + return this.validateAndCallService(body, this.insightsService.getArtistsVsTracks); + } + + @Post("recent-track-genres") + async getRecentTrackGenres(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + return this.validateAndCallService(body, this.insightsService.getRecentTrackGenres); + } + + private validateAndCallService(body: any, serviceMethod: Function) { + const { accessToken, refreshToken, providerName } = body; + if (!accessToken || !refreshToken || !providerName) { + throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); + } + return serviceMethod.call(this.insightsService, accessToken, refreshToken, providerName); + } } diff --git a/Backend/src/insights/services/insights.service.ts b/Backend/src/insights/services/insights.service.ts index 0dd27f6c..9961e3c6 100644 --- a/Backend/src/insights/services/insights.service.ts +++ b/Backend/src/insights/services/insights.service.ts @@ -529,4 +529,251 @@ export class InsightsService { throw new HttpException("Failed to set Supabase session", HttpStatus.INTERNAL_SERVER_ERROR); } } + + async getListeningTrends(accessToken: string, refreshToken: string, providerName: string) { + if (providerName === 'spotify') { + return await this.getSpotifyListeningTrends(accessToken); + } else if (providerName === 'youtube') { + return await this.getYoutubeListeningTrends(accessToken); + } else { + throw new HttpException("Unsupported provider", HttpStatus.BAD_REQUEST); + } + } + + // Method to get weekly playlist + async getWeeklyPlaylist(accessToken: string, refreshToken: string, providerName: string) { + if (providerName === 'spotify') { + return await this.getSpotifyWeeklyPlaylist(accessToken); + } else if (providerName === 'youtube') { + return await this.getYoutubeWeeklyPlaylist(accessToken); + } else { + throw new HttpException("Unsupported provider", HttpStatus.BAD_REQUEST); + } + } + + // Method to get the most listened day + async getMostListenedDay(accessToken: string, refreshToken: string, providerName: string) { + if (providerName === 'spotify') { + return await this.getSpotifyMostListenedDay(accessToken); + } else if (providerName === 'youtube') { + return await this.getYoutubeMostListenedDay(accessToken); + } else { + throw new HttpException("Unsupported provider", HttpStatus.BAD_REQUEST); + } + } + + // Spotify-specific method to get listening trends + private async getSpotifyListeningTrends(accessToken: string) { + const response = await axios.get('https://api.spotify.com/v1/me/top/tracks', { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + return response.data; + } + + // YouTube-specific method to get listening trends + private async getYoutubeListeningTrends(accessToken: string) { + // Replace with the actual endpoint and logic to fetch YouTube trends + // This is a placeholder example + const response = await axios.get('https://www.googleapis.com/youtube/v3/videos', { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { + part: 'snippet,contentDetails', + chart: 'mostPopular', + regionCode: 'US', + }, + }); + return response.data; + } + + // Spotify-specific method to get the weekly playlist + private async getSpotifyWeeklyPlaylist(accessToken: string) { + const response = await axios.get('https://api.spotify.com/v1/me/top/artists', { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + return response.data; + } + + // YouTube-specific method to get the weekly playlist + private async getYoutubeWeeklyPlaylist(accessToken: string) { + // Replace with the actual endpoint and logic to fetch YouTube playlists + const response = await axios.get('https://www.googleapis.com/youtube/v3/playlists', { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { + part: 'snippet', + maxResults: 10, + }, + }); + return response.data; + } + + // Spotify-specific method to get the most listened day + private async getSpotifyMostListenedDay(accessToken: string) { + const response = await axios.get('https://api.spotify.com/v1/me/top/artists', { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + return response.data; // Modify according to the actual API response structure + } + + // YouTube-specific method to get the most listened day + private async getYoutubeMostListenedDay(accessToken: string) { + // Replace with the actual logic to determine the most listened day on YouTube + // This is a placeholder example + const response = await axios.get('https://www.googleapis.com/youtube/v3/videos', { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { + part: 'snippet', + chart: 'mostPopular', + }, + }); + return response.data; // Modify according to the actual API response structure + } + + async getListeningOverTime(accessToken: string, refreshToken: string, providerName: string) { + if (providerName === "spotify") { + return this.fetchSpotifyListeningOverTime(accessToken, refreshToken); + } else if (providerName === "youtube") { + return this.fetchYouTubeListeningOverTime(accessToken, refreshToken); + } + throw new HttpException("Invalid provider name", HttpStatus.BAD_REQUEST); + } + + // Fetch comparison of distinct artists vs tracks for Spotify or YouTube + async getArtistsVsTracks(accessToken: string, refreshToken: string, providerName: string) { + if (providerName === "spotify") { + return this.fetchSpotifyArtistsVsTracks(accessToken, refreshToken); + } else if (providerName === "youtube") { + return this.fetchYouTubeArtistsVsTracks(accessToken, refreshToken); + } + throw new HttpException("Invalid provider name", HttpStatus.BAD_REQUEST); + } + + // Fetch recent track genres for Spotify or YouTube + async getRecentTrackGenres(accessToken: string, refreshToken: string, providerName: string) { + if (providerName === "spotify") { + return this.fetchSpotifyRecentTrackGenres(accessToken, refreshToken); + } else if (providerName === "youtube") { + return this.fetchYouTubeRecentTrackGenres(accessToken, refreshToken); + } + throw new HttpException("Invalid provider name", HttpStatus.BAD_REQUEST); + } + + // ====== Spotify Methods ====== + + // Fetch listening over time from Spotify + private async fetchSpotifyListeningOverTime(accessToken: string, refreshToken: string) { + const response = await axios.get('https://api.spotify.com/v1/me/player/recently-played', { + headers: { Authorization: `Bearer ${accessToken}` } + }); + return this.parseListeningOverTime(response.data.items); + } + + // Fetch distinct artists and tracks from Spotify + private async fetchSpotifyArtistsVsTracks(accessToken: string, refreshToken: string) { + const artistResponse = await axios.get('https://api.spotify.com/v1/me/top/artists', { + headers: { Authorization: `Bearer ${accessToken}` } + }); + const trackResponse = await axios.get('https://api.spotify.com/v1/me/top/tracks', { + headers: { Authorization: `Bearer ${accessToken}` } + }); + return this.parseArtistsVsTracks(artistResponse.data.items, trackResponse.data.items); + } + + // Fetch recent track genres from Spotify + private async fetchSpotifyRecentTrackGenres(accessToken: string, refreshToken: string) { + const response = await axios.get('https://api.spotify.com/v1/me/top/tracks', { + headers: { Authorization: `Bearer ${accessToken}` } + }); + return this.parseRecentTrackGenres(response.data.items); + } + + // ====== YouTube Methods ====== + + // Fetch listening over time from YouTube + private async fetchYouTubeListeningOverTime(accessToken: string, refreshToken: string) { + const response = await axios.get(`https://www.googleapis.com/youtube/v3/videos`, { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { + part: 'snippet,contentDetails,statistics', + myRating: 'like' // Example of fetching videos liked by user + } + }); + return this.parseListeningOverTime(response.data.items); + } + + // Fetch distinct artists and tracks from YouTube + private async fetchYouTubeArtistsVsTracks(accessToken: string, refreshToken: string) { + const playlistResponse = await axios.get(`https://www.googleapis.com/youtube/v3/playlists`, { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { + part: 'snippet', + mine: true + } + }); + // YouTube API doesn't have the same concept of "artists" as Spotify, + // so we'll assume each playlist corresponds to an artist. + const trackResponse = await axios.get(`https://www.googleapis.com/youtube/v3/playlistItems`, { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { + part: 'snippet', + playlistId: playlistResponse.data.items[0].id + } + }); + return this.parseArtistsVsTracks(playlistResponse.data.items, trackResponse.data.items); + } + + // Fetch recent track genres from YouTube + private async fetchYouTubeRecentTrackGenres(accessToken: string, refreshToken: string) { + const response = await axios.get(`https://www.googleapis.com/youtube/v3/videos`, { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { + part: 'snippet,contentDetails,statistics', + myRating: 'like' + } + }); + return this.parseRecentTrackGenres(response.data.items); + } + + // ====== Parsing Methods ====== + + // Parse listening data over time + private parseListeningOverTime(data: any) { + const result = {}; + data.forEach((item: any) => { + const date = new Date(item.played_at || item.snippet.publishedAt).toDateString(); + if (!result[date]) { + result[date] = 1; + } else { + result[date]++; + } + }); + return result; // Format suitable for graphing: { 'Date': playCount } + } + + // Parse artists vs tracks data + private parseArtistsVsTracks(artistsData: any, tracksData: any) { + const distinctArtists = new Set(); + artistsData.forEach((artist: any) => distinctArtists.add(artist.name)); + + const distinctTracks = new Set(); + tracksData.forEach((track: any) => distinctTracks.add(track.name)); + + return { + distinctArtists: distinctArtists.size, + distinctTracks: distinctTracks.size + }; // Format suitable for graphing: { distinctArtists, distinctTracks } + } + + // Parse recent track genres data + private parseRecentTrackGenres(tracksData: any) { + const genres = {}; + tracksData.forEach((track: any) => { + const genre = track.album?.genres?.[0] || "Unknown"; // Assuming genre from album data + if (!genres[genre]) { + genres[genre] = 1; + } else { + genres[genre]++; + } + }); + return genres; // Format suitable for graphing: { 'Genre': count } + } } \ No newline at end of file From 3d64afb1283ebcd15d0b4c15576d2a30f1ba8c11 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Sun, 20 Oct 2024 13:08:13 +0200 Subject: [PATCH 06/10] :triangular_ruler: Adjusted graph endpoints in insights controller and service --- .../controller/insights.controller.ts | 88 +- .../src/insights/services/insights.service.ts | 819 ++++++++++++------ 2 files changed, 603 insertions(+), 304 deletions(-) diff --git a/Backend/src/insights/controller/insights.controller.ts b/Backend/src/insights/controller/insights.controller.ts index da53bb85..e9db7964 100644 --- a/Backend/src/insights/controller/insights.controller.ts +++ b/Backend/src/insights/controller/insights.controller.ts @@ -2,126 +2,156 @@ import { Controller, Post, Body, HttpException, HttpStatus } from "@nestjs/commo import { InsightsService } from "../services/insights.service"; @Controller("insights") -export class InsightsController { - constructor(private readonly insightsService: InsightsService) {} +export class InsightsController +{ + constructor(private readonly insightsService: InsightsService) + { + } @Post("top-mood") - async getTopMood(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getTopMood(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getTopMood(accessToken, refreshToken, providerName); } @Post("total-listening-time") - async getTotalListeningTime(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getTotalListeningTime(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getTotalListeningTime(accessToken, refreshToken, providerName); } @Post("most-listened-artist") - async getMostListenedArtist(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getMostListenedArtist(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getMostListenedArtist(accessToken, refreshToken, providerName); } @Post("most-played-track") - async getMostPlayedTrack(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getMostPlayedTrack(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getMostPlayedTrack(accessToken, refreshToken, providerName); } @Post("top-genre") - async getTopGenre(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getTopGenre(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getTopGenre(accessToken, refreshToken, providerName); } @Post("average-song-duration") - async getAverageSongDuration(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getAverageSongDuration(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getAverageSongDuration(accessToken, refreshToken, providerName); } @Post("most-active-day") - async getMostActiveDay(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getMostActiveDay(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getMostActiveDay(accessToken, refreshToken, providerName); } @Post("unique-artists-listened") - async getUniqueArtistsListened(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getUniqueArtistsListened(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getUniqueArtistsListened(accessToken, refreshToken, providerName); } @Post("listening-trends") - async getListeningTrends(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getListeningTrends(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getListeningTrends(accessToken, refreshToken, providerName); } @Post("weekly-playlist") - async getWeeklyPlaylist(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getWeeklyPlaylist(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getWeeklyPlaylist(accessToken, refreshToken, providerName); } @Post("most-listened-day") - async getMostListenedDay(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getMostListenedDay(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return this.insightsService.getMostListenedDay(accessToken, refreshToken, providerName); } @Post("listening-over-time") - async getListeningOverTime(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getListeningOverTime(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { return this.validateAndCallService(body, this.insightsService.getListeningOverTime); } @Post("artists-vs-tracks") - async getArtistsVsTracks(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getArtistsVsTracks(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { return this.validateAndCallService(body, this.insightsService.getArtistsVsTracks); } @Post("recent-track-genres") - async getRecentTrackGenres(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) { + async getRecentTrackGenres(@Body() body: { accessToken: string; refreshToken: string; providerName: string }) + { return this.validateAndCallService(body, this.insightsService.getRecentTrackGenres); } - private validateAndCallService(body: any, serviceMethod: Function) { + private validateAndCallService(body: any, serviceMethod: Function) + { const { accessToken, refreshToken, providerName } = body; - if (!accessToken || !refreshToken || !providerName) { + if (!accessToken || !refreshToken || !providerName) + { throw new HttpException("Missing required parameters", HttpStatus.BAD_REQUEST); } return serviceMethod.call(this.insightsService, accessToken, refreshToken, providerName); diff --git a/Backend/src/insights/services/insights.service.ts b/Backend/src/insights/services/insights.service.ts index 9961e3c6..1a2c65bc 100644 --- a/Backend/src/insights/services/insights.service.ts +++ b/Backend/src/insights/services/insights.service.ts @@ -4,15 +4,20 @@ import { createSupabaseClient } from "../../supabase/services/supabaseClient"; import { SupabaseService } from "../../supabase/services/supabase.service"; @Injectable() -export class InsightsService { - constructor(private supabaseService: SupabaseService) {} +export class InsightsService +{ + constructor(private supabaseService: SupabaseService) + { + } // Helper to get user details from Supabase - private async getUserFromSupabase(accessToken: string): Promise { + private async getUserFromSupabase(accessToken: string): Promise + { const supabase = createSupabaseClient(); const { data, error } = await supabase.auth.getUser(accessToken); - if (error) { + if (error) + { throw new HttpException("Invalid access token", HttpStatus.UNAUTHORIZED); } @@ -24,68 +29,162 @@ export class InsightsService { accessToken: string, refreshToken: string, providerName: string - ): Promise { - await this.setSupabaseSession(accessToken, refreshToken); // Set session before retrieving tokens + ): Promise + { + await this.setSupabaseSession(accessToken, refreshToken); const userId = await this.getUserIdFromAccessToken(accessToken); const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") { + if (providerName === "spotify") + { return this.getTopMoodFromSpotify(providerToken); - } else if (providerName === "youtube") { + } + else if (providerName === "youtube") + { return this.getTopMoodFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getTopMoodFromSpotify(providerToken: string): Promise { + private async fetchSpotifyArtistsVsTracks(providerToken: string) + { + try + { + const artistResponse = await axios.get("https://api.spotify.com/v1/me/player/recently-played?limit=50", { + headers: { Authorization: `Bearer ${providerToken}` } + }); + const trackResponse = await axios.get("https://api.spotify.com/v1/me/player/recently-played?limit=50", { + headers: { Authorization: `Bearer ${providerToken}` } + }); + + const artistsData = artistResponse.data.items.map((item: any) => item.track.artists).flat(); + const tracksData = trackResponse.data.items.map((item: any) => item.track); + + return this.parseArtistsVsTracks(artistsData, tracksData); + } + catch (error) + { + if (error.response && error.response.status === 401) + { + throw new HttpException("Unauthorized access to Spotify API", HttpStatus.UNAUTHORIZED); + } + throw new HttpException("Spotify API error with tracks vs artists", HttpStatus.BAD_REQUEST); + } + } + + private async fetchSpotifyListeningOverTime(providerToken: string) + { + const url = "https://api.spotify.com/v1/me/player/recently-played"; + try + { + const response = await axios.get(url, { + headers: { Authorization: `Bearer ${providerToken}` } + }); + + if (!response.data || !response.data.items || response.data.items.length === 0) + { + return {}; // Return an empty object if data is empty + } + + return this.parseListeningOverTime(response.data.items); + } + catch (error) + { + throw new HttpException("Spotify API error listening over time", HttpStatus.BAD_REQUEST); + } + } + + async getTopMoodFromSpotify(providerToken: string): Promise + { const url = "https://api.spotify.com/v1/me/top/tracks?limit=10"; - try { + try + { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}`, - }, + Authorization: `Bearer ${providerToken}` + } }); const topTracks = response.data.items; - const genres = new Set( - topTracks.flatMap((track) => track.artists.map((artist) => artist.genres)) - ); + if (!topTracks || topTracks.length === 0) + { + return { mood: "Happy" }; // Default mood if no tracks are found + } - let mood = "Happy"; // Default mood + const genreCounts: { [key: string]: number } = {}; + + // Count occurrences of each genre + topTracks.forEach((track) => + { + track.artists.forEach((artist) => + { + if (artist.genres) + { + artist.genres.forEach((genre) => + { + if (genreCounts[genre]) + { + genreCounts[genre]++; + } + else + { + genreCounts[genre] = 1; + } + }); + } + }); + }); - if (genres.has("pop")) { - mood = "Energetic"; - } else if (genres.has("classical")) { - mood = "Relaxed"; - } else if (genres.has("rock")) { - mood = "Energetic"; - } + // Determine the top genre + const topGenre = Object.keys(genreCounts).reduce((a, b) => genreCounts[a] > genreCounts[b] ? a : b, ""); + + // Map genre to mood + const genreToMoodMap: { [key: string]: string } = { + "pop": "Energetic", + "classical": "Relaxed", + "rock": "Energetic", + "jazz": "Calm", + "blues": "Melancholic", + "hip hop": "Energetic", + "electronic": "Upbeat" + }; + + const mood = genreToMoodMap[topGenre] || "Happy"; return { mood }; - } catch (error) { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + catch (error) + { + throw new HttpException("Spotify API error top mood", HttpStatus.BAD_REQUEST); } } - async getTopMoodFromYouTube(providerToken: string): Promise { + async getTopMoodFromYouTube(providerToken: string): Promise + { const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try { + try + { const response = await axios.get(url); const likedVideos = response.data.items; - let mood = "Energetic"; // Default mood + let mood = "Energetic"; - if (likedVideos.some((video) => video.snippet.categoryId === "Music")) { + if (likedVideos.some((video) => video.snippet.categoryId === "Music")) + { mood = "Energetic"; - } else if (likedVideos.some((video) => video.snippet.categoryId === "Education")) { + } + else if (likedVideos.some((video) => video.snippet.categoryId === "Education")) + { mood = "Focused"; } return { mood }; - } catch (error) { + } + catch (error) + { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } @@ -95,50 +194,62 @@ export class InsightsService { accessToken: string, refreshToken: string, providerName: string - ): Promise { + ): Promise + { await this.setSupabaseSession(accessToken, refreshToken); const userId = await this.getUserIdFromAccessToken(accessToken); const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") { + if (providerName === "spotify") + { return this.getTotalListeningTimeFromSpotify(providerToken); - } else if (providerName === "youtube") { + } + else if (providerName === "youtube") + { return this.getTotalListeningTimeFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getTotalListeningTimeFromSpotify(providerToken: string): Promise { + async getTotalListeningTimeFromSpotify(providerToken: string): Promise + { const url = "https://api.spotify.com/v1/me/player/recently-played"; - try { + try + { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}`, - }, + Authorization: `Bearer ${providerToken}` + } }); const tracks = response.data.items; let totalListeningTime = 0; - tracks.forEach((track) => { + tracks.forEach((track) => + { totalListeningTime += track.track.duration_ms; }); const hours = totalListeningTime / (1000 * 60 * 60); return { totalListeningTime: `${hours.toFixed(2)} hours` }; - } catch (error) { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + catch (error) + { + throw new HttpException("Spotify API error total listening time", HttpStatus.BAD_REQUEST); } } - async getTotalListeningTimeFromYouTube(providerToken: string): Promise { + async getTotalListeningTimeFromYouTube(providerToken: string): Promise + { const url = `https://www.googleapis.com/youtube/v3/videos?part=contentDetails&myRating=like&access_token=${providerToken}`; - try { + try + { const response = await axios.get(url); const likedVideos = response.data.items; let totalListeningTime = 0; - likedVideos.forEach((video) => { + likedVideos.forEach((video) => + { const duration = video.contentDetails.duration; const time = this.parseYouTubeDuration(duration); totalListeningTime += time; @@ -146,7 +257,9 @@ export class InsightsService { const hours = totalListeningTime / (60 * 60); return { totalListeningTime: `${hours.toFixed(2)} hours` }; - } catch (error) { + } + catch (error) + { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } @@ -156,45 +269,57 @@ export class InsightsService { accessToken: string, refreshToken: string, providerName: string - ): Promise { + ): Promise + { await this.setSupabaseSession(accessToken, refreshToken); const userId = await this.getUserIdFromAccessToken(accessToken); const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") { + if (providerName === "spotify") + { return this.getMostPlayedTrackFromSpotify(providerToken); - } else if (providerName === "youtube") { + } + else if (providerName === "youtube") + { return this.getMostPlayedTrackFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getMostPlayedTrackFromSpotify(providerToken: string): Promise { + async getMostPlayedTrackFromSpotify(providerToken: string): Promise + { const url = "https://api.spotify.com/v1/me/top/tracks?limit=1"; - try { + try + { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}`, - }, + Authorization: `Bearer ${providerToken}` + } }); const track = response.data.items[0]; return { name: track.name, - artist: track.artists.map((artist) => artist.name).join(", "), + artist: track.artists.map((artist) => artist.name).join(", ") }; - } catch (error) { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + catch (error) + { + throw new HttpException("Spotify API error most played track", HttpStatus.BAD_REQUEST); } } - async getMostPlayedTrackFromYouTube(providerToken: string): Promise { + async getMostPlayedTrackFromYouTube(providerToken: string): Promise + { const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try { + try + { const response = await axios.get(url); const mostLikedVideo = response.data.items[0]; return { name: mostLikedVideo.snippet.title }; - } catch (error) { + } + catch (error) + { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } @@ -204,48 +329,61 @@ export class InsightsService { accessToken: string, refreshToken: string, providerName: string - ): Promise { + ): Promise + { await this.setSupabaseSession(accessToken, refreshToken); const userId = await this.getUserIdFromAccessToken(accessToken); const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") { + if (providerName === "spotify") + { return this.getMostListenedArtistFromSpotify(providerToken); - } else if (providerName === "youtube") { + } + else if (providerName === "youtube") + { return this.getMostListenedArtistFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getMostListenedArtistFromSpotify(providerToken: string): Promise { + async getMostListenedArtistFromSpotify(providerToken: string): Promise + { const url = "https://api.spotify.com/v1/me/top/artists?limit=1"; - try { + try + { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}`, - }, + Authorization: `Bearer ${providerToken}` + } }); const artist = response.data.items[0]; return { artist: artist.name }; - } catch (error) { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + catch (error) + { + throw new HttpException("Spotify API error listened artist", HttpStatus.BAD_REQUEST); } } - async getMostListenedArtistFromYouTube(providerToken: string): Promise { + async getMostListenedArtistFromYouTube(providerToken: string): Promise + { const url = `https://www.googleapis.com/youtube/v3/channels?part=snippet&mine=true&access_token=${providerToken}`; - try { + try + { const response = await axios.get(url); const channel = response.data.items[0]; return { artist: channel.snippet.title }; - } catch (error) { + } + catch (error) + { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } // Helper to parse YouTube ISO 8601 duration - parseYouTubeDuration(duration: string): number { + parseYouTubeDuration(duration: string): number + { const regex = /PT(\d+H)?(\d+M)?(\d+S)?/; const matches = regex.exec(duration); let totalSeconds = 0; @@ -258,63 +396,85 @@ export class InsightsService { } // Get top genre from Spotify or YouTube - async getTopGenre( - accessToken: string, - refreshToken: string, - providerName: string - ): Promise { + + + async getTopGenre(accessToken: string, refreshToken: string, providerName: string): Promise + { await this.setSupabaseSession(accessToken, refreshToken); const userId = await this.getUserIdFromAccessToken(accessToken); const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") { + if (providerName === "spotify") + { return this.getTopGenreFromSpotify(providerToken); - } else if (providerName === "youtube") { + } + else if (providerName === "youtube") + { return this.getTopGenreFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getTopGenreFromSpotify(providerToken: string): Promise { - const url = "https://api.spotify.com/v1/me/top/tracks"; - try { + async getTopGenreFromSpotify(providerToken: string): Promise + { + const url = "https://api.spotify.com/v1/me/top/tracks?limit=20"; + try + { const response = await axios.get(url, { - headers: { - Authorization: `Bearer ${providerToken}`, - }, + headers: { Authorization: `Bearer ${providerToken}` } }); + + if (!response.data || !response.data.items) + { + throw new HttpException("Invalid response from Spotify API", HttpStatus.BAD_REQUEST); + } + const topTracks = response.data.items; const genres = {}; // Accumulate genres from artists - topTracks.forEach((track) => { - track.artists.forEach((artist) => { - artist.genres.forEach((genre) => { - if (!genres[genre]) { - genres[genre] = 0; - } - genres[genre]++; - }); + topTracks.forEach((track) => + { + track.artists.forEach((artist) => + { + if (artist.genres) + { + artist.genres.forEach((genre) => + { + if (!genres[genre]) + { + genres[genre] = 0; + } + genres[genre]++; + }); + } }); }); // Find the top genre const topGenre = Object.keys(genres).reduce((a, b) => (genres[a] > genres[b] ? a : b)); return { topGenre }; - } catch (error) { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + catch (error) + { + console.error("Spotify API error:", error.message); + throw new HttpException("Spotify API error with top genre", HttpStatus.BAD_REQUEST); } } - async getTopGenreFromYouTube(providerToken: string): Promise { + async getTopGenreFromYouTube(providerToken: string): Promise + { const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try { + try + { const response = await axios.get(url); const likedVideos = response.data.items; - const genre = "Pop"; // You'll need to implement logic to determine genre from YouTube data + const genre = "Pop"; return { topGenre: genre }; - } catch (error) { + } + catch (error) + { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } @@ -324,32 +484,39 @@ export class InsightsService { accessToken: string, refreshToken: string, providerName: string - ): Promise { + ): Promise + { await this.setSupabaseSession(accessToken, refreshToken); const userId = await this.getUserIdFromAccessToken(accessToken); const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") { + if (providerName === "spotify") + { return this.getAverageSongDurationFromSpotify(providerToken); - } else if (providerName === "youtube") { + } + else if (providerName === "youtube") + { return this.getAverageSongDurationFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getAverageSongDurationFromSpotify(providerToken: string): Promise { + async getAverageSongDurationFromSpotify(providerToken: string): Promise + { const url = "https://api.spotify.com/v1/me/player/recently-played"; - try { + try + { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}`, - }, + Authorization: `Bearer ${providerToken}` + } }); const tracks = response.data.items; let totalDuration = 0; - tracks.forEach((track) => { + tracks.forEach((track) => + { totalDuration += track.track.duration_ms; }); @@ -357,19 +524,24 @@ export class InsightsService { const minutes = Math.floor(averageDuration / 60000); const seconds = Math.floor((averageDuration % 60000) / 1000); return { averageDuration: `${minutes}:${seconds < 10 ? "0" : ""}${seconds}` }; - } catch (error) { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + catch (error) + { + throw new HttpException("Spotify API error with duration", HttpStatus.BAD_REQUEST); } } - async getAverageSongDurationFromYouTube(providerToken: string): Promise { + async getAverageSongDurationFromYouTube(providerToken: string): Promise + { const url = `https://www.googleapis.com/youtube/v3/videos?part=contentDetails&myRating=like&access_token=${providerToken}`; - try { + try + { const response = await axios.get(url); const likedVideos = response.data.items; let totalDuration = 0; - likedVideos.forEach((video) => { + likedVideos.forEach((video) => + { const duration = video.contentDetails.duration; const time = this.parseYouTubeDuration(duration); totalDuration += time; @@ -379,7 +551,9 @@ export class InsightsService { const minutes = Math.floor(averageDuration / 60); const seconds = Math.floor(averageDuration % 60); return { averageDuration: `${minutes}:${seconds < 10 ? "0" : ""}${seconds}` }; - } catch (error) { + } + catch (error) + { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } @@ -389,33 +563,40 @@ export class InsightsService { accessToken: string, refreshToken: string, providerName: string - ): Promise { + ): Promise + { await this.setSupabaseSession(accessToken, refreshToken); const userId = await this.getUserIdFromAccessToken(accessToken); const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") { + if (providerName === "spotify") + { return this.getMostActiveDayFromSpotify(providerToken); - } else if (providerName === "youtube") { + } + else if (providerName === "youtube") + { return this.getMostActiveDayFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getMostActiveDayFromSpotify(providerToken: string): Promise { + async getMostActiveDayFromSpotify(providerToken: string): Promise + { const url = "https://api.spotify.com/v1/me/player/recently-played"; - try { + try + { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}`, - }, + Authorization: `Bearer ${providerToken}` + } }); const tracks = response.data.items; const days = {}; // Count tracks per day - tracks.forEach((track) => { + tracks.forEach((track) => + { const date = new Date(track.played_at).toLocaleDateString("en-US", { weekday: "long" }); days[date] = (days[date] || 0) + 1; }); @@ -423,30 +604,35 @@ export class InsightsService { // Find the day with the most plays const mostActiveDay = Object.keys(days).reduce((a, b) => (days[a] > days[b] ? a : b)); return { mostActiveDay }; - } catch (error) { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + catch (error) + { + throw new HttpException("Spotify API error most active day", HttpStatus.BAD_REQUEST); } } - async getMostActiveDayFromYouTube(providerToken: string): Promise { + async getMostActiveDayFromYouTube(providerToken: string): Promise + { const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try { + try + { const response = await axios.get(url); const likedVideos = response.data.items; const days = {}; - // Count videos per day - likedVideos.forEach((video) => { + likedVideos.forEach((video) => + { const date = new Date(video.snippet.publishedAt).toLocaleDateString("en-US", { - weekday: "long", + weekday: "long" }); days[date] = (days[date] || 0) + 1; }); - // Find the day with the most plays const mostActiveDay = Object.keys(days).reduce((a, b) => (days[a] > days[b] ? a : b)); return { mostActiveDay }; - } catch (error) { + } + catch (error) + { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } @@ -456,44 +642,55 @@ export class InsightsService { accessToken: string, refreshToken: string, providerName: string - ): Promise { + ): Promise + { await this.setSupabaseSession(accessToken, refreshToken); const userId = await this.getUserIdFromAccessToken(accessToken); const { providerToken } = await this.supabaseService.retrieveTokens(userId); - if (providerName === "spotify") { + if (providerName === "spotify") + { return this.getUniqueArtistsFromSpotify(providerToken); - } else if (providerName === "youtube") { + } + else if (providerName === "youtube") + { return this.getUniqueArtistsFromYouTube(providerToken); } throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - async getUniqueArtistsFromSpotify(providerToken: string): Promise { + async getUniqueArtistsFromSpotify(providerToken: string): Promise + { const url = "https://api.spotify.com/v1/me/player/recently-played"; - try { + try + { const response = await axios.get(url, { headers: { - Authorization: `Bearer ${providerToken}`, - }, + Authorization: `Bearer ${providerToken}` + } }); const tracks = response.data.items; const uniqueArtists = new Set(); - tracks.forEach((track) => { + tracks.forEach((track) => + { track.track.artists.forEach((artist) => uniqueArtists.add(artist.name)); }); return { uniqueArtists: Array.from(uniqueArtists) }; - } catch (error) { - throw new HttpException("Spotify API error", HttpStatus.BAD_REQUEST); + } + catch (error) + { + throw new HttpException("Spotify API error unique artists", HttpStatus.BAD_REQUEST); } } - async getUniqueArtistsFromYouTube(providerToken: string): Promise { + async getUniqueArtistsFromYouTube(providerToken: string): Promise + { const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&myRating=like&access_token=${providerToken}`; - try { + try + { const response = await axios.get(url); const likedVideos = response.data.items; const uniqueChannels = new Set(); @@ -501,279 +698,351 @@ export class InsightsService { likedVideos.forEach((video) => uniqueChannels.add(video.snippet.channelTitle)); return { uniqueArtists: Array.from(uniqueChannels) }; - } catch (error) { + } + catch (error) + { throw new HttpException("YouTube API error", HttpStatus.BAD_REQUEST); } } - private async getUserIdFromAccessToken(accessToken: string): Promise { + private async getUserIdFromAccessToken(accessToken: string): Promise + { const supabase = createSupabaseClient(); const { data, error } = await supabase.auth.getUser(accessToken); - if (error || !data || !data.user) { + if (error || !data || !data.user) + { throw new HttpException("Invalid access token", HttpStatus.UNAUTHORIZED); } return data.user.id; } - private async setSupabaseSession(accessToken: string, refreshToken: string): Promise { + private async setSupabaseSession(accessToken: string, refreshToken: string): Promise + { const supabase = createSupabaseClient(); const { error } = await supabase.auth.setSession({ access_token: accessToken, - refresh_token: refreshToken, + refresh_token: refreshToken }); - if (error) { + if (error) + { console.error("Error setting Supabase session:", error); throw new HttpException("Failed to set Supabase session", HttpStatus.INTERNAL_SERVER_ERROR); } } - async getListeningTrends(accessToken: string, refreshToken: string, providerName: string) { - if (providerName === 'spotify') { + async getListeningTrends(accessToken: string, refreshToken: string, providerName: string) + { + if (providerName === "spotify") + { return await this.getSpotifyListeningTrends(accessToken); - } else if (providerName === 'youtube') { + } + else if (providerName === "youtube") + { return await this.getYoutubeListeningTrends(accessToken); - } else { + } + else + { throw new HttpException("Unsupported provider", HttpStatus.BAD_REQUEST); } } // Method to get weekly playlist - async getWeeklyPlaylist(accessToken: string, refreshToken: string, providerName: string) { - if (providerName === 'spotify') { + async getWeeklyPlaylist(accessToken: string, refreshToken: string, providerName: string) + { + if (providerName === "spotify") + { return await this.getSpotifyWeeklyPlaylist(accessToken); - } else if (providerName === 'youtube') { + } + else if (providerName === "youtube") + { return await this.getYoutubeWeeklyPlaylist(accessToken); - } else { + } + else + { throw new HttpException("Unsupported provider", HttpStatus.BAD_REQUEST); } } // Method to get the most listened day - async getMostListenedDay(accessToken: string, refreshToken: string, providerName: string) { - if (providerName === 'spotify') { + async getMostListenedDay(accessToken: string, refreshToken: string, providerName: string) + { + if (providerName === "spotify") + { return await this.getSpotifyMostListenedDay(accessToken); - } else if (providerName === 'youtube') { + } + else if (providerName === "youtube") + { return await this.getYoutubeMostListenedDay(accessToken); - } else { + } + else + { throw new HttpException("Unsupported provider", HttpStatus.BAD_REQUEST); } } // Spotify-specific method to get listening trends - private async getSpotifyListeningTrends(accessToken: string) { - const response = await axios.get('https://api.spotify.com/v1/me/top/tracks', { - headers: { Authorization: `Bearer ${accessToken}` }, + private async getSpotifyListeningTrends(accessToken: string) + { + const response = await axios.get("https://api.spotify.com/v1/me/top/tracks", { + headers: { Authorization: `Bearer ${accessToken}` } }); return response.data; } // YouTube-specific method to get listening trends - private async getYoutubeListeningTrends(accessToken: string) { + private async getYoutubeListeningTrends(accessToken: string) + { // Replace with the actual endpoint and logic to fetch YouTube trends // This is a placeholder example - const response = await axios.get('https://www.googleapis.com/youtube/v3/videos', { + const response = await axios.get("https://www.googleapis.com/youtube/v3/videos", { headers: { Authorization: `Bearer ${accessToken}` }, params: { - part: 'snippet,contentDetails', - chart: 'mostPopular', - regionCode: 'US', - }, + part: "snippet,contentDetails", + chart: "mostPopular", + regionCode: "US" + } }); return response.data; } // Spotify-specific method to get the weekly playlist - private async getSpotifyWeeklyPlaylist(accessToken: string) { - const response = await axios.get('https://api.spotify.com/v1/me/top/artists', { - headers: { Authorization: `Bearer ${accessToken}` }, + private async getSpotifyWeeklyPlaylist(accessToken: string) + { + const response = await axios.get("https://api.spotify.com/v1/me/top/artists", { + headers: { Authorization: `Bearer ${accessToken}` } }); return response.data; } // YouTube-specific method to get the weekly playlist - private async getYoutubeWeeklyPlaylist(accessToken: string) { - // Replace with the actual endpoint and logic to fetch YouTube playlists - const response = await axios.get('https://www.googleapis.com/youtube/v3/playlists', { + private async getYoutubeWeeklyPlaylist(accessToken: string) + { + const response = await axios.get("https://www.googleapis.com/youtube/v3/playlists", { headers: { Authorization: `Bearer ${accessToken}` }, params: { - part: 'snippet', - maxResults: 10, - }, + part: "snippet", + maxResults: 10 + } }); return response.data; } // Spotify-specific method to get the most listened day - private async getSpotifyMostListenedDay(accessToken: string) { - const response = await axios.get('https://api.spotify.com/v1/me/top/artists', { - headers: { Authorization: `Bearer ${accessToken}` }, + private async getSpotifyMostListenedDay(accessToken: string) + { + const response = await axios.get("https://api.spotify.com/v1/me/top/artists", { + headers: { Authorization: `Bearer ${accessToken}` } }); - return response.data; // Modify according to the actual API response structure + return response.data; } // YouTube-specific method to get the most listened day - private async getYoutubeMostListenedDay(accessToken: string) { - // Replace with the actual logic to determine the most listened day on YouTube - // This is a placeholder example - const response = await axios.get('https://www.googleapis.com/youtube/v3/videos', { + private async getYoutubeMostListenedDay(accessToken: string) + { + const response = await axios.get("https://www.googleapis.com/youtube/v3/videos", { headers: { Authorization: `Bearer ${accessToken}` }, params: { - part: 'snippet', - chart: 'mostPopular', - }, + part: "snippet", + chart: "mostPopular" + } }); - return response.data; // Modify according to the actual API response structure + return response.data; } - async getListeningOverTime(accessToken: string, refreshToken: string, providerName: string) { - if (providerName === "spotify") { - return this.fetchSpotifyListeningOverTime(accessToken, refreshToken); - } else if (providerName === "youtube") { - return this.fetchYouTubeListeningOverTime(accessToken, refreshToken); + async getListeningOverTime(accessToken: string, refreshToken: string, providerName: string) + { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); + + if (providerName === "spotify") + { + return this.fetchSpotifyListeningOverTime(providerToken); + } + else if (providerName === "youtube") + { + return this.fetchYouTubeListeningOverTime(providerToken); } + throw new HttpException("Invalid provider name", HttpStatus.BAD_REQUEST); } // Fetch comparison of distinct artists vs tracks for Spotify or YouTube - async getArtistsVsTracks(accessToken: string, refreshToken: string, providerName: string) { - if (providerName === "spotify") { - return this.fetchSpotifyArtistsVsTracks(accessToken, refreshToken); - } else if (providerName === "youtube") { - return this.fetchYouTubeArtistsVsTracks(accessToken, refreshToken); + async getArtistsVsTracks(accessToken: string, refreshToken: string, providerName: string) + { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); + + if (!providerToken) + { + throw new HttpException("Provider token not found", HttpStatus.UNAUTHORIZED); } - throw new HttpException("Invalid provider name", HttpStatus.BAD_REQUEST); - } - // Fetch recent track genres for Spotify or YouTube - async getRecentTrackGenres(accessToken: string, refreshToken: string, providerName: string) { - if (providerName === "spotify") { - return this.fetchSpotifyRecentTrackGenres(accessToken, refreshToken); - } else if (providerName === "youtube") { - return this.fetchYouTubeRecentTrackGenres(accessToken, refreshToken); + if (providerName === "spotify") + { + return this.fetchSpotifyArtistsVsTracks(providerToken); } - throw new HttpException("Invalid provider name", HttpStatus.BAD_REQUEST); + else if (providerName === "youtube") + { + return this.fetchYouTubeArtistsVsTracks(providerToken); + } + + throw new HttpException("Invalid provider", HttpStatus.BAD_REQUEST); } - // ====== Spotify Methods ====== + // Fetch recent track genres for Spotify or YouTube + async getRecentTrackGenres(accessToken: string, refreshToken: string, providerName: string) + { + await this.setSupabaseSession(accessToken, refreshToken); + const userId = await this.getUserIdFromAccessToken(accessToken); + const { providerToken } = await this.supabaseService.retrieveTokens(userId); + + if (providerName === "spotify") + { + return this.fetchSpotifyRecentTrackGenres(providerToken); + } + else if (providerName === "youtube") + { + return this.fetchYouTubeRecentTrackGenres(providerToken); + } - // Fetch listening over time from Spotify - private async fetchSpotifyListeningOverTime(accessToken: string, refreshToken: string) { - const response = await axios.get('https://api.spotify.com/v1/me/player/recently-played', { - headers: { Authorization: `Bearer ${accessToken}` } - }); - return this.parseListeningOverTime(response.data.items); + throw new HttpException("Invalid provider name", HttpStatus.BAD_REQUEST); } - // Fetch distinct artists and tracks from Spotify - private async fetchSpotifyArtistsVsTracks(accessToken: string, refreshToken: string) { - const artistResponse = await axios.get('https://api.spotify.com/v1/me/top/artists', { - headers: { Authorization: `Bearer ${accessToken}` } - }); - const trackResponse = await axios.get('https://api.spotify.com/v1/me/top/tracks', { - headers: { Authorization: `Bearer ${accessToken}` } - }); - return this.parseArtistsVsTracks(artistResponse.data.items, trackResponse.data.items); + private async fetchSpotifyRecentTrackGenres(providerToken: string) + { + try + { + const response = await axios.get("https://api.spotify.com/v1/me/top/tracks", { + headers: { Authorization: `Bearer ${providerToken}` } + }); + return this.parseRecentTrackGenres(response.data.items); + } + catch (error) + { + if (error.response && error.response.status === 401) + { + throw new HttpException("Unauthorized access to Spotify API", HttpStatus.UNAUTHORIZED); + } + throw new HttpException("Spotify API error with recent genres", HttpStatus.BAD_REQUEST); + } } - // Fetch recent track genres from Spotify - private async fetchSpotifyRecentTrackGenres(accessToken: string, refreshToken: string) { - const response = await axios.get('https://api.spotify.com/v1/me/top/tracks', { - headers: { Authorization: `Bearer ${accessToken}` } + private async fetchYouTubeRecentTrackGenres(providerToken: string) + { + const response = await axios.get(`https://www.googleapis.com/youtube/v3/videos`, { + headers: { Authorization: `Bearer ${providerToken}` }, + params: { + part: "snippet,contentDetails,statistics", + myRating: "like" + } }); return this.parseRecentTrackGenres(response.data.items); } - // ====== YouTube Methods ====== - // Fetch listening over time from YouTube - private async fetchYouTubeListeningOverTime(accessToken: string, refreshToken: string) { + private async fetchYouTubeListeningOverTime(providerToken: string) + { const response = await axios.get(`https://www.googleapis.com/youtube/v3/videos`, { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${providerToken}` }, params: { - part: 'snippet,contentDetails,statistics', - myRating: 'like' // Example of fetching videos liked by user + part: "snippet,contentDetails,statistics", + myRating: "like" } }); return this.parseListeningOverTime(response.data.items); } // Fetch distinct artists and tracks from YouTube - private async fetchYouTubeArtistsVsTracks(accessToken: string, refreshToken: string) { + private async fetchYouTubeArtistsVsTracks(providerToken: string) + { const playlistResponse = await axios.get(`https://www.googleapis.com/youtube/v3/playlists`, { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${providerToken}` }, params: { - part: 'snippet', + part: "snippet", mine: true } }); - // YouTube API doesn't have the same concept of "artists" as Spotify, - // so we'll assume each playlist corresponds to an artist. const trackResponse = await axios.get(`https://www.googleapis.com/youtube/v3/playlistItems`, { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${providerToken}` }, params: { - part: 'snippet', + part: "snippet", playlistId: playlistResponse.data.items[0].id } }); return this.parseArtistsVsTracks(playlistResponse.data.items, trackResponse.data.items); } - // Fetch recent track genres from YouTube - private async fetchYouTubeRecentTrackGenres(accessToken: string, refreshToken: string) { - const response = await axios.get(`https://www.googleapis.com/youtube/v3/videos`, { - headers: { Authorization: `Bearer ${accessToken}` }, - params: { - part: 'snippet,contentDetails,statistics', - myRating: 'like' - } - }); - return this.parseRecentTrackGenres(response.data.items); - } - - // ====== Parsing Methods ====== - // Parse listening data over time - private parseListeningOverTime(data: any) { - const result = {}; - data.forEach((item: any) => { + private parseListeningOverTime(data: any) + { + if (!data || data.length === 0) + { + return {}; + } + + const result = data.reduce((acc: any, item: any) => + { const date = new Date(item.played_at || item.snippet.publishedAt).toDateString(); - if (!result[date]) { - result[date] = 1; - } else { - result[date]++; + if (!acc[date]) + { + acc[date] = 1; } - }); - return result; // Format suitable for graphing: { 'Date': playCount } + else + { + acc[date]++; + } + return acc; + }, {}); + + return result; } // Parse artists vs tracks data - private parseArtistsVsTracks(artistsData: any, tracksData: any) { + private parseArtistsVsTracks(artistsData: any, tracksData: any) + { const distinctArtists = new Set(); - artistsData.forEach((artist: any) => distinctArtists.add(artist.name)); - const distinctTracks = new Set(); - tracksData.forEach((track: any) => distinctTracks.add(track.name)); + + artistsData.forEach((artist: any) => + { + if (!distinctArtists.has(artist.name)) + { + distinctArtists.add(artist.name); + } + }); + + tracksData.forEach((track: any) => + { + distinctTracks.add(track.name); + }); return { distinctArtists: distinctArtists.size, distinctTracks: distinctTracks.size - }; // Format suitable for graphing: { distinctArtists, distinctTracks } + }; } // Parse recent track genres data - private parseRecentTrackGenres(tracksData: any) { + private parseRecentTrackGenres(tracksData: any) + { const genres = {}; - tracksData.forEach((track: any) => { - const genre = track.album?.genres?.[0] || "Unknown"; // Assuming genre from album data - if (!genres[genre]) { + tracksData.forEach((track: any) => + { + const genre = track.album?.genres?.[0] || "Unknown"; + if (!genres[genre]) + { genres[genre] = 1; - } else { + } + else + { genres[genre]++; } }); - return genres; // Format suitable for graphing: { 'Genre': count } + return genres; } } \ No newline at end of file From 086d820d70fe953879e1fe61dad6ab428865af88 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Sun, 20 Oct 2024 13:08:56 +0200 Subject: [PATCH 07/10] :triangular_ruler: Refactored frontend insights service to match format returned by backend --- Frontend/src/app/services/insights.service.ts | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Frontend/src/app/services/insights.service.ts b/Frontend/src/app/services/insights.service.ts index 5daf485e..34b3c7da 100644 --- a/Frontend/src/app/services/insights.service.ts +++ b/Frontend/src/app/services/insights.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from "@angular/common/http"; import { Observable, of, throwError } from "rxjs"; import { TokenService } from "./token.service"; import { ProviderService } from "./provider.service"; +import { CacheService } from "./cache.service"; import { environment } from "../../environments/environment"; import { catchError, switchMap } from "rxjs/operators"; @@ -15,7 +16,8 @@ export class InsightsService { constructor( private http: HttpClient, private tokenService: TokenService, - private providerService: ProviderService + private providerService: ProviderService, + private cacheService: CacheService ) {} private getParams(): Observable<{ accessToken: string; refreshToken: string; providerName: string }> { @@ -43,13 +45,23 @@ export class InsightsService { private makeRequest(endpoint: string): Observable { return this.getParams().pipe( - switchMap(({ accessToken, refreshToken, providerName }) => - this.http.post(`${this.apiUrl}/${endpoint}`, { + switchMap(({ accessToken, refreshToken, providerName }) => { + const cacheKey = `${endpoint}-${accessToken}-${providerName}`; + if (this.cacheService.has(cacheKey)) { + return of(this.cacheService.get(cacheKey)); + } + + return this.http.post(`${this.apiUrl}/${endpoint}`, { accessToken, refreshToken, providerName, - }) - ), + }).pipe( + switchMap(response => { + this.cacheService.set(cacheKey, response); + return of(response); + }) + ); + }), catchError((error) => { console.error(`Error fetching ${endpoint}:`, error); return of(null); @@ -88,4 +100,16 @@ export class InsightsService { getUniqueArtistsListened(): Observable { return this.makeRequest("unique-artists-listened"); } + + getListeningOverTime(): Observable { + return this.makeRequest("listening-over-time"); + } + + getArtistsVsTracks(): Observable { + return this.makeRequest("artists-vs-tracks"); + } + + getRecentTrackGenres(): Observable { + return this.makeRequest("recent-track-genres"); + } } From 1fc40d4cf37055b5b62fc2f4ec9cc0a094f5ea00 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Sun, 20 Oct 2024 13:09:47 +0200 Subject: [PATCH 08/10] :triangular_ruler: Updated graphs on insights page --- .../pages/insights/insights.component.html | 26 +- .../app/pages/insights/insights.component.ts | 351 +++++++++++------- 2 files changed, 222 insertions(+), 155 deletions(-) diff --git a/Frontend/src/app/pages/insights/insights.component.html b/Frontend/src/app/pages/insights/insights.component.html index e69e5ed2..bf4d7ba1 100644 --- a/Frontend/src/app/pages/insights/insights.component.html +++ b/Frontend/src/app/pages/insights/insights.component.html @@ -35,23 +35,21 @@

Unique Artists Listened

- -
-

Mood Distribution

- + +
+

Listening Over Time

+
- - -
-

Listening by Service

- + +
+

Distinct Artists vs Distinct Tracks

+
- -
-

Top Genres

- + +
+

Recent Track Genres

+
-
diff --git a/Frontend/src/app/pages/insights/insights.component.ts b/Frontend/src/app/pages/insights/insights.component.ts index 99fc95bc..b1734eb1 100644 --- a/Frontend/src/app/pages/insights/insights.component.ts +++ b/Frontend/src/app/pages/insights/insights.component.ts @@ -1,24 +1,17 @@ -import { AfterViewInit, AfterViewChecked, Component, Inject, PLATFORM_ID, Input, ElementRef, ViewChild } from "@angular/core"; -import { isPlatformBrowser } from "@angular/common"; -import Chart, { ChartType } from "chart.js/auto"; -import { MoodService } from '../../services/mood-service.service'; -import { NgClass, NgIf } from '@angular/common'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Chart, registerables } from 'chart.js'; import { InsightsService } from "../../services/insights.service"; @Component({ selector: "app-insights", - standalone: true, - imports: [NgClass, NgIf], templateUrl: "./insights.component.html", + standalone: true, styleUrls: ["./insights.component.css"] }) -export class InsightsComponent implements AfterViewInit, AfterViewChecked { - @Input() percentageData: number[] = [25, 5, 30, 40, 10, 15, 20, 25, 30, 10, 15, 5, 20, 5, 5, 15, 10, 10, 25, 10, 20, 15, 10, 5, 20, 15]; - public chart: any; - public chartTypes: ChartType[] = ["pie", "bar", "line", "doughnut", "radar", "polarArea"]; - public currentChartIndex: number = 0; - public moodComponentClasses!: { [key: string]: string }; - private chartInitialized: boolean = false; +export class InsightsComponent implements OnInit, OnDestroy { + listeningOverTime: any = {}; + artistsVsTracks: any = {}; + recentTrackGenres: any = {}; topMood: string = ''; totalListeningTime: string = ''; @@ -29,168 +22,244 @@ export class InsightsComponent implements AfterViewInit, AfterViewChecked { mostActiveDay: string = ''; uniqueArtistsListened: number = 0; - // ViewChild sections for smooth scrolling - @ViewChild('widgets', { static: false }) widgetsSection!: ElementRef; - @ViewChild('moodChart', { static: false }) moodChartSection!: ElementRef; - @ViewChild('serviceChart', { static: false }) serviceChartSection!: ElementRef; - @ViewChild('genreChart', { static: false }) genreChartSection!: ElementRef; - - constructor( - @Inject(PLATFORM_ID) private platformId: Object, - public moodService: MoodService, - private insightsService: InsightsService - ) { - this.moodComponentClasses = { - 'Joy': 'bg-yellow-400 text-black', - 'Sadness': 'bg-blue-400 text-white', - 'Anger': 'bg-red-400 text-white', - 'Love': 'bg-pink-400 text-white', - 'Fear': 'bg-gray-400 text-white', - 'Optimism': 'bg-green-400 text-white' - }; - - // Fetch insights data from backend - this.fetchInsights(); - } + private artistsVsTracksChartInstance: Chart | null = null; - ngAfterViewInit() { - this.chartInitialized = false; - } - - ngAfterViewChecked() { - if (isPlatformBrowser(this.platformId) && !this.chartInitialized) { - this.initializeCharts(); - } + constructor(private insightsService: InsightsService) { + Chart.register(...registerables); } - initializeCharts() { - this.createChart('MoodChart', this.chartTypes[this.currentChartIndex], this.getMoodData()); - this.createChart('ServiceChart', 'doughnut', this.getServiceDistributionData()); - this.createChart('GenreChart', 'bar', this.getGenreData()); - this.chartInitialized = true; - } - - createChart(chartId: string, type: ChartType, chartData: any) { - const chartCanvas = document.getElementById(chartId) as HTMLCanvasElement; - if (chartCanvas) { - const existingChart = Chart.getChart(chartId); - if (existingChart) existingChart.destroy(); - - new Chart(chartCanvas, { - type: type, - data: chartData, - options: { - aspectRatio: 2.5, - responsive: true, - plugins: { - legend: { display: true, position: 'bottom' }, - tooltip: { enabled: true } - } - } - }); - } - } - - nextChartType() { - this.currentChartIndex = (this.currentChartIndex + 1) % this.chartTypes.length; - this.createChart('MoodChart', this.chartTypes[this.currentChartIndex], this.getMoodData()); - } + ngOnInit(): void { + this.fetchInsights(); - scrollToSection(section: string) { - let targetSection: ElementRef | undefined; - switch (section) { - case 'widgets': - targetSection = this.widgetsSection; - break; - case 'moodChart': - targetSection = this.moodChartSection; - break; - case 'serviceChart': - targetSection = this.serviceChartSection; - break; - case 'genreChart': - targetSection = this.genreChartSection; - break; - } + this.insightsService.getListeningOverTime().subscribe((data: any) => { + this.listeningOverTime = data || {}; + this.createListeningOverTimeChart(); + }); - if (targetSection) { - targetSection.nativeElement.scrollIntoView({ behavior: 'smooth' }); - } - } + this.insightsService.getArtistsVsTracks().subscribe((data: any) => { + this.artistsVsTracks = data || {}; + this.createArtistsVsTracksChart(); + }); - getMoodData() { - return { - labels: [ - "Joy", "Sadness", "Anger", "Disgust", "Fear", "Surprise", "Love", "Optimism", "Pride", "Relief" - ], - datasets: [{ - label: 'Mood Distribution', - data: [30, 10, 5, 3, 7, 8, 25, 5, 2, 5], // Replace with actual mood data if available - backgroundColor: [ - '#facc15', '#94a3b8', '#ef4444', '#a3e635', '#3b82f6', '#eab308', - '#ec4899', '#10b981', '#fb923c', '#6b7280' - ], - hoverOffset: 4 - }] - }; - } + this.insightsService.getRecentTrackGenres().subscribe((data: any) => { + this.recentTrackGenres = data || {}; + this.createRecentTrackGenresChart(); + }); - getServiceDistributionData() { - return { - labels: ['Spotify', 'YouTube'], - datasets: [{ - label: 'Listening Distribution', - data: [70, 30], // Replace with actual service distribution data if available - backgroundColor: ['#1DB954', '#FF0000'], - }] - }; + this.fetchArtistsVsTracksData(); } - getGenreData() { - return { - labels: ['Pop', 'Rock', 'Hip-Hop', 'Electronic', 'Jazz', 'Classical', 'Indie', 'R&B'], - datasets: [{ - label: 'Top Genres', - data: [35, 20, 15, 10, 5, 5, 7, 3], // Replace with actual genre data if available - backgroundColor: [ - '#f43f5e', '#3b82f6', '#22c55e', '#facc15', '#6366f1', '#8b5cf6', '#f59e0b', '#10b981' - ] - }] - }; + ngOnDestroy(): void { + if (this.artistsVsTracksChartInstance) { + this.artistsVsTracksChartInstance.destroy(); + } } fetchInsights(): void { - this.insightsService.getTopMood().subscribe(data => { + this.insightsService.getTopMood().subscribe((data: any) => { this.topMood = data?.mood || 'N/A'; }); - this.insightsService.getTotalListeningTime().subscribe(data => { + this.insightsService.getTotalListeningTime().subscribe((data: any) => { this.totalListeningTime = data?.totalListeningTime || 'N/A'; }); - this.insightsService.getMostListenedArtist().subscribe(data => { + this.insightsService.getMostListenedArtist().subscribe((data: any) => { this.mostListenedArtist = data?.artist || 'N/A'; }); - this.insightsService.getMostPlayedTrack().subscribe(data => { + this.insightsService.getMostPlayedTrack().subscribe((data: any) => { this.mostPlayedTrack = data?.name || 'N/A'; }); - this.insightsService.getTopGenre().subscribe(data => { + this.insightsService.getTopGenre().subscribe((data: any) => { this.topGenre = data?.topGenre || 'N/A'; }); - this.insightsService.getAverageSongDuration().subscribe(data => { + this.insightsService.getAverageSongDuration().subscribe((data: any) => { this.averageSongDuration = data?.averageDuration || 'N/A'; }); - this.insightsService.getMostActiveDay().subscribe(data => { + this.insightsService.getMostActiveDay().subscribe((data: any) => { this.mostActiveDay = data?.mostActiveDay || 'N/A'; }); - this.insightsService.getUniqueArtistsListened().subscribe(data => { + this.insightsService.getUniqueArtistsListened().subscribe((data: any) => { this.uniqueArtistsListened = data?.uniqueArtists?.length || 0; }); } + createListeningOverTimeChart() { + new Chart('listeningOverTimeChart', { + type: 'line', + data: { + labels: Object.keys(this.listeningOverTime), + datasets: [{ + label: 'Listening Over Time', + data: Object.values(this.listeningOverTime), + borderColor: 'rgba(75, 192, 192, 1)', + borderWidth: 1, + fill: false, + tension: 0.1 + }] + }, + options: { + scales: { + x: { + title: { + display: true, + text: 'Time' + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: 'Listening Time (minutes)' + } + } + }, + plugins: { + legend: { + display: true, + position: 'top' + }, + tooltip: { + callbacks: { + label: function(context) { + return `${context.dataset.label}: ${context.raw} minutes`; + } + } + } + } + } + }); + } + + private fetchArtistsVsTracksData() { + this.insightsService.getArtistsVsTracks().subscribe((data: any) => { + this.artistsVsTracks = data || { + distinctArtists: 0, + distinctTracks: 0 + }; + console.log("Artists vs Tracks Data:", this.artistsVsTracks); + this.createArtistsVsTracksChart(); + }); + } + + createArtistsVsTracksChart() { + if (this.artistsVsTracksChartInstance) { + this.artistsVsTracksChartInstance.destroy(); + } + + console.log("Creating Artists vs Tracks Chart with data:", this.artistsVsTracks); + + this.artistsVsTracksChartInstance = new Chart('artistsVsTracksChart', { + type: 'bar', + data: { + labels: ['Distinct Artists', 'Distinct Tracks'], + datasets: [ + { + label: 'Count', + data: [this.artistsVsTracks.distinctArtists, this.artistsVsTracks.distinctTracks], + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)' + ], + borderColor: [ + 'rgba(255, 99, 132, 1)', + 'rgba(54, 162, 235, 1)' + ], + borderWidth: 1 + } + ] + }, + options: { + scales: { + x: { + title: { + display: true, + text: 'Categories' + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: 'Count' + } + } + }, + plugins: { + legend: { + display: true, + position: 'top' + }, + tooltip: { + callbacks: { + label: function(context) { + return `${context.dataset.label}: ${context.raw}`; + } + } + } + } + } + }); + } + + createRecentTrackGenresChart() { + new Chart('recentTrackGenresChart', { + type: 'pie', + data: { + labels: Object.keys(this.recentTrackGenres), + datasets: [{ + label: 'Recent Track Genres', + data: Object.values(this.recentTrackGenres), + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255, 99, 132, 1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + legend: { + display: true, + position: 'top' + }, + tooltip: { + callbacks: { + label: function(context) { + let label = context.label || ''; + if (label) { + label += ': '; + } + if (context.raw !== null && typeof context.raw === 'number') { + const total = (context.chart.data.datasets[0].data as (number | null)[]).reduce((a, b) => (a ?? 0) + (b ?? 0), 0); + if (total !== 0) { + label += `${context.raw} (${((context.raw / (total ?? 1)) * 100).toFixed(2)}%)`; + } + } + return label; + } + } + } + } + } + }); + } } From e9a2125a676f9612221faaedccf5cac83107fa74 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Sun, 20 Oct 2024 13:10:19 +0200 Subject: [PATCH 09/10] :tada: Cache service for frontend --- Frontend/src/app/services/cache.service.ts | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Frontend/src/app/services/cache.service.ts diff --git a/Frontend/src/app/services/cache.service.ts b/Frontend/src/app/services/cache.service.ts new file mode 100644 index 00000000..d18a1175 --- /dev/null +++ b/Frontend/src/app/services/cache.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from "@angular/core"; + +@Injectable({ + providedIn: "root" +}) +export class CacheService +{ + private cache: Map = new Map(); + + set(key: string, data: any): void + { + this.cache.set(key, data); + } + + get(key: string): any + { + return this.cache.get(key); + } + + has(key: string): boolean + { + return this.cache.has(key); + } + + clear(): void + { + this.cache.clear(); + } +} From 9989062f07a696ef7209e3af121c359f38c7e52a Mon Sep 17 00:00:00 2001 From: 21797545 Date: Sun, 20 Oct 2024 13:13:16 +0200 Subject: [PATCH 10/10] :triangular_ruler: Added caching for moods in frontend --- Frontend/src/app/services/search.service.ts | 125 +++++++++----------- 1 file changed, 54 insertions(+), 71 deletions(-) diff --git a/Frontend/src/app/services/search.service.ts b/Frontend/src/app/services/search.service.ts index 8813e684..a5b1f286 100644 --- a/Frontend/src/app/services/search.service.ts +++ b/Frontend/src/app/services/search.service.ts @@ -1,20 +1,19 @@ import { Injectable } from "@angular/core"; import { HttpClient } from "@angular/common/http"; -import { Observable, BehaviorSubject } from "rxjs"; -import { tap } from "rxjs/operators"; +import { Observable, BehaviorSubject, of } from "rxjs"; +import { tap, switchMap, catchError } from "rxjs/operators"; import { TokenService } from "./token.service"; +import { CacheService } from "./cache.service"; import { environment } from "../../environments/environment"; -export interface Track -{ +export interface Track { name: string; albumName: string; albumImageUrl: string; artistName: string; } -export interface TrackInfo -{ +export interface TrackInfo { id: string; text: string; albumName: string; @@ -25,8 +24,7 @@ export interface TrackInfo explicit: boolean; } -export interface AlbumTrack -{ +export interface AlbumTrack { id: string; name: string; albumName: string; @@ -34,45 +32,41 @@ export interface AlbumTrack artist: string; } -export interface Artist -{ +export interface Artist { name: string; image: string; topTracks: Track[]; albums: AlbumTrack[]; } - @Injectable({ providedIn: "root" }) -export class SearchService -{ - //Subjects for search results (tracks, albums, and top search) +export class SearchService { + // Subjects for search results (tracks, albums, and top search) private searchResultSubject = new BehaviorSubject([]); private albumResultSubject = new BehaviorSubject([]); private topResultSubject = new BehaviorSubject({ name: "", albumName: "", albumImageUrl: "", artistName: "" }); - //Observables for search results (tracks, albums, and top search) + // Observables for search results (tracks, albums, and top search) searchResult$ = this.searchResultSubject.asObservable(); albumResult$ = this.albumResultSubject.asObservable(); topResult$ = this.topResultSubject.asObservable(); private apiUrl = environment.apiUrl; - constructor(private httpClient: HttpClient, private tokenService: TokenService, private http: HttpClient) - { - } + constructor( + private httpClient: HttpClient, + private tokenService: TokenService, + private cacheService: CacheService + ) {} // Store search results in searchResultSubject and set topResultSubject - storeSearch(query: string): Observable - { + storeSearch(query: string): Observable { return this.httpClient.post(`${this.apiUrl}/search/search`, { "title": query }) .pipe( - tap(results => - { + tap(results => { this.searchResultSubject.next(results); - if (results.length > 0) - { + if (results.length > 0) { this.topResultSubject.next(results[0]); // Update topResultSubject } }) @@ -80,53 +74,43 @@ export class SearchService } // Store album search results in albumResultSubject - storeAlbumSearch(query: string): Observable - { + storeAlbumSearch(query: string): Observable { return this.httpClient.post(`${this.apiUrl}/search/album`, { "title": query }) .pipe( - tap(results => - { + tap(results => { this.albumResultSubject.next(results); }) ); } // Get search results (for individual tracks) - getSearch(): Observable - { + getSearch(): Observable { return this.searchResult$; } // Get top search result - getTopResult(): Observable - { + getTopResult(): Observable { return this.topResult$; } // Get album search results (as albums) - getAlbumSearch(): Observable - { + getAlbumSearch(): Observable { return this.albumResult$; } // Get the suggested songs based on an input song from the ECHO API - public async echo(trackName: string, artistName: string): Promise - { - - + public async echo(trackName: string, artistName: string): Promise { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post(`${this.apiUrl}/spotify/queue`, { + const response = await this.httpClient.post(`${this.apiUrl}/spotify/queue`, { artist: artistName, song_name: trackName, accessToken: laccessToken, refreshToken: lrefreshToken }).toPromise(); - - if (response && Array.isArray(response.tracks)) - { + if (response && Array.isArray(response.tracks)) { const tracks = response.tracks.map((track: any) => ({ id: track.id, text: track.name, @@ -139,22 +123,18 @@ export class SearchService } as TrackInfo)); return tracks; - } - else - { + } else { throw new Error("Invalid response structure"); } } // Get the tracks of a specific album - public async getAlbumInfo(albumName: string): Promise - { - const response = await this.http.post(`${this.apiUrl}/search/album-info`, { + public async getAlbumInfo(albumName: string): Promise { + const response = await this.httpClient.post(`${this.apiUrl}/search/album-info`, { title: albumName }).toPromise(); - if (response && Array.isArray(response.tracks)) - { + if (response && Array.isArray(response.tracks)) { const albumName = response.name; const albumImageUrl = response.imageUrl; const artistName = response.artistName; @@ -167,23 +147,18 @@ export class SearchService } as AlbumTrack)); return tracks; - } - else - { + } else { throw new Error("Invalid response structure when searching for an album"); } } - //This function gets the details of a specific artist - public async getArtistInfo(artistName: string): Promise - { - console.log(`${this.apiUrl}/search/album-info`); - const response = await this.http.post(`${this.apiUrl}/search/album-info`, { + // This function gets the details of a specific artist + public async getArtistInfo(artistName: string): Promise { + const response = await this.httpClient.post(`${this.apiUrl}/search/album-info`, { artist: artistName }).toPromise(); - console.log("Here"); - if (response && Array.isArray(response.albums)) - { + + if (response && Array.isArray(response.albums)) { const artistName = response.name; const artistImage = response.image; const topTracks = response.topTracks.map((track: any) => ({ @@ -199,24 +174,32 @@ export class SearchService artist: album.artist } as AlbumTrack)); return [{ name: artistName, image: artistImage, topTracks: topTracks, albums: albums }]; - } - else - { + } else { throw new Error("Invalid response structure when searching for an artist"); } } // This function gets the songs for a specific mood - getSongsByMood(mood: string): Observable<{ imageUrl: string, tracks: Track[] }> - { + getSongsByMood(mood: string): Observable<{ imageUrl: string, tracks: Track[] }> { return this.httpClient.get<{ imageUrl: string, tracks: Track[] }>(`${this.apiUrl}/search/mood?mood=${mood}`); } - - // New Method: Fetch suggested moods with their tracks + // Fetch suggested moods with their tracks getSuggestedMoods(): Observable<{ mood: string; imageUrl: string; tracks: Track[] }[]> { - return this.httpClient.get<{ mood: string; imageUrl: string; tracks: Track[] }[]>(`${this.apiUrl}/search/suggested-moods`); - } - + const cacheKey = 'suggestedMoods'; + if (this.cacheService.has(cacheKey)) { + return of(this.cacheService.get(cacheKey)); + } + return this.httpClient.get<{ mood: string; imageUrl: string; tracks: Track[] }[]>(`${this.apiUrl}/search/suggested-moods`) + .pipe( + tap(response => { + this.cacheService.set(cacheKey, response); + }), + catchError(error => { + console.error("Error fetching suggested moods:", error); + return of([]); + }) + ); + } }