Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ build/
# Local Netlify folder
.netlify
data/
.venv/
.venv/
figs/
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@ It allows for a simple Tinder-inspired swipe left-or-right motion to decide whe
<img src="dist/assets/img/screenshot.png" alt="Screenshot" width="300">
</p>

## Feature Plans
## Features
### Core Features:
- Swiping mechanic
- Staging area for left and right swipes
- Options to remove from playlist, move to a new playlist, etc. for staging areas
- Preview tracks playing automatically
- Song, album, artist info display
- Playlist discoverer from other people to copy, filter and save other peoples playlists

### Stretch Features:
### Potential Future Features:
- YouTube API translation layer
- Desktop version (Shouldn't be too hard)
- Playlist discoverer from other people to copy, filter and save other peoples playlists
- Pretty animations would be nice
- Undo button
- **Settings Options:**
- Light / Dark Mode / Themes
Expand Down Expand Up @@ -55,7 +54,7 @@ You need to provide a `.env` file with some information in order for the express
SPOTIFY_CLIENT_ID=<YOUR SPOTIFY CLIENT ID>
SPOTIFY_CLIENT_SECRET=<YOUR SPOTIFY 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
REDIRECT_URI_HOME=http://127.0.0.1:8080/index.html
METRICS_ENABLED=<true or false>
```
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.
Expand All @@ -76,6 +75,7 @@ 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)
npm run windows (basically npm run dev for windows cause windows is quirky!)
```
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.
```
Expand Down Expand Up @@ -114,12 +114,25 @@ If there are any future plans for the future of the project, write and note them


## Version History
- 0.0
- We have nothing
### v1.0
- Playlist selection from user's playlists from Spotify.
- Card swiping features to manage playlists.
- Swiping left stages a song to be removed.
- Swiping right stages a song to be kept.
- During management song preview is played.
- Staging area where you can review changes to be made.
- Write changes to playlist to Spotify.
- Remove disliked songs.
- Create new playlist with liked songs.
- Import another Spotify user's public playlists.

## License

© 2025 Christopher Coco, Raj Ray, Anthony Simao, Katiana Sourn, Nena Heng
All rights reserved.

## Acknowledgments

Dr. Daly for offering this course and providing insight and feedback throughout the semester.

Our peers who reviewed our progress throughout the project.
286 changes: 286 additions & 0 deletions analytics.ipynb

Large diffs are not rendered by default.

Binary file added dist/assets/img/back.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added dist/assets/img/pause.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added dist/assets/img/play.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added dist/assets/img/question.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added dist/assets/img/restart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added dist/assets/img/stage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions dist/cards.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,16 @@
</div>
</div>
</div>
<div id = "question">
<div class = "question_content">
<span class="bold_title" id="question_text">Display_Q/A</span>
<button id="close-question">Close</button>
</div>
</div>
<div class="mobile-wrapper">
<div id="app_container">
<button class="back_button" title="Go Back"></button>
<button class="question_button" title="Question"></button>
<div id="header_container">
<div id="playlist_title">
<span class="subtitle">Current playlist:</span><br>
Expand Down Expand Up @@ -86,9 +93,17 @@
</div>
</div>
</div>
<div class="slider-container">
<div class="slider-track">
<div class="slider-progress" id="progress"></div>
<div class="slider-handle" id="handle"></div>
</div>
<div class="slider-value" id="value">55</div>
</div>
<div class="button_container">
<button class="song_button"></button>
<button class="song_restart"></button>
<button class="stage_area"></button>
</div>
</div>
</div>
Expand Down
80 changes: 77 additions & 3 deletions dist/cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ $(document).ready(async function () {
closeButton.classList.add('hidden');
// Overlay Variables
overlay_playlist_title = document.getElementById('loading_playlist_title_variable');

// Song_player object
// Empty song file instead of null
let song_player = new Audio("https://bigsoundbank.com/UPLOAD/mp3/0917.mp3");
Expand Down Expand Up @@ -192,7 +193,7 @@ $(document).ready(async function () {
}

// Refactor to play songs
function playSong() {
window.playSong = function() {
song_player.play()
.then(() => {
$(".song_button").addClass('playing');
Expand Down Expand Up @@ -289,19 +290,31 @@ $(document).ready(async function () {
// Toggle play state
if (isPlaying) {
// Currently playing, so pause
song_player.pause();
notPlaying();
pauseSong();
} else {
// Currently paused, so play
playSong();
}
});

window.pauseSong = function() {
// Currently playing, so pause
song_player.pause();
notPlaying();
}

$(".back_button").click(function () {
save(playlist_id, save_state, user_id);
window.location.href = "playlists.html";
});

$(".stage_area").click(function () {
let params = new URLSearchParams();
params.set('user_id', user_id);
params.set('playlist_id', playlist_id);
window.location.href = window.location.pathname.replace('cards', 'stagingarea') + `?${params.toString()}`;
});

// Restart Song Button aka start from 0
$(".song_restart").click(function () {
// Make sure we have a song player
Expand Down Expand Up @@ -410,7 +423,62 @@ $(document).ready(async function () {
// location.reload();
// });

// Volume Button functionality
const track = document.querySelector('.slider-track');
const progress = document.getElementById('progress');
const handle = document.getElementById('handle');
const valueDisplay = document.getElementById('value');
let isDragging = false;

// Initialize with 55
updateSlider(55);

// Handle mouse/touch events
handle.addEventListener('mousedown', startDrag);
handle.addEventListener('touchstart', startDrag, { passive: true });

document.addEventListener('mousemove', drag);
document.addEventListener('touchmove', drag, { passive: false });

document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);

// Allow clicking on track to set value
track.addEventListener('click', function(e) {
const rect = track.getBoundingClientRect();
const percent = Math.min(Math.max(0, ((e.clientX - rect.left) / rect.width) * 100), 100);
updateSlider(Math.round(percent));
});

function startDrag(e) {
isDragging = true;
handle.style.transition = 'none';
progress.style.transition = 'none';
}

function drag(e) {
if (!isDragging) return;
e.preventDefault();
const rect = track.getBoundingClientRect();
const clientX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
const percent = Math.min(Math.max(0, ((clientX - rect.left) / rect.width) * 100), 100);
updateSlider(Math.round(percent));
}

function endDrag() {
if (!isDragging) return;
isDragging = false;
handle.style.transition = 'left 0.1s ease';
progress.style.transition = 'width 0.1s ease';
}

function updateSlider(value) {
handle.style.left = value + '%';
progress.style.width = value + '%';
valueDisplay.textContent = value;
// Adjust volume
song_player.volume = value / 100;
}
//! Swiping action listener and logic
const SWIPE_SENSITIVITY = window.innerWidth / 1; // Animation sensitivity
const wrapper = document.querySelector('.mobile-wrapper');
Expand Down Expand Up @@ -713,15 +781,21 @@ function simulateSwipe(direction) {

// Process the swipe action (save track etc.)
let track_id = songs[track_index].track_id;
let swipe_time = getSecondsSinceEpoch() - song_time;
if (direction === -1) {
console.log(songs[track_index]);
save_state = saveTrack(save_state, 'left', track_id, track_index, songs);
save(playlist_id, save_state, user_id);
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 (direction === 1) {
console.log(songs[track_index]);
save_state = saveTrack(save_state, 'right', track_id, track_index, songs);
save(playlist_id, save_state, user_id);
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');
}
song_time = getSecondsSinceEpoch();

// Play next song
track_index += 1;
Expand Down
1 change: 1 addition & 0 deletions dist/playlists.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
</head>


<body style="overflow: scroll;">
<div class="mobile-wrapper">
<div id="app_container">
Expand Down
8 changes: 6 additions & 2 deletions dist/stagingarea.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,18 @@

<body style="overflow: scroll;">
<div class="mobile-wrapper">
<div id="header_container">
<div id="header_container_staging">
<div id="stage_title">
<span class="subtitle">Staging Area</span><br>
<div class="toggle-container">
<div class="toggle-option action" id="removeAction">Remove Songs</div>
<div class="toggle-option action" id="createAction">Create New Playlist</div>
</div>
<div class="toggle-container">
<div class="toggle-option selected" id="removeOption">Songs to Be Removed</div>
<div class="toggle-option unselected" id="keepOption">Songs to Keep</div>
</div>
</div>
</div>
</div>
<div id="stageContainer">
</div>
Expand Down
100 changes: 100 additions & 0 deletions dist/stagingarea.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,106 @@ $(document).ready(async function () {
Create a staging card for a song
Params - name {string}, artists {string}, imgUrl {string}
*/

// Remove songs from a playlist
let removeAction = document.getElementById('removeAction');
removeAction.addEventListener('click', async function() {
let data = JSON.parse(localStorage.getItem(user_id));
let songs = Object.keys(data[playlist_id]['left_tracks']);

let removeSongs = confirm(`Are you sure you want to remove ${songs.length} songs?`);
if (removeSongs) {
let access_token = localStorage.getItem('access_token');
if (checkAccessTokenExpiration()) access_token = refreshAccessToken();
if (access_token == null) renderError('Error refreshing access token.');

let params = new URLSearchParams();
params.set('playlist_id', playlist_id);

let data = { to_remove: songs };
let res = await fetch(`${API_URI}/playlist/remove?${params.toString()}`, {
method: 'DELETE',
headers: {
'Authorization': access_token,
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});

if (res.status != 200) {
alert('Error removing songs');
} else {
alert('Songs successfully removed!');
}

}

data[playlist_id]['left_tracks'] = {};
save_state = data[playlist_id];
localStorage.setItem(user_id, JSON.stringify(data));
while (container.firstChild) {
container.removeChild(container.firstChild);
}
generateCard('left_tracks');
});


let createAction = document.getElementById('createAction');
createAction.addEventListener('click', async function() {
let data = JSON.parse(localStorage.getItem(user_id));
let songs = Object.keys(data[playlist_id]['right_tracks']);

let playlistName = prompt(`Enter a name for a new playlist with ${songs.length} songs:`);
if (playlistName != "" && playlistName != null) {
let access_token = localStorage.getItem('access_token');
if (checkAccessTokenExpiration()) access_token = refreshAccessToken();
if (access_token == null) renderError('Error refreshing access token.');

let data = {
name: playlistName,
description: 'Created with filtered songs from SongSwipe! https://github.com/ListenToAJ/SongSwipe',
is_public: true,
};

let createRes = await fetch(`${API_URI}/playlist/create`, {
method: 'POST',
headers: {
'Authorization': access_token,
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});

if (createRes.status != 201) {
alert('Error creating playlist!');
}

let newPlaylist = await createRes.json();

let params = new URLSearchParams();
params.set('playlist_id', newPlaylist.id);

data = { to_add: songs };
let addRes = await fetch(`${API_URI}/playlist/add?${params.toString()}`, {
method: 'POST',
headers: {
'Authorization': access_token,
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});

if (addRes.status != 201) {
alert('Error adding songs to new playlist!');
} else {
alert('Created new playlist with songs.');
}
}
})

let container = document.getElementById('stageContainer');
function createStagingCard(track, trackId) {
let card = document.createElement('div');
Expand Down
Loading