generated from melaasar/template
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of https://github.com/jaden-yejun-lee/MuseGuesser
- Loading branch information
Showing
13 changed files
with
406 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.