@@ -2,11 +2,14 @@ const HistorySong = require("../../models/music/HistorySong")
22const Song = require ( "../../models/music/Song" )
33const { Op } = require ( "sequelize" )
44const sequelize = require ( "../../utils/sequelize" )
5+ const {
6+ getRecommendationsForUser,
7+ getRecentlyPlayed,
8+ queueUserForRecalc,
9+ } = require ( "../../services/recommendationService" )
510
611require ( "../../models/music/index" )
712
8- const recommendationCache = new Map ( )
9-
1013const addToHistory = async ( req , res ) => {
1114 try {
1215 const { songData : rawSongData , playedTime = 0 } = req . body
@@ -33,6 +36,7 @@ const addToHistory = async (req, res) => {
3336 lastPlayedAt : new Date ( ) ,
3437 } )
3538
39+ queueUserForRecalc ( userId )
3640 res . json ( { message : "History updated successfully" } )
3741 } catch ( error ) {
3842 console . error ( "Error in addToHistory:" , error )
@@ -83,6 +87,7 @@ const batchAddToHistory = async (req, res) => {
8387 }
8488 }
8589
90+ queueUserForRecalc ( userId )
8691 res . json ( {
8792 message : "Batch history updated successfully" ,
8893 results,
@@ -100,34 +105,16 @@ const getPersonalizedRecommendations = async (req, res) => {
100105 const userId = req . user . userid
101106 const limit = parseInt ( req . query . limit || 12 , 10 )
102107
103- const cacheKey = `recs:${ userId } :${ limit } `
104- const cached = recommendationCache . get ( cacheKey )
105-
106- let recommendationSongs
107- if ( cached && cached . expiresAt > Date . now ( ) ) {
108- recommendationSongs = cached . data
109- } else {
110- const rows = await calculateWeightedRecommendations ( userId , limit )
111- recommendationSongs = rows . map ( ( r ) => r . song ?. songData )
112-
113- recommendationCache . set ( cacheKey , {
114- expiresAt : Date . now ( ) + 5 * 60 * 1000 ,
115- data : recommendationSongs ,
116- } )
117- }
118-
119- const recentlyPlayed = await HistorySong . findAll ( {
120- where : { userId, songRefId : { [ Op . ne ] : null } } ,
121- include : [ { model : Song , as : "song" , attributes : [ "songData" ] , required : true } ] ,
122- order : [ [ "lastPlayedAt" , "DESC" ] ] ,
123- limit : 15 ,
124- } )
108+ const [ recommendationSongs , recentlyPlayedSongs ] = await Promise . all ( [
109+ getRecommendationsForUser ( userId , limit ) ,
110+ getRecentlyPlayed ( userId , 15 ) ,
111+ ] )
125112
126113 res . status ( 200 ) . json ( {
127114 success : true ,
128115 data : {
129116 songs : recommendationSongs ,
130- recentlyPlayed : recentlyPlayed . map ( ( r ) => r . song ?. songData ) ,
117+ recentlyPlayed : recentlyPlayedSongs ,
131118 } ,
132119 } )
133120 } catch ( error ) {
@@ -136,107 +123,68 @@ const getPersonalizedRecommendations = async (req, res) => {
136123 }
137124}
138125
139- const calculateWeightedRecommendations = async ( userId , limit ) => {
140- const { artists, languages } = await getRecentContext ( userId )
141-
142- const artistLikes = artists . map ( ( a ) => `%${ a } %` )
143- const languageList = languages . map ( ( l ) => sequelize . escape ( l ) ) . join ( "," )
144-
145- return HistorySong . findAll ( {
146- include : [
147- {
148- model : Song ,
149- as : "song" ,
150- attributes : [ "songData" , "artistNames" , "language" ] ,
151- required : true ,
152- } ,
153- ] ,
154- where : {
155- userId,
156- songRefId : { [ Op . ne ] : null } ,
157- lastPlayedAt : {
158- [ Op . gte ] : new Date ( Date . now ( ) - 90 * 24 * 60 * 60 * 1000 ) ,
159- } ,
160- } ,
161- order : [
162- [
163- sequelize . literal ( `
164- (
165- -- 🔥 STRONG RECENCY
166- EXP(-EXTRACT(HOUR FROM NOW() - "HistorySong"."lastPlayedAt") / 24) * 0.45 +
167-
168- -- 🎧 ARTIST CONTEXT
169- CASE
170- WHEN "song"."artistNames" ILIKE ANY (
171- ARRAY[${ artistLikes . map ( ( a ) => sequelize . escape ( a ) ) . join ( "," ) } ]
172- )
173- THEN 0.25
174- ELSE 0
175- END +
176-
177- -- 🌍 LANGUAGE CONTEXT
178- CASE
179- WHEN "song"."language" IN (${ languageList || "NULL" } )
180- THEN 0.15
181- ELSE 0
182- END +
183-
184- -- ❤️ USER SIGNALS
185- ("HistorySong"."completionRate" / 100) * 0.1 +
186- CASE WHEN "HistorySong"."likeStatus" = true THEN 0.1 ELSE 0 END
187- )
188- ` ) ,
189- "DESC" ,
190- ] ,
191- ] ,
192- limit,
193- } )
194- }
195-
196126const getHistorySongs = async ( req , res ) => {
197127 try {
198128 const userId = req . user . userid
199- const { page = 1 , limit = 10 , searchQuery = "" } = req . query
200-
201- const whereClause = { userId, songRefId : { [ Op . ne ] : null } }
202-
203- const includeOptions = {
204- model : Song ,
205- as : "song" ,
206- attributes : [ "songData" , "name" , "artistNames" ] ,
207- required : true ,
208- }
209-
210- if ( searchQuery ?. trim ( ) ) {
211- const q = searchQuery . toLowerCase ( ) . trim ( )
212- includeOptions . where = {
213- [ Op . or ] : [
214- sequelize . literal ( `similarity("song"."name", ${ sequelize . escape ( q ) } ) > 0.2` ) ,
215- sequelize . literal ( `similarity("song"."artistNames", ${ sequelize . escape ( q ) } ) > 0.2` ) ,
216- sequelize . literal ( `similarity("song"."albumName", ${ sequelize . escape ( q ) } ) > 0.2` ) ,
217- ] ,
218- }
129+ const pageNum = parseInt ( req . query . page , 10 ) || 1
130+ const limitNum = parseInt ( req . query . limit , 10 ) || 10
131+ const searchQuery = req . query . searchQuery ?. trim ( ) || ""
132+ const offset = ( pageNum - 1 ) * limitNum
133+
134+ let query
135+ let replacements = { userId, limit : limitNum , offset }
136+
137+ if ( searchQuery ) {
138+ query = `
139+ WITH matched_songs AS (
140+ SELECT id, "songData",
141+ GREATEST(
142+ similarity(name, :search),
143+ similarity("artistNames", :search),
144+ similarity("albumName", :search)
145+ ) AS score
146+ FROM songs
147+ WHERE name % :search
148+ OR "artistNames" % :search
149+ OR "albumName" % :search
150+ )
151+ SELECT
152+ s."songData",
153+ COUNT(*) OVER() AS total_count
154+ FROM history_songs hs
155+ INNER JOIN matched_songs s ON s.id = hs."songRefId"
156+ WHERE hs."userId" = :userId
157+ ORDER BY s.score DESC, hs."lastPlayedAt" DESC
158+ LIMIT :limit OFFSET :offset
159+ `
160+ replacements . search = searchQuery . toLowerCase ( )
161+ } else {
162+ query = `
163+ SELECT
164+ s."songData",
165+ COUNT(*) OVER() AS total_count
166+ FROM history_songs hs
167+ INNER JOIN songs s ON s.id = hs."songRefId"
168+ WHERE hs."userId" = :userId AND hs."songRefId" IS NOT NULL
169+ ORDER BY hs."lastPlayedAt" DESC
170+ LIMIT :limit OFFSET :offset
171+ `
219172 }
220173
221- const offset = ( parseInt ( page , 10 ) - 1 ) * parseInt ( limit , 10 )
222-
223- const historySongs = await HistorySong . findAndCountAll ( {
224- where : whereClause ,
225- include : [ includeOptions ] ,
226- limit : parseInt ( limit , 10 ) ,
227- offset,
228- order : [ [ "lastPlayedAt" , "DESC" ] ] ,
229- distinct : true ,
230- subQuery : false ,
174+ const results = await sequelize . query ( query , {
175+ replacements,
176+ type : sequelize . QueryTypes . SELECT ,
231177 } )
232178
179+ const totalCount = results [ 0 ] ?. total_count || 0
180+
233181 res . status ( 200 ) . json ( {
234182 status : "success" ,
235183 data : {
236- songs : historySongs . rows . map ( ( r ) => r . song ? .songData ) ,
237- count : historySongs . count ,
238- currentPage : parseInt ( page , 10 ) ,
239- totalPages : Math . ceil ( historySongs . count / parseInt ( limit , 10 ) ) ,
184+ songs : results . map ( ( r ) => r . songData ) ,
185+ count : parseInt ( totalCount , 10 ) ,
186+ currentPage : pageNum ,
187+ totalPages : Math . ceil ( totalCount / limitNum ) ,
240188 } ,
241189 } )
242190 } catch ( error ) {
@@ -245,48 +193,6 @@ const getHistorySongs = async (req, res) => {
245193 }
246194}
247195
248- const getRecentContext = async ( userId ) => {
249- const recent = await HistorySong . findAll ( {
250- where : {
251- userId,
252- lastPlayedAt : {
253- [ Op . gte ] : new Date ( Date . now ( ) - 24 * 60 * 60 * 1000 ) , // last 24h
254- } ,
255- } ,
256- include : [
257- {
258- model : Song ,
259- as : "song" ,
260- attributes : [ "artistNames" , "language" ] ,
261- required : true ,
262- } ,
263- ] ,
264- limit : 20 ,
265- } )
266-
267- const artists = new Set ( )
268- const languages = new Set ( )
269-
270- for ( const r of recent ) {
271- const artistNames = r . song ?. artistNames
272- if ( artistNames ) {
273- artistNames . split ( "," ) . forEach ( ( a ) => {
274- const name = a . trim ( )
275- if ( name ) artists . add ( name )
276- } )
277- }
278-
279- if ( r . song ?. language ) {
280- languages . add ( r . song . language )
281- }
282- }
283-
284- return {
285- artists : [ ...artists ] ,
286- languages : [ ...languages ] ,
287- }
288- }
289-
290196const updateLikeStatus = async ( req , res ) => {
291197 try {
292198 const userId = req . user . userid
0 commit comments