-
Notifications
You must be signed in to change notification settings - Fork 0
Sep
-
Orienteren en begrijpen -> Ik heb deze week op mijzelf onderzoek gedaan naar de OAuth van spotify en de diverse endpoints die hun Web API heeft, meer info is te vinden bij het onderzoek. Daarnaast heb ik verder de volgende tutorials gevolgd om github beter te begrijpen/leren: Github crashcourse
-
Prototpyen en uitwerken -> Ik heb een werkend prototype gemaakt waarin ik kan verbinden met de Spotify Web API met de OAuth. In dit prototype krijg ik ook wat nummers aanbevolen, op basis van artiesten en nummers die ik heb aangegeven. Op dit principe gaat onze app werken. Meer info bij het onderzoek
-
Samen ontwerpen -> Ik heb samen met joost protection op de main branch gezet. Er moet minimaal 1 reviewer zijn als je iets wil mergen in de main, ook mag je niet direct een bestand toevoegen aan de main, hier moet eerst een branch voor worden aangemaakt zodat je deze kan mergen met de main. Daarnaast heb ik wat verduidelijking gegeven aan ons team over het gebruik van branches, GitHub Desktop en commits.
-
Verbeelden en conceptualiseren -> Ik heb gewerkt (samen met het team) aan ons concept, wat nu Juke is geworden. Hier heb ik meegeholpen aan een moodboard, logo ideatie. Daarnaast heb ik een persona, empathy map en job stories gemaakt, wat aansluit bij ons persona en hun benodigheden. Resultaten zijn te zien op de ontwerp pagina (Moodboard, logo, stylesheet)
-
Kenmerken:
- Leeftijd: Voornamelijk jongvolwassenen en volwassenen tussen 18 en 35 jaar.
- Interesses: Passie voor muziek, van casual luisteraars tot fanatieke muziekliefhebbers.
- Technologiegebruik: Comfortabel met het gebruik van digitale platforms zoals Spotify en mobiele apps.
-
Wensen en doelen:
- Ontdekken van nieuwe muziek: Ze willen hun muzikale horizon verbreden en nieuwe artiesten en nummers ontdekken die passen bij hun smaak en voorkeuren.
- Personalisatie: Ze willen graag op maat gemaakte aanbevelingen die zijn afgestemd op hun individuele luistergeschiedenis en voorkeuren.
- Gemak: Ze zoeken naar een eenvoudige en intuïtieve manier om nieuwe muziek te ontdekken, zonder veel tijd te hoeven besteden aan het zoeken naar geschikte nummers.
-
Uitdagingen:
- Overweldiging: Het grote aanbod van muziek kan overweldigend zijn, waardoor het moeilijk kan zijn om nieuwe artiesten en nummers te vinden die echt aanspreken.
- Muzikale stagnatie: Sommige gebruikers kunnen vast komen te zitten in een routine van het luisteren naar dezelfde muziekgenres of artiesten, en hebben moeite om nieuwe inspiratie op te doen.
Voor deze doelgroep hebben wij ook een empathy map gemaakt, de empathy map is gebaseert op het huidige gedrag van de gebruiker.
- Samen ontwerpen -> In groepsverband hebben we gewerkt aan het design en concept van onze web-app, wat nu Juke gaat heten. Tijdens deze werkgroep hebben we actief feedback aan elkaar gevraagd en zijn zo samen tot een mooi en passende oplossing gekomen, resultaat hiervan is te vinden op de ontwerp pagina (Moodboard, logo, stylesheet)
Welke dingen zijn nog niet duidelijk of moeilijk, waarbij kun je hulp gebruiken?
- (Docent) Vraag over endpoints, hoe gaat het werken met dat :id? Bijvoorbeeld om een genre te discoveren
Benoem concrete vragen over het werk wat je deze week hebt gedaan, die je aan je teamgenoten en/of docenten wilt stellen
- (Team) Wat zouden jullie veranderen aan de wireframes?
- (Team) Denk je dat je als buitenstaander zou kunnen begrijpen wat de app nu doet?
- (Team) Zijn de huidige job stories in orde, en wat zouden jullie nog toevoegen?
Beschrijf de feedback die je hebt ontvangen en hoe je deze gaat gebruiken om je werk verder te verbeteren.
- De menubalk is niet ideaal voor een web applicatie, daarom gaan we dit veranderen naar een soort humburger menu variant. Verder onderzoek naar mogelijkheden is nog nodig.
- Job stories, waren in orde. Hebben wel een User Requirement list opgesteld, die we hebben gerangschikt op belangrijk, m.b.v. de MoSCoW techniek, zie ontwerp pagina voor meer info.
-
Prototypen en uitwerken -> Authorization prototype gemaakt, maar nu zonder PKCE standard. Dus standard "authorization code", hiervoor gekozen omdat het beter past bij server-side JavaScript.
Link naar prototype -
Oriënteren en begrijpen -> Verder onderzoek gedaan naar spotify authorization, ben erachter gekomen dat we de "authorization code" flow moesten gebruiken, aangezien deze beter past bij bij een server-side JS. Meer info te vinden bij het onderzoek over de spotify OAuth
- Verbeelden en conceptualiseren -> Ik heb, samen met Vivanne, de User Requirement List opgesteld, zodat we de eindbelangen van de gebruiker goed kunnen neerzetten. Daarna hadden we deze gerangschikt met de MoSCoW methode, dit helpt met onze prioriteiten.
Hieronder is onze user requirement list te zien, de verschillende requirements hebben een prioriteit gekregen door de MoSCoW methode te gebruiken.
-
M -> Must have
-
S -> Should have
-
C -> Could have
-
W -> Won't have
-
Aanmeldings- en Gebruikersbeheer
- (M) Gebruikers moeten zich kunnen aanmelden met hun Spotify-account.
- (M) Gebruikers moeten hun profiel kunnen bekijken.
- (M) Gebruikers moeten zich kunnen afmelden van de web-app.
-
Muziekaanbevelingen
- (M) Aanbevelingen op Basis van Luistergeschiedenis: De web-app moet aanbevelingen doen op basis van de recente luistergeschiedenis van de gebruiker op Spotify.
- (M) Aanbevelingen op Basis van persoonlijke top Nummers: De web-app moet aanbevelingen doen op basis van de top nummers van de gebruiker op Spotify.
- (C) Populaire Aanbevelingen: De web-app moet populaire nummers aanbevelen die veel worden beluisterd door andere gebruikers met vergelijkbare smaak.
-
Interactie met Aanbevelingen
- (M) Swipe Functie: Gebruikers moeten nummers naar rechts kunnen swipen als ze het nummer leuk vinden en naar links kunnen swipen als ze het nummer niet leuk vinden.
- (M) Nummer Opslaan: Gebruikers moeten de optie hebben om een nummer dat ze leuk vinden op te slaan in een Spotify-afspeellijst.
- (M) Nummer Overslaan: Gebruikers moeten de optie hebben om een nummer over te slaan als ze het niet leuk vinden.
-
Gebruikerservaring
- (S) Zoekfunctie: Gebruikers moeten handmatig naar nummers of artiesten kunnen zoeken binnen de web-app.
- (M) Afspelen van Fragmenten: Gebruikers moeten een fragment van elk aanbevolen nummer kunnen beluisteren voordat ze besluiten het leuk te vinden of over te slaan.
-
Feedback en Optimalisatie
- (W) Feedback Functie: Gebruikers moeten feedback kunnen geven over de nauwkeurigheid van de aanbevelingen.
- (C) Voorkeuren Instellen: Gebruikers moeten hun muziekvoorkeuren kunnen instellen of aanpassen voor betere aanbevelingen.
-
Integratie met Spotify
- (S) Synchronisatie met Spotify: De web-app moet naadloos synchroniseren met de Spotify-bibliotheek van de gebruiker.
- (C) Afspelen op Spotify: Gebruikers moeten de mogelijkheid hebben om een aanbevolen nummer direct af te spelen op Spotify.
- (C) Afspeellijsten Beheren: Gebruikers moeten hun Spotify-afspeellijsten kunnen beheren binnen de web-app.
-
Beveiliging en Privacy
- (S) Gegevensbescherming: De web-app moet voldoen aan de GDPR en andere relevante regelgeving omtrent gegevensbescherming.
- (S) Privacy Instellingen: Gebruikers moeten hun privacy-instellingen kunnen beheren, zoals welke gegevens worden gedeeld en hoe hun gegevens worden gebruikt.
-
Ondersteuning en Hulp
- (C) Helpsectie: De web-app moet een helpsectie bevatten waar gebruikers antwoorden kunnen vinden op veelgestelde vragen.
- (C) Contact Ondersteuning: Gebruikers moeten contact kunnen opnemen met de ondersteuningsteam voor hulp en technische ondersteuning.
- Samen ontwerpen -> Zoals ik net had vermeld, had ik samen met Vivanne gewerkt aan de User Requirement List en deze gerangschikt met de MoSCoW methode.
Welke dingen zijn nog niet duidelijk of moeilijk, waarbij kun je hulp gebruiken?
- Welke Authorization flow moet ik gaan gebruiken?
Benoem concrete vragen over het werk wat je deze week hebt gedaan, die je aan je teamgenoten en/of docenten wilt stellen
- Geen vraag, maar ik ga het hebben over code standards met het team.
- Navragen over login page design.
Beschrijf de feedback die je hebt ontvangen en hoe je deze gaat gebruiken om je werk verder te verbeteren.
Beschrijf wat je deze week hebt gedaan in termen van de competenties waaraan we in dit project werken. Je zult niet elke week aan elke competentie werken, maar in het hele project komen ze als het goed is wel allemaal aan de orde. De voorbeeldvragen die zijn genoemd bij de uitleg over de wiki op DLO kunnen je hierbij helpen.
-
Samen ontwerpen -> Ik heb een GitHub Project aangemaakt en uitgelegd hoe dit werkt aan de rest van het team, zodat wij als team een goede teamverdeling konden behouden en zodat we een goed overzicht hebben van alle taken wat nog gedaan moet worden en wie waar mee bezig is.
-
Oriënteren en begrijpen -> Ik heb mijzelf verdiept in database structuren. Voor onze applicatie willen we voornamelijk: de recommended nummers en gebruikers activiteit opslaan in de database. Ik heb dit geprompt bij wat AI-tools:
User collection:
"_id": ObjectId("user_id"),
"spotifyId": "user_spotify_id",
"displayName": "User Name",
"email": "user@example.com",
"likedSongs": [
ObjectId("song_id1"),
ObjectId("song_id2"),
...
],
"dislikedSongs": [
ObjectId("song_id3"),
ObjectId("song_id4"),
...
],
"swipeHistory": [
{
"songId": ObjectId("song_id1"),
"action": "liked",
"timestamp": ISODate("2024-05-30T12:34:56Z")
},
{
"songId": ObjectId("song_id3"),
"action": "disliked",
"timestamp": ISODate("2024-05-30T12:35:56Z")
},
...
]
}
Songs collection:
{
"_id": ObjectId("song_id"),
"title": "Song Title",
"artist": "Artist Name",
"album": "Album Name",
"spotifyId": "song_spotify_id",
"duration": 210, // duration in seconds
"genre": "Genre",
...
}
Swipe collection (geschiedenis)
{
"_id": ObjectId("swipe_id"),
"userId": ObjectId("user_id"),
"songId": ObjectId("song_id"),
"action": "liked", // or "disliked"
"timestamp": ISODate("2024-05-30T12:34:56Z")
}
User collection:
db.users.insertOne({
name: "John Doe",
email: "john@example.com",
likedSongs: [],
dislikedSongs: []
})
Songs collection:
db.songs.insertOne({
title: "Song Title",
artist: "Artist Name",
album: "Album Name",
likes: [],
dislikes: [],
swipes: 0
})
Ik ga de database structuur baseren op het idee van Claude AI, omdat deze efficienter voor ons geval en doeleind is. De structuur van Claude AI is ook overzichtelijker, door maar 2 collections te gebruiken i.p.v. 3.
- Prototypen en uitwerken -> Ik ben bezig geweest met de functionaliteit van nummers liken en disliken. Ik heb er voornamelijk voor gezorgd dat de back-end de data van de front-end goed krijgt doorgegeven en daarna heb ervoor gezorgd dat de liked en disliked nummer worden opslagen in de database onder de juiste gebruiker. Ook heb ik gewerkt aan de functionaliteit van playlists toevoegen aan Spotify. Een obstakel waar ik tegenaan liep was dat ik wou kijken of er al een "My Juke Playlist" was aangemaakt, zodat we daar weer nummers in konden plaatsen. Ik had toen dit geschreven:
async function fetchUserPlaylists(req) {
try {
const playlists = await fetchWebApi(
req,
'v1/me/playlists',
'GET'
)
console.log('Fetched playlists:', playlists.items) // Debug log
return playlists.items
} catch (error) {
console.error('Error fetching user playlists:', error)
}
}
// Function to create the Juke Playlist
async function createJukePlaylist(req) {
try {
const userInfo = req.session.user
// Fetch user's playlists
const userPlaylists = await fetchUserPlaylists(req)
if (!userPlaylists) {
throw new Error('Failed to fetch user playlists.')
}
// Check if "Your Juke Playlist" exists
const existingPlaylist = userPlaylists.find(playlist => playlist.name === "Your Juke Playlist")
if (existingPlaylist) {
console.log('Playlist "Your Juke Playlist" already exists:', existingPlaylist)
return existingPlaylist
} else {
// Create the playlist if it doesn't exist
const newPlaylist = await fetchWebApi(
req,
`v1/users/${userInfo.id}/playlists`,
'POST',
{
name: "Your Juke Playlist",
description: "Playlist created by the tutorial on developer.spotify.com",
public: false
}
)
console.log('Created new playlist:', newPlaylist)
return newPlaylist
}
} catch (error) {
console.error('Error creating playlist:', error)
}
}
// Route to handle playlist creation
app.get('/create-playlist', async (req, res) => {
try {
const playlist = await createJukePlaylist(req)
console.log("createdPlaylist", playlist)
res.redirect('/')
} catch (error) {
console.error('Error url - creating playlist:', error)
}
})
Maar ik besefte mij dat dit erg veel request en code was voor een eigenlijk simpele taak. Ook zou deze code breken als de gebruiker de naam van de playlist zou veranderen etc. Ik ben toen op het idee gekomen om gewoon het ID van de playlist 1 keer op te slaan in de database onder de gebruikers naam, zodat ik deze altijd snel en makkelijk kon vinden en het minder ingewikkeld werd.
async function JukePlaylist(req) {
try {
const userInfo = req.session.user
const user = await usersCollection.findOne({ _id: userInfo.id }, { playlist_id: 1 }) // playlist_id: 1, to only return the playlist_id field
if (user.playlist_id) {
console.log('Playlist_id exists:', user.playlist_id)
return user.playlist_id
} else {
console.log('Playlist does not exist yet')
const playlist = await fetchWebApi(
req,
`v1/users/${userInfo.id}/playlists`,
'POST',
{
name: "My Juke Playlist",
description: "Playlist created by the tutorial on developer.spotify.com",
public: false
}
)
// Set playlist_id in the user's DB object
await usersCollection.updateOne(
{ _id: userInfo.id },
{ $set: { playlist_id: playlist.id } }
)
console.log('Created playlist:', playlist.id)
return playlist.id
}
} catch (error) {
console.error('Error creating playlist:', error)
}
}
// Route to handle playlist creation
app.get('/create-playlist', async (req, res) => {
try {
const playlist = await JukePlaylist(req)
// console.log("createdPlaylist", playlist)
res.redirect('/')
} catch (error) {
console.error('Error url - creating playlist:', error)
}
})
- Vraag over database structuur, had aan wat AI tools gevraagd om te helpen met database structuren te bedenken. Samen met docent kijken welke we kunnen toepassen of wat we moeten aanpassen.
- Vraag over hoe we vanuit front-end, back-end functies moeten aanroepen? Bv. Song liken: event triggered, en hoe dan naar database en in spotify playlist?
Benoem concrete vragen over het werk wat je deze week hebt gedaan, die je aan je teamgenoten en/of docenten wilt stellen
- Info verzamelen data dat we verzamelen: Even op een lijstje zetten, wat voor data willen we allemaal verzamelen en opslaan. Loop de app samen door en denk erover na
- Wanneer willen we front-end en back-end gaan samenvoegen?
- Database structuur van claude.ai is, zoals ik had vermoedt, de beter optie uit de twee. Er zullen nog wel wat aanpassingen aan gedaan moeten worden. Maar het is een solide begin.
- Van Berry: We kunnen data en request versturen vanuit de front-end naar de back-end met de fetch en dan een url, dus fetch('/route'). We kunnen deze informatie post naar de server of via een get request.
Beschrijf wat je deze week hebt gedaan in termen van de competenties waaraan we in dit project werken. Je zult niet elke week aan elke competentie werken, maar in het hele project komen ze als het goed is wel allemaal aan de orde. De voorbeeldvragen die zijn genoemd bij de uitleg over de wiki op DLO kunnen je hierbij helpen.
-
Samen ontwerpen -> Vivanne was bezig met de overzichts pagina's voor de liked en disliked recommendations. Een onderdeel hiervan was de functionaliteit om gelikete nummers te kunnen un-liken en vice versa. Maar dit kreeg ze niet werkend en vroeg om mijn hulp. Wij hebben er samen even naar gekeken en het probleem bleek te zitten in de back-end functie die het de "action" property van het nummer in de database niet goed was geschreven.
Dit is de code geworden die we samen hebben geschreven:
server.js:
app.post('/unlike', async (req, res) => {
const { track_id } = req.body;
if (!track_id) {
return res.status(400).send({ status: 'No track_id found' });
}
try {
const userId = req.session.user.id; // Get the user ID from the session
// Check if the user exists in the database
const user = await usersCollection.findOne({ _id: userId });
if (!user) {
return res.status(404).send({ status: 'error', message: 'User not found' });
}
// Remove the track from the user's recommendations array
await usersCollection.updateOne(
{ _id: userId, "recommendations._id": track_id },
{
$set: { "recommendations.$.action": "dislike" },
$inc: { 'swipes.likes': -1 }
}
);
console.log(`Song removed with id: ${track_id}`);
// Remove the song from the playlist
await removeSongFromPlaylist(req);
// Update the song in the songs collection
await songsCollection.updateOne(
{ _id: track_id },
{ $pull: { likes: userId } }
);
res.status(200).send({ status: 'success' });
} catch (err) {
console.error(err);
res.status(500).send({ status: 'error', message: err.message });
}
});
app.js:
ocument.addEventListener('DOMContentLoaded', () => {
const allHearts = document.querySelectorAll('#heart');
const likeUnlikePost = (heart) => {
const trackId = heart.dataset.trackId;
const trackName = heart.dataset.trackName;
const trackArtists = heart.dataset.trackArtists.split(',');
const trackImages = heart.dataset.trackImages.split(',');
const isLiked = heart.classList.contains('like');
const action = isLiked ? 'unlike' : 'like';
const url = `/${action}`;
const bodyData = { track_id: trackId };
if (!isLiked) {
bodyData.track_name = trackName;
bodyData.track_artists = trackArtists;
bodyData.track_images = trackImages;
}
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(bodyData),
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
heart.classList.toggle('like', !isLiked);
heart.classList.toggle('unlike', isLiked);
// Verwijder de huidige LI van de lijst na een korte vertraging
setTimeout(() => {
const listItem = heart.closest('li');
listItem.remove();
}, 2200); // Pas de vertraging aan op basis van je animatieduur
} else {
console.error('Error:', data.message);
}
})
.catch(error => {
console.error('Error:', error);
});
};
allHearts.forEach(heart => {
heart.addEventListener('click', () => {
likeUnlikePost(heart);
});
});
});
-
Evalueren -> Tijdens het testen van de site kwam Vivanne erachter dat niet elke recommendation die wij opvragen van de Spotify web API, een preview_url heeft die wij kunnen gebruiken om een kort voorstuk van het nummer te laten horen aan de gebruiker. Dit zou voor een slechte gebruikers ervaring zorgen, daarom vroeg Vivanne aan mij: "Sommige recommendations hebben geen preview link, zou je ervoor kunnen zorgen dat we alleen de nummers gebruiken die wel een preview hebben?". Daarom ben ik aan de slag gegaan met het maken van een functie die de recommendation filtert. Tijdens dit process was ik erachter gekomen dat nummers soms dubbel werden aanbevolen, terwijl je ze al had geliked of disliked. Dit zou ook echt vervelend zijn voor de gebruikers ervaring. Dus ben ik gaan door itereren op de filter functionaliteit en hier een oplossing voor gemaakt.
-
Prototypen en uitwerken -> Zoals ik net had vermeld bij de competentie: evalueren, Om de functionaliteit en gebruikers vriendelijkheid heb ik de functionaliteit van de recommendations fetchen aangepast. Ik heb een functionaliteit gemaakt die de recommendations, die geen preview_url hebben en recommendations die al eerder zijn aanbevolen, eruit filtert. Ik was begonnen met het maken van de filter voor de preview_urls, maar toen eenmaal bezig was kwam ik erachter dat sommige recommendations meerdere malen voor voorgesteld aan de gebruiker terwijl ze deze al eerder hebben geliked/disliked. Ik heb dus meteen dit meegepakt in de filterRecommendation functie.
Dit is de vernieuwde code geworden voor het fetchen van de recommendations, inclusief filter:
async function getRecommendations(req, seedTracks, limit) {
try {
let approvedRecommendations = [];
let remainingLimit = limit;
while (remainingLimit > 0) {
const recommendations = (
await fetchWebApi(
req,
`v1/recommendations?limit=${remainingLimit}&seed_tracks=${seedTracks.join(',')}`,
'GET'
)
).tracks;
const filteredRecommendations = await filterRecommendations(req, recommendations);
approvedRecommendations.push(...filteredRecommendations);
remainingLimit = limit - approvedRecommendations.length;
}
return approvedRecommendations.slice(0, limit);
} catch (error) {
console.error('Error getting recommendations:', error);
}
}
async function filterRecommendations(req, recommendations) {
try {
const filteredRecommendations = await Promise.all(
recommendations.map(async (track) => {
if (!hasPreviewUrl(track)) {
console.log(`Track "${track.name}" doesn't have a preview_url`)
return null
}
const userRecommendations = await usersCollection.findOne(
{
_id: req.session.user.id,
'recommendations._id': track.id
},
{ projection: { _id: 1 } }
)
if (userRecommendations) {
console.log(`Track "${track.name}" already registered.`)
return null
}
return track
})
)
return filteredRecommendations.filter(Boolean)
} catch (error) {
console.error('Error filtering recommendations:', error)
}
}
function hasPreviewUrl(track) {
return track.preview_url !== null && track.preview_url !== ''
}
Beschrijf wat je deze week hebt gedaan in termen van de competenties waaraan we in dit project werken. Je zult niet elke week aan elke competentie werken, maar in het hele project komen ze als het goed is wel allemaal aan de orde. De voorbeeldvragen die zijn genoemd bij de uitleg over de wiki op DLO kunnen je hierbij helpen.
-
Samen ontwerpen -> Vivanne was bezig met het maken van functionaliteit waar de gebruiker kan zoeken op nummers en artiesten, zodat ze vervolgens op een specifiek nummer/artiest recommendations kunnen krijgen. Zij had de zoekbalk en resultaten al helemaal werkend, ze had alleen hulp nodig met het goed werkend krijgen van deze specifieke recommendations, ze had het aanbevelen op basis van nummers al redelijk werkend, maar de artiesten wou maar niet lukken. Ze vroeg het volgende aan mij: "Kan je mij helpen om deze recommendations te laten werken? Ik krijg de artiesten maar niet werkend". Ik ben vervolgens gaan door itereren op haar eigen code en heb de volgende aanpassingen gemaakt:
- Form method naar GET,
- getRecommendations functie laten werken met diverse parameters
- seed_uri van het (gezoekte) nummers ophalen uit de url. Met deze aanpassing heb ik samen met Vivanne deze functionaliteit werkend gekregen.
-
Prototypen en uitwerken -> Ik heb door geïtereerd op de zoek-functionaliteit van Vivanne. Zij had de zoekbalk en resultaten al helemaal werkend, ze had alleen hulp nodig met het goed werkend krijgen van deze specifieke recommendations. De aanpassing wat ik heb gemaakt, om dit te laten werken zijn:
- Form method van de "swipe" knop naar GET veranderd, dit heb ik gedaan zodat de parameters in de url komen. Een vraag die ik mijzelf stelde was namelijk: "Hoe kan ik het beste de gegevens van het nummer waar ik recommendations op zoek, opslaan", de conclusie hiervan was de url, aangezien deze globaal beschikbaar is en niet weggaat als er een nummer wordt geliked/disliked.
- De getRecommendations functie aangepast zodat deze werkt met allerlei verschillende parameters. Nu kan het in dezelfde functie beide gevallen: seed_tracks & seed_artists, afhandelen.
- In de front-end code de parameters uit de url halen, zodat deze gebruikt kan worden om een nieuw nummer aan te maken gebaseert op het juiste nummer/artiest. \
Server.js:
/*==========================================\
Searched recommendations
===========================================*/
app.get('/search-recommendations', async (req, res) => {
try {
const { seed_uri, seed_type, seed_name } = req.query
if (!seed_uri || !seed_type) {
return res.status(400).json({ error: 'No query provided' })
}
const limit = 2
const recommendedTracks = await getRecommendations(req, limit, seed_type, [seed_uri])
res.render('pages/recommendations', { tracks: recommendedTracks, user: req.session.user, seed_name: seed_name })
} catch (error) {
console.error('Error fetching recommendations:', error)
res.status(500).json({ error: 'An error occurred while fetching recommendations.' })
}
})
app.get('/new-search-recommendation', async (req, res) => {
try {
const { seed_type, seed_uri } = req.query
if (!seed_type || !seed_uri) {
return res.status(400).json({ error: 'Missing seed_type or seed_uri' })
}
const limit = 1
const recommendedTracks = await getRecommendations(req, limit, seed_type, [seed_uri])
res.json({ recommendation: recommendedTracks[0] })
} catch (error) {
console.error('Error fetching new recommendation:', error)
res.status(500).json({ error: 'An error occurred while fetching a new recommendation.' })
}
})
async function getRecommendations(req, limit, seed_type, seed_uri) {
try {
let approvedRecommendations = []
let remainingLimit = limit
while (remainingLimit > 0) {
const recommendations = (
await fetchWebApi(
req,
`v1/recommendations?limit=${remainingLimit}&${seed_type}=${seed_uri.join(',')}`,
'GET'
)
).tracks
const filteredRecommendations = await filterRecommendations(req, recommendations)
approvedRecommendations.push(...filteredRecommendations)
remainingLimit = limit - approvedRecommendations.length
}
return approvedRecommendations.slice(0, limit)
} catch (error) {
console.error('Error getting recommendations:', error)
}
}
searchResults.ejs snippet:
<section class="searchResultList">
<h2>Artiesten</h2>
<ul>
<% results.artists.items.forEach(artist => { %>
<li>
<img src="<%= artist.images[0]?.url %>" alt="<%= artist.name %>">
<div>
<p class="textOverflow"><%= artist.name %></p>
</div>
<form action="/search-recommendations" method="get">
<input type="hidden" name="seed_uri" value="<%= artist.id %>">
<input type="hidden" name="seed_type" value="seed_artists">
<input type="hidden" name="seed_name" value="<%= artist.name %>">
<input type="submit" value="Swipe" class="swipeButton">
</form>
</li>
<% }); %>
</ul>
</section>
handleSwiping.js verandering:
// Haal seed_uri en seed_type op uit de url
const urlParams = new URLSearchParams(window.location.search);
const seed_uri = urlParams.get('seed_uri');
const seed_type = urlParams.get('seed_type');
// Haal nieuwe recommendation op
const newRecommendation = await fetchNewRecommendation(isSearch, seed_type, seed_uri);
const newCard = createCardElement(newRecommendation, isSearch);
cardsContainer.appendChild(newCard);
async function fetchNewRecommendation(isSearch, seed_type, seed_uri) {
// Standard url
let url = '/new-recommendation';
// Als search recommendation, verander url
if (isSearch) {
url = `/new-search-recommendation?seed_type=${seed_type}&seed_uri=${seed_uri}`;
}
const res = await fetch(url);
if (res.ok) {
const data = await res.json();
return data.recommendation;
}
return null;
}
-
Prototypen en uitwerken -> Ik kreeg het volgende van Vivanne te horen: "De website stuurt nu te veel request naar de API, zou je dit kunnen verbeteren?". En het klopte wat ze zei, de code voor het halen van recommendations uit de Spotify API was niet geoptimaliseerd. Hier heb ik het volgende aan veranderd:
- Persoonlijke TopTracks cachen met een expiration time van 1 uur
- Recommendations opvragen in grote batches ipv, per 1 of 2.
- De goed recommended tracks opslaan in de req.session voor makkelijke toegang en persistence.
TopTracks cache:
async function getTopTracks(req) {
const cacheTimeTillExpiration = 60 * 60 * 1000; // 1 hour in milliseconds
// Check if top tracks are in the session and not expired
if (req.session.topTracks && new Date() < new Date(req.session.topTracksExpiry)) {
return req.session.topTracks;
}
// Fetch top tracks from Spotify API
const topTracks = (await fetchWebApi(
req,
'v1/me/top/tracks?time_range=short_term&limit=5', 'GET'
)).items;
// Store top tracks and expiry time in session
req.session.topTracks = topTracks;
req.session.topTracksExpiry = new Date(new Date().getTime() + cacheTimeTillExpiration);
return topTracks;
}
recommendations functie aanpassing:
async function getRecommendations(req, seed_type, seed_uri) {
try {
const maxBatchAmount = 20
const minBatchAmount = 10
let approvedRecommendations = []
console.log('Fetching initial recommendations...')
while (approvedRecommendations.length < minBatchAmount) {
console.log('approvedRecommendations lentgh:', approvedRecommendations.length)
const recommendations = (
await fetchWebApi(
req,
`v1/recommendations?limit=${maxBatchAmount}&${seed_type}=${seed_uri.join(',')}`,
'GET'
)
).tracks
console.log(`Received ${recommendations.length} recommendations`)
const filteredRecommendations = await filterRecommendations(req, recommendations)
approvedRecommendations.push(...filteredRecommendations)
}
// Put the approved recommendations in the session
req.session.recommendationTracks = approvedRecommendations
console.log('Initial recommendations fetched:', approvedRecommendations.length)
} catch (error) {
console.error('Error getting recommendations:', error)
}
}
recommendations routes aanpassing:
app.get('/recommendations', async (req, res) => {
try {
const recommendationsAmount = 2
const seed_type = 'seed_tracks'
const topTracks = await getTopTracks(req)
const seed_uri = topTracks.map((track) => track.id)
await getRecommendations(req, seed_type, seed_uri)
// Get first 2 recommendations and remove them from the session
const recommendedTracks = req.session.recommendationTracks.slice(0, recommendationsAmount)
req.session.recommendationTracks = req.session.recommendationTracks.slice(recommendationsAmount)
// Debugging
console.log('length:', req.session.recommendationTracks.length)
console.log('showing recommended tracks:', recommendedTracks.map(track => track.name))
// Render page with recommendations
res.render('pages/verkennen', { tracks: recommendedTracks, user: req.session.user })
} catch (error) {
console.error('Error fetching recommendations:', error)
res.status(500).json({ error: 'An error occurred while fetching recommendations.' })
}
})
app.get('/new-recommendation', async (req, res) => {
try {
const seed_type = 'seed_tracks'
const topTracks = await getTopTracks(req)
const seed_uri = topTracks.map((track) => track.id)
// Fetch new recommendations if there are less than 5 left in the session batch
if (req.session.recommendationTracks.length < 5) {
await getRecommendations(req, seed_type, seed_uri)
}
// Get the first recommendation from the session and remove it from the session
const newRecommendation = req.session.recommendationTracks.shift()
console.log('recommendationTracks length:', req.session.recommendationTracks.length)
// Send the new recommendation to the frontend
res.json({ recommendation: newRecommendation })
} catch (error) {
console.error('Error fetching new recommendation:', error)
res.status(500).json({ error: 'An error occurred while fetching a new recommendation.' })
}
})
Wat er nu gebeurt, is dat we eerst 20 recommendations opvragen van de API, vervolgens in een while loop kijken we of de approvedRecommendations (die gefiltered zijn) groter is dan minBatchAmount (=10). Zo ja dan worden ze gestopt in req.session.recommendationTracks.
Vervolgens kan je swipen op de nummers en dan wordt er steeds uit deze array een nummer gehaald en naar de pagina gestuurd, wanneer de lengte van de req.session.recommendationTracks < 5, wordt er weer een nieuwe batch opgevraagd. Hiermee verminderen we het aantal request naar de API enorm!
- Ik heb mijzelf verdiept in de Spotify Web API, met name hoe de autorisatie werkt end ook een eerste duik in de verschillende endpoints.
- Voor dit onderzoek heb ik de Spotify developer documentatie gebruikt: https://developer.spotify.com/documentation/web-api
- Uit mijn onderzoek is gebleken dat spotify een zeer uitgebreide documentatie heeft, wat erg fijn was en mij enorm heeft geholpen met het uitvinden van hoe alles werkt. Daarnaast heb ik een werkend protoype kunnen maken waar de gebruiker zich verifiërd en vervolgens een lijst van aanbevolen nummers krijgen (voor nu in de console), die gebaseert is op de top 5 nummers van de gebruiker. Dit idee lijkt erg op ons concept voor de app, dus dit heeft veel waarde voor ons team, zodat we weten dat we verder kunnen gaan met dit concept. Link naar de demo GitHub repo: https://github.com/SF-Duijkersloot/spotify-api-test
Beschrijf het volgende:
- Voor dit onderzoek heb ik gekeken naar welke OAuth flow we moeten/het beste kunnen gebruiken. Aangezien ik hier een fout in had gemaakt bij mijn vorige onderzoek.
- Voor dit onderzoek heb ik de Spotify developer documentatie gebruikt: https://developer.spotify.com/documentation/web-api
-
- Bij mijn eerste onderzoek had ik de "Authorization code with PKCE" flow gebruikt. Maar die flow was blijkbaar bedoelt voor client-side code. Tijdens mijn onderzoek ben ik erachter gekomen dat we de "Authoruization flow" kunnen gebruiken en daar een server-side secret key te gebruiken. Daarnaast had ik een probleem met het laten zien van de scopes zodat gebruiker hierop kan agree-en. Dit kon ik oplossen door paramater: "show_dialog: true" mee te geven aan "https://accounts.spotify.com/authorize?"
- Ik kreeg een tijd de authorization niet werkend met de "Authorization flow" en had daarom de "Authorization flow PKCE" geprobeerd. deze werkte wel met voorbereide code van spotify, maar dit bleek toch niet de juiste en gepaste flow te zijn voor onze app.
- Wij gaan dus verder met de authorization flow aangezien we deze veilig werkend kunnen krijgen op onze server-side code en dit maakt de code voor het inloggen ook een stukje eenvoudiger dan met PKCE
Beschrijf het volgende:
- Op welke manieren heb je je prototype geevalueerd? Benoem gebruikte CMD methods
- Beschrijf de aanpak van je test / evaluatie
- Beschrijf de resultaten van je test / evaluatie
- Hoe ga je deze resultaten gebruiken om je prototype verder te verbeteren?