Skip to content

Commit f4e36c1

Browse files
committed
feat: Implement a 'play next' song recommendation feature, including a new server service, API endpoints, and client integration.
1 parent 505f000 commit f4e36c1

3 files changed

Lines changed: 404 additions & 2 deletions

File tree

client/src/api/music/songs.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,12 @@ export const fetchExternalPlaylist = async (id) => {
5151
}
5252

5353
export const fetchSongRecommendations = async (id) => {
54-
const { data } = await axios.get(`${SONG_URL}/song/recommend?id=${id}`)
55-
return data?.data || []
54+
try {
55+
const { data } = await axios.get(`${API_URL}/api/play-next/${id}?limit=20`)
56+
return data?.data || []
57+
} catch {
58+
return []
59+
}
5660
}
5761

5862
export const fetchSongById = async (id) => {

server/routes/musicRoutes.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
POPULAR_ARTISTS,
3434
SEARCH_QUERIES,
3535
} = require("../services/musicSyncService")
36+
const { getPlayNextSongs, rebuildAllPlayNext } = require("../services/playNextService")
3637

3738
const musicRoutes = express.Router()
3839

@@ -184,4 +185,48 @@ musicRoutes.get("/sync/status", async (req, res) => {
184185
}
185186
})
186187

188+
musicRoutes.get("/play-next/:songId", async (req, res) => {
189+
try {
190+
const { songId } = req.params
191+
192+
if (!songId || typeof songId !== "string") {
193+
return res.status(400).json({ success: false, error: "Invalid songId" })
194+
}
195+
196+
const limitRaw = parseInt(req.query.limit, 10)
197+
const limit = Math.min(Math.max(limitRaw || 20, 1), 50)
198+
199+
const excludeSongIds = req.query.exclude
200+
? req.query.exclude.split(",").map((s) => s.trim())
201+
: []
202+
203+
const data = await getPlayNextSongs({
204+
baseSongId: songId,
205+
limit,
206+
excludeSongIds,
207+
})
208+
res.set(
209+
"Cache-Control",
210+
"public, max-age=86400, s-maxage=604800, stale-while-revalidate=604800",
211+
)
212+
return res.json({ success: true, data })
213+
} catch (error) {
214+
console.error("[PlayNext] Error:", error.message)
215+
if (res.headersSent) return
216+
const status = error.message.includes("not found") ? 404 : 500
217+
res.set("Cache-Control", "no-store")
218+
return res.status(status).json({ success: false, error: error.message })
219+
}
220+
})
221+
222+
musicRoutes.post("/play-next/rebuild", async (req, res) => {
223+
try {
224+
const stats = await rebuildAllPlayNext()
225+
res.json({ success: true, data: stats })
226+
} catch (error) {
227+
console.error("[PlayNext] Rebuild failed:", error)
228+
res.status(500).json({ success: false, error: error.message })
229+
}
230+
})
231+
187232
module.exports = musicRoutes

0 commit comments

Comments
 (0)