From 08796dc5e7711b00fe5df6de1eb0c8d1cc6a949b Mon Sep 17 00:00:00 2001 From: cjcocokrisp Date: Tue, 15 Apr 2025 20:29:29 -0400 Subject: [PATCH 1/5] feat: add endpoints for usage metrics for user testing --- .gitignore | 1 + src/api.ts | 6 ++- src/endpoints.ts | 129 +++++++++++++++++++++++++++++++++++++++++++++++ src/util.ts | 4 +- 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 35783cf..31209aa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ functions/ build/ # Local Netlify folder .netlify +data/ \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index d465171..17f8b20 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,7 @@ import express, { Router } from 'express' import serverless from 'serverless-http' import cors from 'cors'; -import { authCallback, authLogin, authRefresh, playlistBuild, playlistData, userData, userPlaylists, songPreview, songRemove, playlistCreate, songAdd } from './endpoints'; +import { authCallback, authLogin, authRefresh, playlistBuild, playlistData, userData, userPlaylists, songPreview, songRemove, playlistCreate, songAdd, metricsDecision, metricsElapsed, metricsInformation } from './endpoints'; const app = express(); const router = Router(); @@ -25,6 +25,10 @@ router.post('/playlist/add', songAdd); router.delete('/playlist/remove', songRemove); // Song related endpoints router.get('/song', songPreview); +// Testing Metrics endpoints +router.post('/metrics/information', metricsInformation); +router.post('/metrics/decision', metricsDecision); +router.post('/metrics/elapsed', metricsElapsed); // Netlify API setup app.use("/.netlify/functions/api", router); diff --git a/src/endpoints.ts b/src/endpoints.ts index 0684f4b..fdbf8be 100644 --- a/src/endpoints.ts +++ b/src/endpoints.ts @@ -2,6 +2,7 @@ import { ERROR_RESPONSES, generateRandomString, StatusCodes } from "./util"; import { config } from 'dotenv'; import { fetchPlaylist, fetchUserInfo, fetchUserPlaylists, buildPlaylist, removeSongsFromPlaylist, createPlaylist, addSongsToPlaylist } from "./spotify-interactions"; import { getSpotifyPreviewUrl } from "./spotify-preview"; +import * as fs from 'fs'; // Load .env with Spotify credentials & set constants for env secrets config(); @@ -9,6 +10,7 @@ const spotify_client_id = process.env.SPOTIFY_CLIENT_ID ?? ""; const spotify_client_secret = process.env.SPOTIFY_CLIENT_SECRET ?? ""; const redirect_uri = process.env.REDIRECT_URI_AUTH ?? ""; const redirect_home = process.env.REDIRECT_URI_HOME ?? ""; +const metrics_enabled = (process.env.METRICS_ENABLED ?? "") == "true" ? true : false; /* * Endpoint: /auth/login @@ -347,3 +349,130 @@ export async function songRemove(req: any, res: any) { res.status(status).json(data); } + +/* +* Endpoint: /playlist/information +* Description: Upload basic information for metrics +* +* Request: +* query_params: playlist_id, user id, username, playlist name +* +* Response: Playlist Snapshot ID +* +*/ +export async function metricsInformation(req: any, res: any) { + if (metrics_enabled) { + let playlist_id = req.query.playlist_id?.toString() ?? ""; + let user_id = req.query.user_id?.toString() ?? ""; + let username = req.query.username?.toString() ?? ""; + let playlist_name = req.query.playlist_name?.toString() ?? ""; + + if (playlist_id == "" || user_id == "" || username == "" || playlist_name == "") { + res.status(StatusCodes.BAD_REQUEST).json(ERROR_RESPONSES.MISSING_PARAM); + return; + } + + const text = `PLAYLIST_NAME=${playlist_name}\nUSER=${username}\n`; + + fs.writeFile(`./data/${user_id}_${playlist_id}_info.txt`, text, (err) => { + const headers = `song_id,song_name,time_sec,direction\n`; + fs.writeFile(`./data/${user_id}_${playlist_id}_actions.txt`, headers, (err) => { + if (err) { + console.log(err); + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({'error': 'file writing error'}); + } else { + res.status(StatusCodes.OK).json({'status': 'success'}); + } + }) + + }); + + } else { + res.status(StatusCodes.BAD_REQUEST).json({ 'error': ERROR_RESPONSES.METRICS_NOT_ENABLED }); + } + +} + +/* +* Endpoint: /metrics/decision +* Description: Uploads a decision for the plays +* +* Request: +* query_params: playlist_id, user id +* body: song_id, song name, swipe_time (in seconds), direction +* +* Response: Nothing just status if success or not +* +*/ +export async function metricsDecision(req: any, res: any) { + let data = undefined; + let status = StatusCodes.OK; + + if (metrics_enabled) { + let playlist_id = req.query.playlist_id?.toString() ?? ""; + let user_id = req.query.user_id?.toString() ?? ""; + let song_id = req.query.song_id?.toString() ?? ""; + let song_name = req.query.song_name?.toString() ?? ""; + let swipe_time = req.query.swipe_time?.toString() ?? ""; + let direction = req.query.direction?.toString() ?? ""; + + if (playlist_id == "" || user_id == "" || song_id == "" || song_name == "" || swipe_time == "" || direction == "") { + res.status(StatusCodes.BAD_REQUEST).json({ 'error': ERROR_RESPONSES.MISSING_PARAM }); + return; + } + + const line = `${song_id},${song_name},${swipe_time},${direction}\n`; + fs.appendFile(`./data/${user_id}_${playlist_id}_actions.txt`, line, (err) => { + if (err) { + console.log(err); + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({'error': 'file writing error'}); + } else { + res.status(StatusCodes.OK).json({'status': 'success'}); + } + }) + + } else { + res.status(StatusCodes.BAD_REQUEST).json({ 'error': ERROR_RESPONSES.METRICS_NOT_ENABLED }); + } + +} + +/* +* Endpoint: /metrics/elapsed +* Description: Upload total elapsed time it took to go through a playlist +* +* Request: +* query_params: playlist_id, user id, total time (in minutes) +* +* Response: Nothing just status if success or not +* +*/ +export async function metricsElapsed(req: any, res: any) { + let data = undefined; + let status = StatusCodes.OK; + + if (metrics_enabled) { + let playlist_id = req.query.playlist_id?.toString() ?? ""; + let user_id = req.query.user_id?.toString() ?? ""; + let total_time = req.query.total_time?.toString() ?? ""; + + if (playlist_id == "" || user_id == "" || total_time == "") { + res.status(StatusCodes.BAD_REQUEST).json(ERROR_RESPONSES.MISSING_PARAM); + return; + } + + const text = `TOTAL_TIME=${total_time}`; + fs.appendFile(`./data/${user_id}_${playlist_id}_info.txt`, text, (err) => { + if (err) { + console.log(err); + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({'error': 'file writing error'}); + } else { + res.status(StatusCodes.OK).json({'status': 'success'}); + } + }) + + } else { + res.status(StatusCodes.BAD_REQUEST).json({ 'error': ERROR_RESPONSES.METRICS_NOT_ENABLED }); + } + +} diff --git a/src/util.ts b/src/util.ts index f7129c3..d635cea 100644 --- a/src/util.ts +++ b/src/util.ts @@ -18,7 +18,9 @@ export const ERROR_RESPONSES = { 'NOT_FOUND': { 'error': 'resource not found' }, 'UNHANDLED': { 'error': 'unhandled response code' }, 'REFRESH_ERROR': { 'error' : 'failed to refresh access token' }, - 'RATE_LIMIT_EXCEED': { 'error' : 'application rate limit exceed' } + 'RATE_LIMIT_EXCEED': { 'error' : 'application rate limit exceed' }, + 'METRICS_NOT_ENABLED': { 'error' : 'metrics are not enabled' }, + 'MISSING_PARAM': {'error' : 'missing parameter'}, } /* From 97c9bd18985907c8217d9e488b4717e8aa538923 Mon Sep 17 00:00:00 2001 From: cjcocokrisp Date: Tue, 15 Apr 2025 22:25:38 -0400 Subject: [PATCH 2/5] feat: add frontend data sending --- dist/cards.js | 45 ++++++++++++++++++++++++++++++++++++++++++--- dist/playlists.js | 11 +++++++++++ dist/util.js | 31 +++++++++++++++++++++++++++++++ src/api.ts | 3 ++- src/endpoints.ts | 24 ++++++++++++++---------- 5 files changed, 100 insertions(+), 14 deletions(-) diff --git a/dist/cards.js b/dist/cards.js index c2f9ee2..9075e5b 100644 --- a/dist/cards.js +++ b/dist/cards.js @@ -1,3 +1,4 @@ + $(document).ready(async function () { // MAKE BUTTON PRETTY // alert("Welcome to the SongSwipe demo!\n\nShown here is the swiping interface loaded with a existing Spotify playlist.\n\nSwipe right on songs you like\nSwipe left on ones you don't!") @@ -14,6 +15,15 @@ $(document).ready(async function () { headers.set('Authorization', access_token); headers.set('Access-Control-Allow-Origin', '*'); + // Check if metrics are enabled and set a boolean to not do the stuff if that aren't enabled + let enabledUrl = new URL(`${API_URI}/metrics/enabled`); + const enabled_request = new Request(enabledUrl.toString(), { + method: 'GET', + }); + const enabled_response = await fetch(enabled_request); + const metrics_enabled = (await enabled_response.json()).enabled; + song_metrics = []; + // Hide Overlay Button until all the songs are loaded const closeButton = document.getElementById('close-overlay'); closeButton.classList.add('hidden'); @@ -266,6 +276,9 @@ $(document).ready(async function () { songIndex += 1; updateSongCard(songIndex, "last_song_card"); + let total_time = getSecondsSinceEpoch(); + let song_time = getSecondsSinceEpoch(); + // Play/pause toggle button $(".song_button").click(function() { // Make sure we have a song player @@ -383,7 +396,7 @@ async function getUserId() { const overlay = document.getElementById('overlay'); // Close overlay when button is clicked -closeButton.addEventListener('click', function() { +closeButton.addEventListener('click', async function() { overlay.classList.add('hidden'); // Starts playing by default // Plays song at the start of the tracklist @@ -489,7 +502,7 @@ closeButton.addEventListener('click', function() { }); // While finger is moving... - $("#app_container").on("touchmove", "#song_card", function (event) { + $("#app_container").on("touchmove", "#song_card", async function (event) { if (tracking) { swipe_details = computeSwipeDetails(event); @@ -506,6 +519,7 @@ closeButton.addEventListener('click', function() { card.style.transform = `translateX(${translateX}px) rotate(${rotateDeg}deg)`; // If the song has been completed_swipe enough to declare it left or right swipe + let swipe_time = undefined; if (swipe_details.distance > DISTANCE_TO_SWIPE) { let track_id = songs[track_index].track_id; if (swipe_details.direction === -1) { @@ -517,6 +531,13 @@ closeButton.addEventListener('click', function() { // 'album_cover': songs[track_index].album_cover_img_url, // }); save(playlist_id, save_state, user_id) + swipe_time = getSecondsSinceEpoch() - song_time; + song_metrics.push({ + 'track_id': track_id, + 'song_name': songs[track_index].name, + 'swipe_time': swipe_time, + 'direction': 'left' + }); } else if (swipe_details.direction === 1) { console.log(songs[track_index]); save_state = saveTrack(save_state, 'right', track_id, track_index, songs); @@ -526,6 +547,14 @@ closeButton.addEventListener('click', function() { // 'album_cover': songs[track_index].album_cover_img_url, // }); save(playlist_id, save_state, user_id); + swipe_time = getSecondsSinceEpoch() - song_time; + song_metrics.push({ + 'track_id': track_id, + 'song_name': songs[track_index].name, + 'swipe_time': swipe_time, + 'direction': 'right' + }); + //if (metrics_enabled) await sendTrackTime(playlist_id, user_id, track_id, songs[track_index].name, swipe_time, 'right'); } // Plays new song after swipe track_index += 1; @@ -536,6 +565,7 @@ closeButton.addEventListener('click', function() { tracking = false; completed_swipe = true; + song_time = getSecondsSinceEpoch(); // Add transition for smooth animation $("#song_card").css({ @@ -552,7 +582,7 @@ closeButton.addEventListener('click', function() { }); // Once front song is fully out of frame - $("#song_card").one('transitionend', function () { + $("#song_card").one('transitionend', async function () { // Variables for readability let $next = $("#next_song_card"); let $current = $("#song_card"); @@ -597,6 +627,15 @@ closeButton.addEventListener('click', function() { params.set('user_id', user_id); params.set('playlist_id', playlist_id); + if (metrics_enabled) { + song_metrics.forEach(async (element) => { + await sendTrackTime(playlist_id, user_id, element['track_id'], element['song_name'], element['swipe_time'], element['direction']); + }); + + let completion_time = getSecondsSinceEpoch() - total_time; + sendElapsedTime(playlist_id, user_id, completion_time); + } + window.location.href = window.location.pathname.replace('cards', 'stagingarea') + `?${params.toString()}`; } updateSongCard(songIndex, "last_song_card"); diff --git a/dist/playlists.js b/dist/playlists.js index 8a1c148..b979d65 100644 --- a/dist/playlists.js +++ b/dist/playlists.js @@ -49,6 +49,17 @@ $(document).ready(async function () { // Redirect to cards page card.addEventListener('click', async () => { + // Send request ot create files for metric collection will not do anything if metrics aren't enabled + let metrics_params = new URLSearchParams(); + metrics_params.set('playlist_id', playlist.id); + metrics_params.set('user_id', data_user.id); + metrics_params.set('username', data_user.display_name); + metrics_params.set('playlist_name', playlist.name); + const information_metric_request = new Request(`${API_URI}/metrics/information?${metrics_params.toString()}`, { + method: 'POST', + }); + await fetch(information_metric_request); + let params = new URLSearchParams(); params.set('playlist_id', playlist.id); diff --git a/dist/util.js b/dist/util.js index a35f3ec..634836d 100644 --- a/dist/util.js +++ b/dist/util.js @@ -134,4 +134,35 @@ function moveTrack(target, source, destination, trackId) { alert(`Track with ID ${trackId} not found in ${array_source}`); } return target; +} + +async function sendTrackTime(playlist_id, user_id, track_id, track_name, swipe_time, direction) { + let params = new URLSearchParams(); + params.set('playlist_id', playlist_id); + params.set('user_id', user_id); + params.set('song_id', track_id); + params.set('song_name', track_name); + params.set('swipe_time', swipe_time); + params.set('direction', direction); + + const request = new Request(`${API_URI}/metrics/decision?${params.toString()}`, { + method: 'POST', + }) + + const response = await fetch(request); + response.status; +} + +async function sendElapsedTime(playlist_id, user_id, total_time) { + let params = new URLSearchParams(); + params.set('playlist_id', playlist_id); + params.set('user_id', user_id); + params.set('total_time', total_time); + + const request = new Request(`${API_URI}/metrics/elapsed?${params.toString()}`, { + method: 'POST', + }) + + const response = await fetch(request); + response.status; } \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 17f8b20..0673a5a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,7 @@ import express, { Router } from 'express' import serverless from 'serverless-http' import cors from 'cors'; -import { authCallback, authLogin, authRefresh, playlistBuild, playlistData, userData, userPlaylists, songPreview, songRemove, playlistCreate, songAdd, metricsDecision, metricsElapsed, metricsInformation } from './endpoints'; +import { authCallback, authLogin, authRefresh, playlistBuild, playlistData, userData, userPlaylists, songPreview, songRemove, playlistCreate, songAdd, metricsDecision, metricsElapsed, metricsInformation, metricsEnabled } from './endpoints'; const app = express(); const router = Router(); @@ -26,6 +26,7 @@ router.delete('/playlist/remove', songRemove); // Song related endpoints router.get('/song', songPreview); // Testing Metrics endpoints +router.get('/metrics/enabled', metricsEnabled); router.post('/metrics/information', metricsInformation); router.post('/metrics/decision', metricsDecision); router.post('/metrics/elapsed', metricsElapsed); diff --git a/src/endpoints.ts b/src/endpoints.ts index fdbf8be..b6ce50e 100644 --- a/src/endpoints.ts +++ b/src/endpoints.ts @@ -376,7 +376,7 @@ export async function metricsInformation(req: any, res: any) { fs.writeFile(`./data/${user_id}_${playlist_id}_info.txt`, text, (err) => { const headers = `song_id,song_name,time_sec,direction\n`; - fs.writeFile(`./data/${user_id}_${playlist_id}_actions.txt`, headers, (err) => { + fs.writeFile(`./data/${user_id}_${playlist_id}_actions.csv`, headers, (err) => { if (err) { console.log(err); res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({'error': 'file writing error'}); @@ -393,6 +393,18 @@ export async function metricsInformation(req: any, res: any) { } +/* +* Endpoint: /metrics/enabled +* Description: returns whether metrics are enabled or not +* +* Response: a json with the boolean +* +*/ +export async function metricsEnabled(req: any, res: any) { + let data = { 'enabled': metrics_enabled}; + res.status(StatusCodes.OK).json(data); +} + /* * Endpoint: /metrics/decision * Description: Uploads a decision for the plays @@ -405,9 +417,6 @@ export async function metricsInformation(req: any, res: any) { * */ export async function metricsDecision(req: any, res: any) { - let data = undefined; - let status = StatusCodes.OK; - if (metrics_enabled) { let playlist_id = req.query.playlist_id?.toString() ?? ""; let user_id = req.query.user_id?.toString() ?? ""; @@ -422,7 +431,7 @@ export async function metricsDecision(req: any, res: any) { } const line = `${song_id},${song_name},${swipe_time},${direction}\n`; - fs.appendFile(`./data/${user_id}_${playlist_id}_actions.txt`, line, (err) => { + fs.appendFile(`./data/${user_id}_${playlist_id}_actions.csv`, line, (err) => { if (err) { console.log(err); res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({'error': 'file writing error'}); @@ -434,7 +443,6 @@ export async function metricsDecision(req: any, res: any) { } else { res.status(StatusCodes.BAD_REQUEST).json({ 'error': ERROR_RESPONSES.METRICS_NOT_ENABLED }); } - } /* @@ -448,9 +456,6 @@ export async function metricsDecision(req: any, res: any) { * */ export async function metricsElapsed(req: any, res: any) { - let data = undefined; - let status = StatusCodes.OK; - if (metrics_enabled) { let playlist_id = req.query.playlist_id?.toString() ?? ""; let user_id = req.query.user_id?.toString() ?? ""; @@ -474,5 +479,4 @@ export async function metricsElapsed(req: any, res: any) { } else { res.status(StatusCodes.BAD_REQUEST).json({ 'error': ERROR_RESPONSES.METRICS_NOT_ENABLED }); } - } From 81363cd0ac3920e99cab29d5ef6ddf6f1df6e4de Mon Sep 17 00:00:00 2001 From: cjcocokrisp Date: Wed, 16 Apr 2025 10:46:02 -0400 Subject: [PATCH 3/5] chore: update README and change total time to be minutes --- .gitignore | 3 +-- README.md | 12 ++++++++---- data/IF_METRICS_ENABLED_DATA_WILL_BE_STORED_HERE | 0 dist/cards.js | 2 +- dist/util.js | 2 ++ 5 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 data/IF_METRICS_ENABLED_DATA_WILL_BE_STORED_HERE diff --git a/.gitignore b/.gitignore index 31209aa..0608340 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ node_modules/ functions/ build/ # Local Netlify folder -.netlify -data/ \ No newline at end of file +.netlify \ No newline at end of file diff --git a/README.md b/README.md index d3f3aa0..c248a59 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,9 @@ SPOTIFY_CLIENT_ID= SPOTIFY_CLIENT_SECRET= REDIRECT_URI_AUTH=http://127.0.0.1:9000/.netlify/functions/api/auth/callback REDIRECT_URI_HOME=http://127.0.0.1:8080/playlists.html +METRICS_ENABLED= ``` -You may also need to edit the sixth line of `dist/playlist.js` if you change the port that the backend api runs off of. +You may also need to edit the sixth line of `dist/util.js` if you change the port that the backend api runs off of. At the current moment our application requires a server that is distributing the `dist` directory of the project to be able to function. This is to emulate what is eventually going to be hosted on netlify. Our dependencies include the `http-server` node module that can be used for this however, if it does not work you can install it with the following command. ``` @@ -65,6 +66,10 @@ npm install http-server ``` Editors like Visual Studio Code also have extensions that can provide a live http-server. If you decide to use something like that instead make sure you update the URIs accordingly. We will attempt to make this a more streamlined thing for running locally as the project progresses. +If metrics are enabled then on completion of going through a playlist the following metrics will be written to a file in the `data` directory. +- Per song: The total amount of time spent on the song, direction swiped, and song information. +- Per playlist: The total amount of time it took to go through the playlist. + ### Running Locally Once dependencies are installed and the `.env` file iat the moment as well due to the URI for API calls being hardcoded at the moment. s created and populated you can run the application with either of the following commands. @@ -72,10 +77,9 @@ Once dependencies are installed and the `.env` file iat the moment as well due t npm start (will just run the backend express app, use if you plan to use your own http server to distribute the frontend) npm run dev (will launch both a server to distribute the frontend and execute the backend) ``` -At the current moment we do not have a landing page set up so to check out what we have for the application you can go to either of these urls. Please note port `8080` is the default port for `http-server` and port `9000` is the default port for the express backend so if you decide to change them make sure you update the urls accordingly. +The below URL will bring you to the landing page for the application. Please note port `8080` is the default port for `http-server` and port `9000` is the default port for the express backend so if you decide to change them make sure you update the urls accordingly. ``` -http://127.0.0.1:8080/index.html (song swiping demo, mobile only) -http://127.0.0.1:9000/.netlify/functions/api/auth/login (will ask you to sign in then redirect you to the work in progres playlist selection screen) +http://127.0.0.1:8080/index.html ``` Our unit tests can be ran with the following command. diff --git a/data/IF_METRICS_ENABLED_DATA_WILL_BE_STORED_HERE b/data/IF_METRICS_ENABLED_DATA_WILL_BE_STORED_HERE new file mode 100644 index 0000000..e69de29 diff --git a/dist/cards.js b/dist/cards.js index 9075e5b..37e0dd0 100644 --- a/dist/cards.js +++ b/dist/cards.js @@ -632,7 +632,7 @@ closeButton.addEventListener('click', async function() { await sendTrackTime(playlist_id, user_id, element['track_id'], element['song_name'], element['swipe_time'], element['direction']); }); - let completion_time = getSecondsSinceEpoch() - total_time; + let completion_time = (getSecondsSinceEpoch() - total_time) / SEC_PER_MIN; sendElapsedTime(playlist_id, user_id, completion_time); } diff --git a/dist/util.js b/dist/util.js index 634836d..103b8d6 100644 --- a/dist/util.js +++ b/dist/util.js @@ -8,6 +8,8 @@ const API_URI = function() { return uri + '/.netlify/functions/api'; }(); +const SEC_PER_MIN = 60; + const getSecondsSinceEpoch = () => Math.floor(Date.now() / 1000); /* From fe43f1769b193b304ceb6b5dc833ab5f6385a77c Mon Sep 17 00:00:00 2001 From: cjcocokrisp Date: Wed, 16 Apr 2025 10:46:58 -0400 Subject: [PATCH 4/5] chore: add 'data/' directory back into .gitignore after getting it there so user does not need to create it --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0608340..31209aa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules/ functions/ build/ # Local Netlify folder -.netlify \ No newline at end of file +.netlify +data/ \ No newline at end of file From 0c341454f670736c2b87f882a2296ffaa7890ac7 Mon Sep 17 00:00:00 2001 From: cjcocokrisp Date: Wed, 16 Apr 2025 13:53:42 -0400 Subject: [PATCH 5/5] feat: update metrics stuff --- .gitignore | 3 +- .vscode/settings.json | 3 ++ dist/cards.html | 108 ++++++++++++++++++++---------------------- dist/cards.js | 22 +++++---- dist/util.js | 6 ++- src/endpoints.ts | 10 ++-- 6 files changed, 79 insertions(+), 73 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 31209aa..39ba9cf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ functions/ build/ # Local Netlify folder .netlify -data/ \ No newline at end of file +data/ +.venv/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3e7bb4e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.ignoreFiles":["data/*"], +} \ No newline at end of file diff --git a/dist/cards.html b/dist/cards.html index 3a5ad45..6635c14 100644 --- a/dist/cards.html +++ b/dist/cards.html @@ -1,7 +1,6 @@ - - + SongSwipe @@ -13,29 +12,27 @@ - + - - + +
-
-
-
-
- Loading... -
+
+
+
+ Loading...
-
- +
+
+

