11const HistorySong = require ( '../../models/music/HistorySong' ) ;
2+ const Song = require ( '../../models/music/Song' ) ;
23const { Op } = require ( 'sequelize' ) ;
34const sequelize = require ( '../../utils/sequelize' ) ;
45
6+ // Initialize associations
7+ require ( '../../models/music/index' ) ;
8+
59const recommendationCache = new Map ( ) ;
610
711/* =========================
@@ -10,29 +14,33 @@ const recommendationCache = new Map();
1014
1115const 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
347376const 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