|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "The Album Exchange Story" |
| 4 | +date: 2025-06-06 16:32:00 -0400 |
| 5 | +categories: general |
| 6 | +tags: album-exchange music spotify |
| 7 | +--- |
| 8 | + |
| 9 | +During the height of Covid, a bunch of brave people gathered around a table and said, "Enough is enough!" and decided to take matters into their own hands. So they made a Discord channel to pair up random people and recommend music, and this was called Album Exchange. |
| 10 | + |
| 11 | +That no longer exists, and Spotify's recommendation algorithm—pardon my language—sucks ass, which is why I have decided to be brave and build a website to get album recommendations. |
| 12 | + |
| 13 | +<figure> |
| 14 | + <img src="/assets/images/blog/album-exchange.png" alt="Social preview for Album Exchange"> |
| 15 | + <figcaption>Album Exchange</figcaption> |
| 16 | +</figure> |
| 17 | + |
| 18 | +## The Core Idea |
| 19 | + |
| 20 | +The concept is intentionally simple: |
| 21 | + |
| 22 | +- I feature a weekly album pick on the homepage |
| 23 | +- You can submit one album recommendation per week |
| 24 | +- Your submission automatically becomes a playlist on my Spotify account |
| 25 | +- Everyone can browse the gallery of all submitted albums |
| 26 | +- The site shows what I'm currently listening to (when I am) |
| 27 | + |
| 28 | +## Technical Challenges |
| 29 | + |
| 30 | +### The Spotify API |
| 31 | + |
| 32 | +I have never used Spotify's API before, so I had to learn to do a few things like: |
| 33 | + |
| 34 | +1. Fetch album details for display |
| 35 | +2. Create playlists automatically from submissions |
| 36 | +3. Show my current listening status |
| 37 | +4. Handle rate limits gracefully |
| 38 | + |
| 39 | +The rate limit challenge was enjoyable. Spotify is generous with its limits, but you can hit walls quickly when fetching album art and details for every submission. I ended up implementing a caching strategy with multiple fallbacks: |
| 40 | + |
| 41 | +```javascript |
| 42 | +// cache album details for 24h |
| 43 | +const albumDetailsCache = new Map(); |
| 44 | +const ALBUM_CACHE_DURATION = 24 * 60 * 60 * 1000; |
| 45 | + |
| 46 | +// retry logic with exponential backoff |
| 47 | +async function fetchWithRetry(fetchFn, maxRetries = MAX_RETRIES) { |
| 48 | + let retryDelay = INITIAL_RETRY_DELAY; |
| 49 | + |
| 50 | + for (let attempt = 0; attempt <= maxRetries; attempt++) { |
| 51 | + try { |
| 52 | + return await fetchFn(); |
| 53 | + } catch (error) { |
| 54 | + if (error.status === 429) { |
| 55 | + const waitTime = error.retryAfter |
| 56 | + ? error.retryAfter * 1000 |
| 57 | + : retryDelay; |
| 58 | + await new Promise((resolve) => setTimeout(resolve, waitTime)); |
| 59 | + retryDelay *= 2; |
| 60 | + } |
| 61 | + } |
| 62 | + } |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +### Preventing Spam |
| 67 | + |
| 68 | +I hate when websites make me sign up. I just want people to be able to submit recommendations, but I also didn't want someone to flood the site with a 47-album playlist of "souljaboytellem.com." So, I needed a rate-limiting system without asking the users to sign up. |
| 69 | + |
| 70 | +I went with a combination of IP address and browser fingerprinting: |
| 71 | + |
| 72 | +```javascript |
| 73 | +function generateSubmissionId(ip, fingerprint) { |
| 74 | + return crypto |
| 75 | + .createHash("sha256") |
| 76 | + .update(ip + (fingerprint || "") + (process.env.IP_SALT || "")) |
| 77 | + .digest("hex"); |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +This creates a unique but anonymized identifier for each user. The salt means I can't reverse-engineer the original data, and the one-way hash means the system is privacy-friendly while preventing abuse. |
| 82 | + |
| 83 | +The rate limit resets every Monday at midnight UTC, which gives the site a nice weekly rhythm. |
| 84 | + |
| 85 | +### The Playlist Creation Magic |
| 86 | + |
| 87 | +This part is my favorite. When someone submits an album, I don't just store it in a database—I create a Spotify playlist on my account with all the tracks from that album. |
| 88 | + |
| 89 | +```javascript |
| 90 | +export async function createAlbumPlaylist(albumId, nickname, albumName) { |
| 91 | + const api = await getSpotifyApi(); |
| 92 | + |
| 93 | + // get my user profile |
| 94 | + const me = await api.currentUser.profile(); |
| 95 | + |
| 96 | + // create playlist with format "nickname-album title" |
| 97 | + const playlistName = `${nickname}-${albumName}`.substring(0, 100); |
| 98 | + const playlist = await api.playlists.createPlaylist(me.id, { |
| 99 | + name: playlistName, |
| 100 | + description: `album recommendation from ${nickname} via bhav.fun`, |
| 101 | + public: false, |
| 102 | + }); |
| 103 | + |
| 104 | + // get all tracks from the album |
| 105 | + const albumTracks = await api.albums.tracks(albumId); |
| 106 | + |
| 107 | + // add them to the playlist |
| 108 | + if (albumTracks.items.length > 0) { |
| 109 | + const trackUris = albumTracks.items.map((track) => track.uri); |
| 110 | + await api.playlists.addItemsToPlaylist(playlist.id, trackUris); |
| 111 | + } |
| 112 | + |
| 113 | + return playlist; |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +Which means every recommendation becomes an immediately playable playlist. I realize I have let the internet control my Spotify; there is a non-zero chance I wake up to my Spotify having a playlist called "Penis-Punisher." But please don't do that. I beg. |
| 118 | + |
| 119 | +## Architecture: Next.js + Firebase + Prayers |
| 120 | + |
| 121 | +The stack is pretty straightforward: |
| 122 | + |
| 123 | +- **Next.js** for the frontend and API routes |
| 124 | +- **Firebase Firestore** for storing submissions |
| 125 | +- **Spotify Web API** for everything music-related |
| 126 | +- **Vercel** for hosting |
| 127 | + |
| 128 | +I organized it around clear separation of concerns: |
| 129 | + |
| 130 | +``` |
| 131 | +├── app/ # Next.js pages and API routes |
| 132 | +├── components/ # React components (all the cozy UI bits) |
| 133 | +├── lib/ # Services and utilities |
| 134 | +│ ├── spotify-service.js # All Spotify API interactions |
| 135 | +│ ├── firebase-service.js # Database operations |
| 136 | +│ ├── fingerprint-service.js # Rate limiting |
| 137 | +│ └── url-utils.js # Input validation and cleaning |
| 138 | +``` |
| 139 | + |
| 140 | +The API design is RESTful but pragmatic. For example, the submission endpoint does the following: |
| 141 | + |
| 142 | +1. Validate the rate limit |
| 143 | +2. Sanitizes inputs |
| 144 | +3. Validates Spotify URLs |
| 145 | +4. Fetches album details |
| 146 | +5. Create the playlist |
| 147 | +6. Stores everything in the database |
| 148 | + |
| 149 | +It's not the most "pure" API design, but it works well for this use case where each submission is a complex multi-step operation. |
| 150 | + |
| 151 | +## Some Interesting Problems I Solved |
| 152 | + |
| 153 | +### Dynamic Album Cards |
| 154 | + |
| 155 | +I wanted album cards that could dynamically load their artwork and details from just a Spotify URL. This led to an interesting caching strategy where I cache both in-browser (localStorage) and server-side, with intelligent fallbacks when things go wrong. |
| 156 | + |
| 157 | +### Responsive Image Handling |
| 158 | + |
| 159 | +Album artwork comes in various sizes from Spotify, and I needed it to look good across devices. Next.js's Image component helped a lot, but I still had to handle edge cases like missing artwork: |
| 160 | + |
| 161 | +```jsx |
| 162 | +<Image |
| 163 | + src={albumImage || "/images/album-placeholder.jpg"} |
| 164 | + alt={`Album cover: ${albumName} by ${artistName}`} |
| 165 | + width={300} |
| 166 | + height={300} |
| 167 | + quality={85} |
| 168 | + placeholder="blur" |
| 169 | + blurDataURL="data:image/svg+xml,..." |
| 170 | + onError={onImageError} |
| 171 | +/> |
| 172 | +``` |
| 173 | + |
| 174 | +### The "Now Playing" Feature |
| 175 | + |
| 176 | +I quickly realized this was slightly more nuanced than expected because Spotify's "currently playing" endpoint doesn't always have data. So I fall back to "recently played" and show that instead, with appropriate labeling. It makes the site feel more alive and personal. |
| 177 | + |
| 178 | +## The Unexpected Joy of Building Something Small |
| 179 | + |
| 180 | +<figure> |
| 181 | + <img src="/assets/images/blog/the-zen-of-python.png" alt="Zen of Python by Tim Peters"> |
| 182 | + <figcaption>The Zen of Python</figcaption> |
| 183 | +</figure> |
| 184 | + |
| 185 | +One of the most rewarding aspects of this project was its scope. I built and shipped the MVP in two days, and it immediately felt proper and complete. There's something deeply satisfying about creating a tool that solves exactly one problem well. |
| 186 | + |
| 187 | +It also forced me to make decisions quickly. Should I build user accounts? Should I add social features? Should I integrate with other music services? For now, the answer to all of these is "no," and that simplicity is part of what makes the site charming. |
| 188 | + |
| 189 | +## Some Technical Details You Might Care About |
| 190 | + |
| 191 | +**Security:** All user inputs are sanitized, URLs are validated to ensure they're actually Spotify links, and the rate-limiting system is designed to be privacy-friendly. |
| 192 | + |
| 193 | +**Performance:** Album details are cached aggressively, images are optimized with Next.js's Image component, and the Spotify API calls include retry logic and error handling. |
| 194 | + |
| 195 | +**Accessibility:** I tried to follow best practices, such as semantic HTML, proper focus management, alt text for images, and keyboard navigation support. |
| 196 | + |
| 197 | +**SEO:** The site has proper meta tags, a sitemap, and structured data to help with discovery. |
| 198 | + |
| 199 | +## What's Next |
| 200 | + |
| 201 | +As sad as it is, my hyper fixation on this site has worn off, but I'm sure I will revisit this, and when I do, I'd like to make: |
| 202 | + |
| 203 | +- Better error handling for edge cases |
| 204 | +- More granular caching strategies |
| 205 | +- Maybe a Grand Spotify playlist that's automatically updated with all submissions |
| 206 | + |
| 207 | +But I'm pretty happy with it as it is. Sometimes, the best software is the kind that does exactly what it says it will do without trying to be everything to everyone. |
| 208 | + |
| 209 | +## Try It Out |
| 210 | + |
| 211 | +You can check out [Album Exchange](https://bhav.fun/) yourself. Please submit an album you love, browse what others have recommended, or look around the source code on [GitHub](https://github.com/codebhav/album-exchange). |
| 212 | + |
| 213 | +And if you build something similar or have ideas for improvements, [I'd love to hear about it](https://whybhav.in/contact/). The best part of sharing projects like this is the conversations they start. |
0 commit comments