@@ -43,57 +40,54 @@
- + +
+
+
+ +
+
+ Current playlist:
+ Loading...
- -
- -
-
- Current playlist:
- Loading... +
+ +
+
+
+
-
-
- -
-
-
- -
-
-
+
-
- -
-
-
- -
-
-
+
+
+ +
+
+
+
-
-
- - -
- - +
+
+ +
+
+ + +
- - + \ No newline at end of file diff --git a/dist/cards.js b/dist/cards.js index 37e0dd0..8947e0f 100644 --- a/dist/cards.js +++ b/dist/cards.js @@ -1,3 +1,5 @@ +let total_time = undefined; +let song_time = undefined; $(document).ready(async function () { // MAKE BUTTON PRETTY @@ -276,9 +278,6 @@ $(document).ready(async function () { songIndex += 1; updateSongCard(songIndex, "last_song_card"); - let total_time = getSecondsSinceEpoch(); - let song_time = getSecondsSinceEpoch(); - // Play/pause toggle button $(".song_button").click(function() { // Make sure we have a song player @@ -398,6 +397,8 @@ const overlay = document.getElementById('overlay'); // Close overlay when button is clicked closeButton.addEventListener('click', async function() { overlay.classList.add('hidden'); + total_time = getSecondsSinceEpoch(); + song_time = getSecondsSinceEpoch(); // Starts playing by default // Plays song at the start of the tracklist songPlayer(track_index); @@ -538,6 +539,8 @@ closeButton.addEventListener('click', async function() { 'swipe_time': swipe_time, 'direction': 'left' }); + if (metrics_enabled) sendTrackTime(playlist_id, user_id, track_id, songs[track_index].name, songs[track_index].artists[0], + songs[track_index].album_name, swipe_time, 'left'); } else if (swipe_details.direction === 1) { console.log(songs[track_index]); save_state = saveTrack(save_state, 'right', track_id, track_index, songs); @@ -554,7 +557,8 @@ closeButton.addEventListener('click', async function() { 'swipe_time': swipe_time, 'direction': 'right' }); - //if (metrics_enabled) await sendTrackTime(playlist_id, user_id, track_id, songs[track_index].name, swipe_time, 'right'); + if (metrics_enabled) sendTrackTime(playlist_id, user_id, track_id, songs[track_index].name, songs[track_index].artists[0], + songs[track_index].album_name, swipe_time, 'right'); } // Plays new song after swipe track_index += 1; @@ -602,7 +606,7 @@ closeButton.addEventListener('click', async function() { $last.addClass("next_song_card").removeClass("last_song_card"); // Wait for the transition to finish, then remove transition property - $last.one("transitionend", function () { + $last.one("transitionend", async function () { // Remove all transition properties to start with clean slate for future swipes $next.css("transition", ""); $current.css("transition", ""); @@ -628,12 +632,12 @@ closeButton.addEventListener('click', async function() { params.set('playlist_id', playlist_id); if (metrics_enabled) { - song_metrics.forEach(async (element) => { - await sendTrackTime(playlist_id, user_id, element['track_id'], element['song_name'], element['swipe_time'], element['direction']); - }); + // song_metrics.forEach(async (element) => { + // await sendTrackTime(playlist_id, user_id, element['track_id'], element['song_name'], element['swipe_time'], element['direction']); + // }); let completion_time = (getSecondsSinceEpoch() - total_time) / SEC_PER_MIN; - sendElapsedTime(playlist_id, user_id, completion_time); + await sendElapsedTime(playlist_id, user_id, completion_time); } window.location.href = window.location.pathname.replace('cards', 'stagingarea') + `?${params.toString()}`; diff --git a/dist/util.js b/dist/util.js index 103b8d6..77b9b76 100644 --- a/dist/util.js +++ b/dist/util.js @@ -138,12 +138,14 @@ function moveTrack(target, source, destination, trackId) { return target; } -async function sendTrackTime(playlist_id, user_id, track_id, track_name, swipe_time, direction) { +async function sendTrackTime(playlist_id, user_id, track_id, track_name, track_artists, track_album, swipe_time, direction) { let params = new URLSearchParams(); params.set('playlist_id', playlist_id); params.set('user_id', user_id); params.set('song_id', track_id); - params.set('song_name', track_name); + params.set('song_artists', track_artists.replaceAll(",", "")); + params.set('song_album', track_album.replaceAll(",", "")); + params.set('song_name', track_name.replaceAll(",", "")); params.set('swipe_time', swipe_time); params.set('direction', direction); diff --git a/src/endpoints.ts b/src/endpoints.ts index b6ce50e..a74fb8c 100644 --- a/src/endpoints.ts +++ b/src/endpoints.ts @@ -375,7 +375,7 @@ export async function metricsInformation(req: any, res: any) { const text = `PLAYLIST_NAME=${playlist_name}\nUSER=${username}\n`; fs.writeFile(`./data/${user_id}_${playlist_id}_info.txt`, text, (err) => { - const headers = `song_id,song_name,time_sec,direction\n`; + const headers = `id,name,artists,album,time(Sec),direction\n`; fs.writeFile(`./data/${user_id}_${playlist_id}_actions.csv`, headers, (err) => { if (err) { console.log(err); @@ -411,7 +411,7 @@ export async function metricsEnabled(req: any, res: any) { * * Request: * query_params: playlist_id, user id -* body: song_id, song name, swipe_time (in seconds), direction +* body: song_id, song name, song artists, song album, swipe_time (in seconds), direction * * Response: Nothing just status if success or not * @@ -422,15 +422,17 @@ export async function metricsDecision(req: any, res: any) { let user_id = req.query.user_id?.toString() ?? ""; let song_id = req.query.song_id?.toString() ?? ""; let song_name = req.query.song_name?.toString() ?? ""; + let song_artist = req.query.song_artists?.toString() ?? ""; + let song_album = req.query.song_album?.toString() ?? ""; let swipe_time = req.query.swipe_time?.toString() ?? ""; let direction = req.query.direction?.toString() ?? ""; - if (playlist_id == "" || user_id == "" || song_id == "" || song_name == "" || swipe_time == "" || direction == "") { + if (playlist_id == "" || user_id == "" || song_id == "" || song_name == "" || song_artist == "" || song_album == "" || swipe_time == "" || direction == "") { res.status(StatusCodes.BAD_REQUEST).json({ 'error': ERROR_RESPONSES.MISSING_PARAM }); return; } - const line = `${song_id},${song_name},${swipe_time},${direction}\n`; + const line = `${song_id},${song_name},${song_artist},${song_album},${swipe_time},${direction}\n`; fs.appendFile(`./data/${user_id}_${playlist_id}_actions.csv`, line, (err) => { if (err) { console.log(err);