From 59e4d86a3bd7de4d4a6d1dcfe19475a94698d645 Mon Sep 17 00:00:00 2001 From: Bintang Murtifandy <21844@student.stembayo.sch.id> Date: Mon, 25 May 2026 21:23:56 +0700 Subject: [PATCH] feat(playlists): add likes and playlists --- src/controllers/playlistController.js | 67 +++++++++++++++++++++++++++ src/controllers/trackController.js | 48 +++++++++++++++++++ src/models/Playlist.js | 28 +++++++++++ src/models/User.js | 6 +++ src/routes/index.js | 2 + src/routes/playlists.js | 11 +++++ src/routes/tracks.js | 3 ++ 7 files changed, 165 insertions(+) create mode 100644 src/controllers/playlistController.js create mode 100644 src/models/Playlist.js create mode 100644 src/routes/playlists.js diff --git a/src/controllers/playlistController.js b/src/controllers/playlistController.js new file mode 100644 index 0000000..968b335 --- /dev/null +++ b/src/controllers/playlistController.js @@ -0,0 +1,67 @@ +import Playlist from '../models/Playlist.js'; +import Track from '../models/Track.js'; + +export const playlistController = { + async list(req, res, next) { + try { + const playlists = await Playlist.find({ user: req.userId }) + .sort({ createdAt: -1 }) + .lean(); + res.json({ success: true, data: playlists }); + } catch (error) { + next(error); + } + }, + + async create(req, res, next) { + try { + const { name } = req.body; + if (!name) { + return res + .status(400) + .json({ success: false, message: 'Playlist name is required' }); + } + + const playlist = await Playlist.create({ + name, + user: req.userId, + }); + + return res.json({ success: true, data: playlist }); + } catch (error) { + next(error); + } + }, + + async addTrack(req, res, next) { + try { + const { playlistId } = req.params; + const { trackId } = req.body; + + if (!trackId) { + return res + .status(400) + .json({ success: false, message: 'Track id is required' }); + } + + const exists = await Track.exists({ _id: trackId }); + if (!exists) { + return res.status(404).json({ success: false, message: 'Track not found' }); + } + + const playlist = await Playlist.findOneAndUpdate( + { _id: playlistId, user: req.userId }, + { $addToSet: { tracks: trackId } }, + { new: true } + ).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); + } + }, +}; diff --git a/src/controllers/trackController.js b/src/controllers/trackController.js index 657388c..ee4988c 100644 --- a/src/controllers/trackController.js +++ b/src/controllers/trackController.js @@ -1,4 +1,5 @@ import Track from '../models/Track.js'; +import User from '../models/User.js'; export const trackController = { async list(req, res, next) { @@ -9,4 +10,51 @@ export const trackController = { next(error); } }, + + async like(req, res, next) { + try { + const { trackId } = req.params; + + const exists = await Track.exists({ _id: trackId }); + if (!exists) { + return res.status(404).json({ success: false, message: 'Track not found' }); + } + + const user = await User.findByIdAndUpdate( + req.userId, + { $addToSet: { likedTracks: trackId } }, + { new: true } + ) + .select('likedTracks') + .lean(); + + return res.json({ + success: true, + data: { liked: true, likedTracks: user?.likedTracks ?? [] }, + }); + } catch (error) { + next(error); + } + }, + + async unlike(req, res, next) { + try { + const { trackId } = req.params; + + const user = await User.findByIdAndUpdate( + req.userId, + { $pull: { likedTracks: trackId } }, + { new: true } + ) + .select('likedTracks') + .lean(); + + return res.json({ + success: true, + data: { liked: false, likedTracks: user?.likedTracks ?? [] }, + }); + } catch (error) { + next(error); + } + }, }; diff --git a/src/models/Playlist.js b/src/models/Playlist.js new file mode 100644 index 0000000..bb6e094 --- /dev/null +++ b/src/models/Playlist.js @@ -0,0 +1,28 @@ +import mongoose from 'mongoose'; + +const playlistSchema = new mongoose.Schema( + { + name: { + type: String, + required: true, + trim: true, + maxlength: 120, + }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, + }, + tracks: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'Track', + }, + ], + }, + { + timestamps: true, + } +); + +export default mongoose.model('Playlist', playlistSchema); diff --git a/src/models/User.js b/src/models/User.js index d69dfe3..d214a16 100644 --- a/src/models/User.js +++ b/src/models/User.js @@ -37,6 +37,12 @@ const userSchema = new mongoose.Schema( type: String, trim: true, }, + likedTracks: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'Track', + }, + ], }, { timestamps: true, diff --git a/src/routes/index.js b/src/routes/index.js index d70e120..13d30ae 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -2,6 +2,7 @@ import { Router } from 'express'; import authRoutes from './auths.js'; import adminRoutes from './admins.js'; import trackRoutes from './tracks.js'; +import playlistRoutes from './playlists.js'; const router = Router(); @@ -18,5 +19,6 @@ router.get('/health', (req, res) => { router.use('/auth', authRoutes); router.use('/admin', adminRoutes); router.use('/tracks', trackRoutes); +router.use('/playlists', playlistRoutes); export default router; diff --git a/src/routes/playlists.js b/src/routes/playlists.js new file mode 100644 index 0000000..84784cd --- /dev/null +++ b/src/routes/playlists.js @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { playlistController } from '../controllers/playlistController.js'; +import { auth } from '../middlewares/auth.js'; + +const router = Router(); + +router.get('/', auth, playlistController.list); +router.post('/', auth, playlistController.create); +router.post('/:playlistId/tracks', auth, playlistController.addTrack); + +export default router; diff --git a/src/routes/tracks.js b/src/routes/tracks.js index ea309b2..1cb16f3 100644 --- a/src/routes/tracks.js +++ b/src/routes/tracks.js @@ -1,8 +1,11 @@ import { Router } from 'express'; import { trackController } from '../controllers/trackController.js'; +import { auth } from '../middlewares/auth.js'; const router = Router(); router.get('/', trackController.list); +router.post('/:trackId/like', auth, trackController.like); +router.delete('/:trackId/like', auth, trackController.unlike); export default router;