Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelPeng123 committed Nov 30, 2024
2 parents 9e3effb + b8145e7 commit 0f05740
Show file tree
Hide file tree
Showing 13 changed files with 406 additions and 95 deletions.
77 changes: 77 additions & 0 deletions backend/controller/dumbProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const { getAccessToken } = require("../utility/tokenManager");
const axios = require('axios');
const ProviderProxy = require("./providerProxy");

/* Singleton class to manage all spotify-related Requests */
class DumbProxy extends ProviderProxy {
static instance = null;

// Singleton method
static getInstance() {
// Create a singleton instance if none available
if (DumbProxy.instance == null) {
DumbProxy.instance = new DumbProxy()
}

return DumbProxy.instance
}

// Get track with ID
async getTrack(trackId) {
console.log(`Trying to get track with id ${trackId}`)

if (!(trackId in this.cache)) {
// request for specific track
console.log(`Requesting track with id ${trackId} from Spotify`)
try {
const accessToken = await getAccessToken();
const response = await axios.get(
`https://api.spotify.com/v1/tracks/${trackId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
).catch((error) => {console.log(error)});

this.addToCache(trackId, response.data) // add to cache
} catch (error) {
// TODO: error handling
throw error
}
}

return this.cache[trackId]
}

// Recommends tracks from genre
// TODO: for now we assume there is only one genre
async recommendTracks(genre, limit = 10) {
// Dumb proxy provides a list of randomly-generated tracks
let tracks = []
for (let i = 0; i < limit; i++) {
let track = {
genre: genre,
artists: [
{
"name": `Artist ${i + 1}`,
}
],
name: `Track ${i + 1}`,
preview_url: `dumb` // empty
}
tracks.push(track)
}
return tracks
}
}

async function test() {
let proxy = DumbProxy.getInstance()
let tracks = await proxy.recommendTracks("pop")
console.log(tracks)
}

// test()

module.exports = DumbProxy
165 changes: 165 additions & 0 deletions backend/controller/providerProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
const { getAccessToken } = require("../utility/tokenManager");
const axios = require('axios')

// Common filter functions
const PREVIEW_GENRE_FILTER_FAC = (genre) => (track) => track.preview_url && track.genre == genre
const PREVIEW_FILTER = (track) => track.preview_url
const ALL_FILTER = (track) => true

/* Singleton class to manage all spotify-related Requests */
class ProviderProxy {
static instance = null;

constructor() {
this.cache = new Map() // dictionary - trackId: trackInfo
this.rateLimit = 0 // rate limit estimate
this.rlHandler = new CacheHandler()
}

// Singleton method
static getInstance() {
// Create a singleton instance if none available
if (ProviderProxy.instance == null) {
ProviderProxy.instance = new ProviderProxy()
}

return ProviderProxy.instance
}

// Add to cache
addToCache(key, response) {
const cacheDuration = CacheHandler.cacheDuration(this.rateLimit)

// Store response with a timeout to remove it after cacheDuration
const timeoutId = setTimeout(() => {
this.cache.delete(key)
console.log(`Cache expired for key: ${key}`)
}, cacheDuration)

if (this.cache.has(key)) {
console.log(`Replaced track#${key}`)
} else console.log(`Added track#${key}`)

console.log(JSON.stringify(response))
this.cache.set(key, {response, timeoutId})
}

// Clear the cache
clearCache() {
// Clear all timeouts and cache entries
for (const [key, { timeoutId }] of this.cache.entries()) {
clearTimeout(timeoutId)
this.cache.delete(key)
}
}

// Get track with ID
async getTrack(trackId) {
console.log(`Trying to get track with id ${trackId}`)

if (!(trackId in this.cache)) {
// request for specific track
console.log(`Requesting track with id ${trackId} from Spotify`)
try {
const accessToken = await getAccessToken();
const response = await axios.get(
`https://api.spotify.com/v1/tracks/${trackId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
).catch((error) => {console.log(error)});

this.addToCache(trackId, response.data) // add to cache
} catch (error) {
// TODO: error handling
throw error
}
}

return this.cache[trackId]
}

// Recommends tracks from genre
// TODO: for now we assume there is only one genre
async recommendTracks(genre, limit = 10) {
try {
const accessToken = await getAccessToken();
const response = await axios.get(
`https://api.spotify.com/v1/recommendations?seed_genres=${genre}&limit=${limit}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
).catch((error) => console.log(error));

console.log("Getting responses")
response.data.tracks.forEach(track => {
track.genre = genre // append genre to the song
this.addToCache(track.id, track)
});

return response.data.tracks
} catch (error) {
// TODO: error handling
throw error
}

// TODO: Form recommendation from cache
// TODO: could race condition happens here?
}

// (base) Get a random track with filter
// returns null if no match
getRandomTrackFilter(filter) {
const filteredValues = [...this.cache.values()]
.map(entry => entry.response)
.filter((value) => filter(value))
if (filteredValues.length == 0) return null
return filteredValues[~~(Math.random() * filteredValues.length)]
}

// Get a random track
getRandomTrack() {
return this.getRandomTrackFilter(ALL_FILTER)
}

// Get a random track by genre
async getRandomTrackByGenre(genre) {
console.log("Fetching new recommendations of genre", genre)
await this.recommendTracks(genre)

console.log("Filtering results")
const filter = PREVIEW_GENRE_FILTER_FAC(genre)
const result = this.getRandomTrackFilter(filter)

return result
}
}

// Factory to produce different strategies handling rate limits
const LOW_RATE_CACHE = 600000 // 10 minutes
const MID_RATE_CACHE = LOW_RATE_CACHE * 2 // 20 minutes
const HIGH_RATE_CACHE = MID_RATE_CACHE * 2 // 40 minutes

class CacheHandler {
constructor() {

}

static cacheDuration(rateLimit) {
// TODO: strategies
return LOW_RATE_CACHE
}
}

async function test() {
const proxy = ProviderProxy.getInstance()
console.log(proxy.getRandomTrackByGenre("pop"))
await proxy.recommendTracks("pop", 10)
console.log(proxy.getRandomTrackByGenre("pop"))
}

module.exports = ProviderProxy
92 changes: 6 additions & 86 deletions backend/controller/spotifyProxy.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
const { getAccessToken } = require("../utility/tokenManager");
const axios = require('axios')

// Common filter functions
const PREVIEW_GENRE_FILTER_FAC = (genre) => (track) => track.preview_url && track.genre == genre
const PREVIEW_FILTER = (track) => track.preview_url
const ALL_FILTER = (track) => true
const axios = require('axios');
const providerProxy = require("./providerProxy");

/* Singleton class to manage all spotify-related Requests */
class SpotifyProxy {
class SpotifyProxy extends providerProxy {
static instance = null;

constructor() {
this.cache = new Map() // dictionary - trackId: trackInfo
this.rateLimit = 0 // rate limit estimate
this.rlHandler = new CacheHandler()
}

// Singleton method
static getInstance() {
// Create a singleton instance if none available
Expand All @@ -26,33 +16,6 @@ class SpotifyProxy {
return SpotifyProxy.instance
}

// Add to cache
addToCache(key, response) {
const cacheDuration = CacheHandler.cacheDuration(this.rateLimit)

// Store response with a timeout to remove it after cacheDuration
const timeoutId = setTimeout(() => {
this.cache.delete(key)
console.log(`Cache expired for key: ${key}`)
}, cacheDuration)

if (this.cache.has(key)) {
console.log(`Replaced track#${key}`)
} else console.log(`Added track#${key}`)

console.log(JSON.stringify(response))
this.cache.set(key, {response, timeoutId})
}

// Clear the cache
clearCache() {
// Clear all timeouts and cache entries
for (const [key, { timeoutId }] of this.cache.entries()) {
clearTimeout(timeoutId)
this.cache.delete(key)
}
}

// Get track with ID
async getTrack(trackId) {
console.log(`Trying to get track with id ${trackId}`)
Expand Down Expand Up @@ -100,6 +63,8 @@ class SpotifyProxy {
track.genre = genre // append genre to the song
this.addToCache(track.id, track)
});

return response.data.tracks
} catch (error) {
// TODO: error handling
throw error
Expand All @@ -108,49 +73,6 @@ class SpotifyProxy {
// TODO: Form recommendation from cache
// TODO: could race condition happens here?
}

// (base) Get a random track with filter
// returns null if no match
getRandomTrackFilter(filter) {
const filteredValues = [...this.cache.values()]
.map(entry => entry.response)
.filter((value) => filter(value))
if (filteredValues.length == 0) return null
return filteredValues[~~(Math.random() * filteredValues.length)]
}

// Get a random track
getRandomTrack() {
return this.getRandomTrackFilter(ALL_FILTER)
}

// Get a random track by genre
async getRandomTrackByGenre(genre) {
console.log("Fetching new recommendations of genre", genre)
await this.recommendTracks(genre)

console.log("Filtering results")
const filter = PREVIEW_GENRE_FILTER_FAC(genre)
const result = this.getRandomTrackFilter(filter)

return result
}
}

// Factory to produce different strategies handling rate limits
const LOW_RATE_CACHE = 600000 // 10 minutes
const MID_RATE_CACHE = LOW_RATE_CACHE * 2 // 20 minutes
const HIGH_RATE_CACHE = MID_RATE_CACHE * 2 // 40 minutes

class CacheHandler {
constructor() {

}

static cacheDuration(rateLimit) {
// TODO: strategies
return LOW_RATE_CACHE
}
}

async function test() {
Expand All @@ -160,6 +82,4 @@ async function test() {
console.log(proxy.getRandomTrackByGenre("pop"))
}

module.exports = {
SpotifyProxy
}
module.exports = SpotifyProxy
3 changes: 2 additions & 1 deletion backend/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ mongoose.connect(process.env.MONGODB_URI)
const Song = require('./models/songModel.js');
const User = require('./models/userModel.js');
const DailyChallenge = require('./models/dailyChallenge.js')
const Match = require('./models/matchModel.js');

module.exports = {User, Song, DailyChallenge};
module.exports = {User, Song, DailyChallenge, Match};
Loading

0 comments on commit 0f05740

Please sign in to comment.