From ba293fd1406449c4ae8019bf30bf28c2051d26f1 Mon Sep 17 00:00:00 2001 From: Bintang Murtifandy <21844@student.stembayo.sch.id> Date: Tue, 26 May 2026 08:35:04 +0700 Subject: [PATCH] feat: add album routes, playlist detail, liked tracks, fix search/artist routing --- src/controllers/albumController.js | 81 +++++++++++++++++++++++++++ src/controllers/playlistController.js | 22 ++++++++ src/controllers/searchController.js | 40 +++++++++++-- src/controllers/trackController.js | 19 +++++++ src/routes/albums.js | 10 ++++ src/routes/index.js | 2 + src/routes/playlists.js | 1 + src/routes/tracks.js | 1 + 8 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 src/controllers/albumController.js create mode 100644 src/routes/albums.js diff --git a/src/controllers/albumController.js b/src/controllers/albumController.js new file mode 100644 index 0000000..224a993 --- /dev/null +++ b/src/controllers/albumController.js @@ -0,0 +1,81 @@ +import mongoose from 'mongoose'; +import Track from '../models/Track.js'; +import Album from '../models/Album.js'; + +export const albumController = { + async list(req, res, next) { + try { + const albums = await Album.find() + .sort({ createdAt: -1 }) + .populate('tracks') + .lean(); + res.json({ success: true, data: albums }); + } catch (error) { + next(error); + } + }, + + async getById(req, res, next) { + try { + const { albumId } = req.params; + const decodedId = decodeURIComponent(albumId); + + if (mongoose.Types.ObjectId.isValid(decodedId)) { + const album = await Album.findById(decodedId) + .populate('tracks') + .lean(); + if (album) { + return res.json({ success: true, data: album }); + } + } + + const tracks = await Track.find({ album: decodedId }) + .sort({ createdAt: -1 }) + .lean(); + + if (!tracks.length) { + return res.status(404).json({ success: false, message: 'Album not found' }); + } + + const data = { + _id: decodedId, + title: decodedId, + artist: tracks[0].artist, + coverUrl: tracks.find(t => t.coverUrl)?.coverUrl || null, + releaseYear: null, + tracks, + }; + + return res.json({ success: true, data }); + } catch (error) { + next(error); + } + }, + + async getByName(req, res, next) { + try { + const { albumTitle } = req.params; + const decoded = decodeURIComponent(albumTitle); + + const tracks = await Track.find({ album: decoded }) + .sort({ createdAt: -1 }) + .lean(); + + if (!tracks.length) { + return res.status(404).json({ success: false, message: 'Album not found' }); + } + + const data = { + title: decoded, + artist: tracks[0].artist, + coverUrl: tracks.find(t => t.coverUrl)?.coverUrl || null, + tracks, + trackCount: tracks.length, + }; + + return res.json({ success: true, data }); + } catch (error) { + next(error); + } + }, +}; diff --git a/src/controllers/playlistController.js b/src/controllers/playlistController.js index c56e431..9e39e55 100644 --- a/src/controllers/playlistController.js +++ b/src/controllers/playlistController.js @@ -13,6 +13,28 @@ export const playlistController = { } }, + async getById(req, res, next) { + try { + const { playlistId } = req.params; + const playlist = await Playlist.findOne({ + _id: playlistId, + user: req.userId, + }) + .populate('tracks') + .lean(); + + if (!playlist) { + return res + .status(404) + .json({ success: false, message: 'Playlist not found' }); + } + + return res.json({ success: true, data: playlist }); + } catch (error) { + next(error); + } + }, + async create(req, res, next) { try { const { name, description } = req.body; diff --git a/src/controllers/searchController.js b/src/controllers/searchController.js index 182dc15..87d4648 100644 --- a/src/controllers/searchController.js +++ b/src/controllers/searchController.js @@ -1,3 +1,4 @@ +import mongoose from 'mongoose'; import Track from '../models/Track.js'; export const searchController = { @@ -26,11 +27,42 @@ export const searchController = { .limit(50) .lean(); - const artistNames = [...new Set(tracks.map(t => t.artist))]; - const artists = artistNames.map(name => ({ name })); + const artistMap = new Map(); + tracks.forEach(t => { + if (!artistMap.has(t.artist)) { + artistMap.set(t.artist, { + _id: new mongoose.Types.ObjectId(), + name: t.artist, + imageUrl: t.coverUrl || '', + trackCount: 0, + }); + } + const entry = artistMap.get(t.artist); + entry.trackCount++; + if (t.coverUrl && !entry.imageUrl) { + entry.imageUrl = t.coverUrl; + } + }); + const artists = [...artistMap.values()]; - const albumNames = [...new Set(tracks.map(t => t.album))]; - const albums = albumNames.map(name => ({ name })); + const albumMap = new Map(); + tracks.forEach(t => { + if (!albumMap.has(t.album)) { + albumMap.set(t.album, { + _id: new mongoose.Types.ObjectId(), + title: t.album, + artist: t.artist, + coverUrl: t.coverUrl || '', + trackCount: 0, + }); + } + const entry = albumMap.get(t.album); + entry.trackCount++; + if (t.coverUrl && !entry.coverUrl) { + entry.coverUrl = t.coverUrl; + } + }); + const albums = [...albumMap.values()]; const data = { tracks, artists, albums }; diff --git a/src/controllers/trackController.js b/src/controllers/trackController.js index f0e3c8d..7d73137 100644 --- a/src/controllers/trackController.js +++ b/src/controllers/trackController.js @@ -11,6 +11,25 @@ export const trackController = { } }, + async listLiked(req, res, next) { + try { + const user = await User.findById(req.userId) + .select('likedTracks') + .populate('likedTracks') + .lean(); + + if (!user) { + return res + .status(404) + .json({ success: false, message: 'User not found' }); + } + + return res.json({ success: true, data: user.likedTracks || [] }); + } catch (error) { + next(error); + } + }, + async like(req, res, next) { try { const { trackId } = req.params; diff --git a/src/routes/albums.js b/src/routes/albums.js new file mode 100644 index 0000000..a40bbd1 --- /dev/null +++ b/src/routes/albums.js @@ -0,0 +1,10 @@ +import { Router } from 'express'; +import { albumController } from '../controllers/albumController.js'; + +const router = Router(); + +router.get('/', albumController.list); +router.get('/:albumId', albumController.getById); +router.get('/title/:albumTitle', albumController.getByName); + +export default router; diff --git a/src/routes/index.js b/src/routes/index.js index f88685a..3fe9d24 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,6 +4,7 @@ import adminRoutes from './admins.js'; import trackRoutes from './tracks.js'; import playlistRoutes from './playlists.js'; import artistRoutes from './artists.js'; +import albumRoutes from './albums.js'; import searchRoutes from './search.js'; const router = Router(); @@ -23,6 +24,7 @@ router.use('/admin', adminRoutes); router.use('/tracks', trackRoutes); router.use('/playlists', playlistRoutes); router.use('/artists', artistRoutes); +router.use('/albums', albumRoutes); router.use('/search', searchRoutes); export default router; diff --git a/src/routes/playlists.js b/src/routes/playlists.js index ca44484..a85717b 100644 --- a/src/routes/playlists.js +++ b/src/routes/playlists.js @@ -6,6 +6,7 @@ import { uploadPlaylistCover } from '../utils/multer.js'; const router = Router(); router.get('/', auth, playlistController.list); +router.get('/:playlistId', auth, playlistController.getById); router.post( '/', auth, diff --git a/src/routes/tracks.js b/src/routes/tracks.js index 1cb16f3..a217515 100644 --- a/src/routes/tracks.js +++ b/src/routes/tracks.js @@ -5,6 +5,7 @@ import { auth } from '../middlewares/auth.js'; const router = Router(); router.get('/', trackController.list); +router.get('/liked', auth, trackController.listLiked); router.post('/:trackId/like', auth, trackController.like); router.delete('/:trackId/like', auth, trackController.unlike);