Skip to content

Commit e6f54be

Browse files
authored
Merge pull request #3 from thakurdotdev/central-song-management
feat: Introduce central Song model, update HistorySong and PlaylistSong to reference it, and adapt musicController for centralized song data management.
2 parents 06969f4 + 793c49b commit e6f54be

8 files changed

Lines changed: 617 additions & 101 deletions

File tree

server/controllers/music/historyController.js.js

Lines changed: 99 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
const HistorySong = require('../../models/music/HistorySong');
2+
const Song = require('../../models/music/Song');
23
const { Op } = require('sequelize');
34
const sequelize = require('../../utils/sequelize');
45

6+
// Initialize associations
7+
require('../../models/music/index');
8+
59
const recommendationCache = new Map();
610

711
/* =========================
@@ -10,29 +14,33 @@ const recommendationCache = new Map();
1014

1115
const addToHistory = async (req, res) => {
1216
try {
13-
const { songData, playedTime = 0 } = req.body;
17+
const { songData: rawSongData, playedTime = 0 } = req.body;
1418
const userId = req.user.userid;
1519

20+
// Parse songData if it's a string
21+
const songData = typeof rawSongData === 'string' ? JSON.parse(rawSongData) : rawSongData;
22+
1623
if (!songData?.id) {
1724
return res.status(400).json({ error: 'Invalid song data' });
1825
}
1926

20-
const artistNames = extractArtistNames(songData);
21-
const completionRate = calculateCompletionRate(
22-
playedTime,
23-
songData.duration
24-
);
27+
// Get or create song in central table
28+
const song = await Song.getOrCreate(songData);
29+
30+
const completionRate = calculateCompletionRate(playedTime, songData.duration);
2531

2632
const [historySong, created] = await HistorySong.findOrCreate({
27-
where: { userId, songId: songData.id },
33+
where: { userId, songRefId: song.id },
2834
defaults: {
2935
userId,
36+
songRefId: song.id,
37+
// Keep deprecated fields for backward compatibility during migration
3038
songId: songData.id,
31-
songName: songData.name || songData.title || 'Unknown',
32-
artistNames,
33-
songLanguage: songData.language || 'Unknown',
39+
songName: song.name,
40+
artistNames: song.artistNames,
41+
songLanguage: song.language,
3442
songData,
35-
duration: songData.duration,
43+
duration: song.duration,
3644
playedTime,
3745
timeOfDay: new Date().getHours(),
3846
deviceType: getDeviceType(req.headers['user-agent']),
@@ -47,7 +55,7 @@ const addToHistory = async (req, res) => {
4755
await updateExistingSong(historySong, playedTime, completionRate);
4856
}
4957

50-
updateRecommendationScore(userId, songData.id).catch(console.error);
58+
updateRecommendationScore(userId, song.id).catch(console.error);
5159

5260
res.json({ message: 'History updated successfully' });
5361
} catch (error) {
@@ -76,22 +84,23 @@ const batchAddToHistory = async (req, res) => {
7684
}
7785

7886
try {
79-
const artistNames = extractArtistNames(songData);
80-
const completionRate = calculateCompletionRate(
81-
position,
82-
duration || songData.duration
83-
);
87+
// Get or create song in central table
88+
const song = await Song.getOrCreate(songData);
89+
90+
const completionRate = calculateCompletionRate(position, duration || songData.duration);
8491

8592
const [historySong, created] = await HistorySong.findOrCreate({
86-
where: { userId, songId },
93+
where: { userId, songRefId: song.id },
8794
defaults: {
8895
userId,
96+
songRefId: song.id,
97+
// Keep deprecated fields for backward compatibility
8998
songId,
90-
songName: songData.name || songData.title || 'Unknown',
91-
artistNames,
92-
songLanguage: songData.language || 'Unknown',
99+
songName: song.name,
100+
artistNames: song.artistNames,
101+
songLanguage: song.language,
93102
songData,
94-
duration: duration || songData.duration,
103+
duration: duration || song.duration,
95104
playedTime: position,
96105
timeOfDay: new Date(timestamp || Date.now()).getHours(),
97106
deviceType: getDeviceType(req.headers['user-agent']),
@@ -106,7 +115,7 @@ const batchAddToHistory = async (req, res) => {
106115
await updateExistingSong(historySong, position, completionRate);
107116
}
108117

109-
updateRecommendationScore(userId, songId).catch(console.error);
118+
updateRecommendationScore(userId, song.id).catch(console.error);
110119

111120
results.push({ songId, status: 'success' });
112121
} catch (err) {
@@ -136,37 +145,48 @@ const getPersonalizedRecommendations = async (req, res) => {
136145
const userId = req.user.userid;
137146
const limit = parseInt(req.query.limit || 12);
138147

139-
const cacheKey = `${userId}:${limit}`;
148+
// Cache key for recommendations only
149+
const cacheKey = `recs:${userId}:${limit}`;
140150
const cached = recommendationCache.get(cacheKey);
141151

152+
// Get recommendations from cache or compute
153+
let recommendationSongs;
142154
if (cached && cached.expiresAt > Date.now()) {
143-
return res.status(200).json({ success: true, data: cached.data });
155+
recommendationSongs = cached.data;
156+
} else {
157+
const recommendations = await calculateWeightedRecommendations(userId, limit);
158+
recommendationSongs = recommendations.map((r) => r.song?.songData || r.songData);
159+
160+
// Cache only recommendations (complex query)
161+
recommendationCache.set(cacheKey, {
162+
expiresAt: Date.now() + 5 * 60 * 1000,
163+
data: recommendationSongs,
164+
});
144165
}
145166

146-
const recommendations = await calculateWeightedRecommendations(
147-
userId,
148-
limit
149-
);
150-
167+
// Always fetch recently played fresh - no cache (should reflect new additions)
151168
const recentlyPlayed = await HistorySong.findAll({
152-
where: { userId },
169+
where: { userId, songRefId: { [Op.ne]: null } },
170+
include: [
171+
{
172+
model: Song,
173+
as: 'song',
174+
attributes: ['songData'],
175+
required: true,
176+
},
177+
],
153178
order: [['lastPlayedAt', 'DESC']],
154179
limit: 15,
155-
attributes: ['songData'],
156-
raw: true,
180+
attributes: ['id', 'songRefId'],
157181
});
158182

159-
const response = {
160-
songs: recommendations.map((r) => r.songData),
161-
recentlyPlayed: recentlyPlayed.map((r) => r.songData),
162-
};
163-
164-
recommendationCache.set(cacheKey, {
165-
expiresAt: Date.now() + 5 * 60 * 1000,
166-
data: response,
183+
res.status(200).json({
184+
success: true,
185+
data: {
186+
songs: recommendationSongs,
187+
recentlyPlayed: recentlyPlayed.map((r) => r.song?.songData),
188+
},
167189
});
168-
169-
res.status(200).json({ success: true, data: response });
170190
} catch (error) {
171191
console.error('Error in getPersonalizedRecommendations:', error);
172192
res.status(500).json({ error: 'Failed to get recommendations' });
@@ -196,23 +216,28 @@ const calculateWeightedRecommendations = async (userId, limit) => {
196216
],
197217
],
198218
},
219+
include: [
220+
{
221+
model: Song,
222+
as: 'song',
223+
attributes: ['songData', 'language', 'artistNames'],
224+
required: true,
225+
},
226+
],
199227
where: {
200228
userId,
229+
songRefId: { [Op.ne]: null },
201230
lastPlayedAt: {
202231
[Op.gte]: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
203232
},
204233
[Op.or]: [
205234
{ playedCount: { [Op.gt]: 1 } },
206235
{ completionRate: { [Op.gt]: 50 } },
207236
{ likeStatus: true },
208-
{ songLanguage: { [Op.in]: userStats.preferredLanguages } },
209-
...userStats.preferredArtists.map((a) => ({
210-
artistNames: { [Op.iLike]: `%${a}%` },
211-
})),
212237
],
213238
},
214239
order: [
215-
[HistorySong.sequelize.literal('"aiRecommendationScore"'), 'DESC'],
240+
[HistorySong.sequelize.literal('"aiRecommendationScore"'), 'DESC NULLS LAST'],
216241
[HistorySong.sequelize.literal('"weightedScore"'), 'DESC'],
217242
],
218243
limit,
@@ -234,20 +259,19 @@ const getHistorySongs = async (req, res) => {
234259
sortOrder = 'DESC',
235260
} = req.query;
236261

237-
const whereClause = { userId };
262+
const whereClause = { userId, songRefId: { [Op.ne]: null } };
263+
let songWhereClause = {};
238264

239265
if (searchQuery) {
240266
const q = searchQuery.trim();
241-
242-
whereClause[Op.or] = [
243-
{ songName: { [Op.iLike]: `%${q}%` } },
244-
{ artistNames: { [Op.iLike]: `%${q}%` } },
245-
{ songLanguage: { [Op.iLike]: `%${q}%` } },
246-
sequelize.where(
247-
sequelize.cast(sequelize.col('"HistorySong"."songData"'), 'text'),
248-
{ [Op.iLike]: `%${q}%` }
249-
),
250-
];
267+
// Search on Song table's indexed columns for better performance
268+
songWhereClause = {
269+
[Op.or]: [
270+
{ name: { [Op.iLike]: `%${q}%` } },
271+
{ artistNames: { [Op.iLike]: `%${q}%` } },
272+
{ language: { [Op.iLike]: `%${q}%` } },
273+
],
274+
};
251275
}
252276

253277
const order = searchQuery
@@ -270,17 +294,25 @@ const getHistorySongs = async (req, res) => {
270294

271295
const historySongs = await HistorySong.findAndCountAll({
272296
where: whereClause,
297+
include: [
298+
{
299+
model: Song,
300+
as: 'song',
301+
attributes: ['songData'],
302+
where: Object.keys(songWhereClause).length > 0 ? songWhereClause : undefined,
303+
required: true,
304+
},
305+
],
273306
limit: parseInt(limit),
274307
offset,
275308
order,
276-
attributes: ['songData'],
277-
raw: true,
309+
attributes: ['id', 'songRefId', 'playedCount', 'likeStatus', 'lastPlayedAt'],
278310
});
279311

280312
res.status(200).json({
281313
status: 'success',
282314
data: {
283-
songs: historySongs.rows.map((r) => r.songData),
315+
songs: historySongs.rows.map((r) => r.song?.songData),
284316
count: historySongs.count,
285317
currentPage: parseInt(page),
286318
totalPages: Math.ceil(historySongs.count / limit),
@@ -331,10 +363,7 @@ const updateRecommendationScore = async (userId, songId) => {
331363

332364
const score = calculateAlgorithmicScore(song, stats);
333365

334-
await HistorySong.update(
335-
{ aiRecommendationScore: score },
336-
{ where: { userId, songId } }
337-
);
366+
await HistorySong.update({ aiRecommendationScore: score }, { where: { userId, songId } });
338367
} catch (error) {
339368
console.error('Error updating recommendation score:', error);
340369
}
@@ -346,10 +375,7 @@ const updateRecommendationScore = async (userId, songId) => {
346375

347376
const getUserListeningStats = async (userId) => {
348377
const languages = await HistorySong.findAll({
349-
attributes: [
350-
'songLanguage',
351-
[sequelize.fn('COUNT', sequelize.col('songId')), 'count'],
352-
],
378+
attributes: ['songLanguage', [sequelize.fn('COUNT', sequelize.col('songId')), 'count']],
353379
where: {
354380
userId,
355381
lastPlayedAt: {
@@ -363,10 +389,7 @@ const getUserListeningStats = async (userId) => {
363389
});
364390

365391
const artists = await HistorySong.findAll({
366-
attributes: [
367-
'artistNames',
368-
[sequelize.fn('SUM', sequelize.col('playedCount')), 'plays'],
369-
],
392+
attributes: ['artistNames', [sequelize.fn('SUM', sequelize.col('playedCount')), 'plays']],
370393
where: {
371394
userId,
372395
lastPlayedAt: {
@@ -381,9 +404,7 @@ const getUserListeningStats = async (userId) => {
381404

382405
return {
383406
preferredLanguages: languages.map((l) => l.songLanguage),
384-
preferredArtists: artists.flatMap((a) =>
385-
a.artistNames.split(',').map((x) => x.trim())
386-
),
407+
preferredArtists: artists.flatMap((a) => a.artistNames.split(',').map((x) => x.trim())),
387408
};
388409
};
389410

@@ -393,8 +414,7 @@ const calculateAlgorithmicScore = (song, stats) => {
393414
if (stats.preferredLanguages.includes(song.songLanguage)) score += 0.15;
394415

395416
const songArtists = song.artistNames.split(',').map((a) => a.trim());
396-
if (songArtists.some((a) => stats.preferredArtists.includes(a)))
397-
score += 0.2;
417+
if (songArtists.some((a) => stats.preferredArtists.includes(a))) score += 0.2;
398418

399419
if (song.playedCount > 5) score += 0.05;
400420
if (song.completionRate > 80) score += 0.1;

0 commit comments

Comments
 (0)