feat: add album routes, playlist detail, liked tracks, fix search/artist routing

This commit is contained in:
2026-05-26 08:35:04 +07:00
parent b0c29765a9
commit ba293fd140
8 changed files with 172 additions and 4 deletions
+81
View File
@@ -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);
}
},
};
+22
View File
@@ -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;
+36 -4
View File
@@ -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 };
+19
View File
@@ -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;
+10
View File
@@ -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;
+2
View File
@@ -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;
+1
View File
@@ -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,
+1
View File
@@ -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);