@@ -292,11 +292,136 @@ function normalizeTagArtistItem(artist, tag) {
292292 } ;
293293}
294294
295+ function getTagArtistKey ( artist ) {
296+ const artistId = String ( artist ?. id || artist ?. mbid || "" ) . trim ( ) . toLowerCase ( ) ;
297+ if ( artistId ) return `id:${ artistId } ` ;
298+ const artistName = String ( artist ?. name || "" ) . trim ( ) . toLowerCase ( ) ;
299+ return artistName ? `name:${ artistName } ` : null ;
300+ }
301+
302+ function matchesTagSearch ( artist , normalizedTag ) {
303+ const tags = Array . isArray ( artist ?. tags ) ? artist . tags : [ ] ;
304+ const genres = Array . isArray ( artist ?. genres ) ? artist . genres : [ ] ;
305+ return [ ...tags , ...genres ] . some (
306+ ( entry ) => normalizeSearchText ( entry ) === normalizedTag ,
307+ ) ;
308+ }
309+
310+ function dedupeTagArtists ( artists ) {
311+ const seen = new Set ( ) ;
312+ const output = [ ] ;
313+ for ( const artist of Array . isArray ( artists ) ? artists : [ ] ) {
314+ const key = getTagArtistKey ( artist ) ;
315+ if ( ! key || seen . has ( key ) ) continue ;
316+ seen . add ( key ) ;
317+ output . push ( artist ) ;
318+ }
319+ return output ;
320+ }
321+
322+ function getTagSourceMap ( artists ) {
323+ const sourceMap = new Map ( ) ;
324+ for ( const artist of Array . isArray ( artists ) ? artists : [ ] ) {
325+ const key = getTagArtistKey ( artist ) ;
326+ if ( ! key || sourceMap . has ( key ) ) continue ;
327+ sourceMap . set ( key , artist ?. tagResultSource || "all" ) ;
328+ }
329+ return sourceMap ;
330+ }
331+
332+ function normalizeLastfmTagArtist ( artist , tag ) {
333+ let imageUrl = null ;
334+ if ( Array . isArray ( artist ?. image ) ) {
335+ const img =
336+ artist . image . find ( ( entry ) => entry . size === "extralarge" ) ||
337+ artist . image . find ( ( entry ) => entry . size === "large" ) ||
338+ artist . image . slice ( - 1 ) [ 0 ] ;
339+ if (
340+ img ?. [ "#text" ] &&
341+ ! String ( img [ "#text" ] ) . includes ( "2a96cbd8b46e442fc41c2b86b821562f" )
342+ ) {
343+ imageUrl = img [ "#text" ] ;
344+ }
345+ }
346+
347+ return normalizeTagArtistItem (
348+ {
349+ type : "artist" ,
350+ id : artist ?. mbid || null ,
351+ name : artist ?. name || "Unknown Artist" ,
352+ sortName : artist ?. name || "Unknown Artist" ,
353+ image : buildImageProxyUrl ( imageUrl ) || imageUrl ,
354+ imageUrl : buildImageProxyUrl ( imageUrl ) || imageUrl ,
355+ artistType : null ,
356+ country : null ,
357+ area : null ,
358+ begin : null ,
359+ end : null ,
360+ disambiguation : null ,
361+ tags : [ tag ] ,
362+ genres : [ tag ] ,
363+ inLibrary : false ,
364+ score : 0 ,
365+ tagResultSource : "all" ,
366+ } ,
367+ tag ,
368+ ) ;
369+ }
370+
371+ async function fetchMergedLastfmTagArtists ( tag , limitInt , offsetInt , recommendedItems ) {
372+ const pageSize = 50 ;
373+ const requiredCount = offsetInt + limitInt ;
374+ const supplementalItems = [ ] ;
375+ const seen = new Set ( recommendedItems . map ( ( artist ) => getTagArtistKey ( artist ) ) . filter ( Boolean ) ) ;
376+ let page = 1 ;
377+ let exhausted = false ;
378+
379+ while ( ! exhausted && recommendedItems . length + supplementalItems . length < requiredCount ) {
380+ const data = await lastfmRequest ( "tag.getTopArtists" , {
381+ tag,
382+ limit : pageSize ,
383+ page,
384+ } ) ;
385+ const artists = Array . isArray ( data ?. topartists ?. artist )
386+ ? data . topartists . artist
387+ : data ?. topartists ?. artist
388+ ? [ data . topartists . artist ]
389+ : [ ] ;
390+
391+ if ( artists . length === 0 ) {
392+ exhausted = true ;
393+ break ;
394+ }
395+
396+ for ( const artist of artists ) {
397+ const normalized = normalizeLastfmTagArtist ( artist , tag ) ;
398+ const key = getTagArtistKey ( normalized ) ;
399+ if ( ! key || seen . has ( key ) ) continue ;
400+ seen . add ( key ) ;
401+ supplementalItems . push ( normalized ) ;
402+ }
403+
404+ const reportedTotal = Number . parseInt ( data ?. topartists ?. [ "@attr" ] ?. total , 10 ) ;
405+ const totalPages = Number . isFinite ( reportedTotal )
406+ ? Math . ceil ( reportedTotal / pageSize )
407+ : null ;
408+ exhausted =
409+ artists . length < pageSize ||
410+ ( Number . isFinite ( totalPages ) && page >= totalPages ) ;
411+ page += 1 ;
412+ }
413+
414+ return {
415+ items : [ ...recommendedItems , ...supplementalItems ] ,
416+ exhausted,
417+ } ;
418+ }
419+
295420export async function searchTags (
296421 query ,
297422 limit = 24 ,
298423 offset = 0 ,
299- tagScope = "recommended " ,
424+ tagScope = "merged " ,
300425) {
301426 const tag = String ( query || "" ) . trim ( ) . replace ( / ^ # / , "" ) ;
302427 const limitInt = parsePositiveInt ( limit , 24 ) ;
@@ -312,6 +437,33 @@ export async function searchTags(
312437 } ;
313438 }
314439
440+ const discoveryCache = getDiscoveryCache ( ) ;
441+ const tagLower = normalizeSearchText ( tag ) ;
442+ const recommendedMatches = dedupeTagArtists (
443+ ( discoveryCache . recommendations || [ ] )
444+ . filter ( ( artist ) => matchesTagSearch ( artist , tagLower ) )
445+ . map ( ( artist ) =>
446+ normalizeTagArtistItem (
447+ {
448+ ...artist ,
449+ tagResultSource : "recommended" ,
450+ } ,
451+ tag ,
452+ ) ,
453+ ) ,
454+ ) ;
455+
456+ if ( tagScope === "recommended" ) {
457+ return {
458+ scope : "tag" ,
459+ query : tag ,
460+ count : recommendedMatches . length ,
461+ offset : offsetInt ,
462+ hasMore : offsetInt + limitInt < recommendedMatches . length ,
463+ items : recommendedMatches . slice ( offsetInt , offsetInt + limitInt ) ,
464+ } ;
465+ }
466+
315467 if ( tagScope === "all" ) {
316468 if ( getLastfmApiKey ( ) ) {
317469 const page = Math . floor ( offsetInt / limitInt ) + 1 ;
@@ -326,42 +478,7 @@ export async function searchTags(
326478 ? [ data . topartists . artist ]
327479 : [ ] ;
328480 const items = artists
329- . map ( ( artist ) => {
330- let imageUrl = null ;
331- if ( Array . isArray ( artist ?. image ) ) {
332- const img =
333- artist . image . find ( ( entry ) => entry . size === "extralarge" ) ||
334- artist . image . find ( ( entry ) => entry . size === "large" ) ||
335- artist . image . slice ( - 1 ) [ 0 ] ;
336- if (
337- img ?. [ "#text" ] &&
338- ! String ( img [ "#text" ] ) . includes ( "2a96cbd8b46e442fc41c2b86b821562f" )
339- ) {
340- imageUrl = img [ "#text" ] ;
341- }
342- }
343- return normalizeTagArtistItem (
344- {
345- type : "artist" ,
346- id : artist ?. mbid || null ,
347- name : artist ?. name || "Unknown Artist" ,
348- sortName : artist ?. name || "Unknown Artist" ,
349- image : buildImageProxyUrl ( imageUrl ) || imageUrl ,
350- imageUrl : buildImageProxyUrl ( imageUrl ) || imageUrl ,
351- artistType : null ,
352- country : null ,
353- area : null ,
354- begin : null ,
355- end : null ,
356- disambiguation : null ,
357- tags : [ tag ] ,
358- genres : [ tag ] ,
359- inLibrary : false ,
360- score : 0 ,
361- } ,
362- tag ,
363- ) ;
364- } )
481+ . map ( ( artist ) => normalizeLastfmTagArtist ( artist , tag ) )
365482 . filter ( ( artist ) => artist . id ) ;
366483
367484 return {
@@ -370,63 +487,85 @@ export async function searchTags(
370487 count :
371488 Number . parseInt ( data ?. topartists ?. [ "@attr" ] ?. total , 10 ) || items . length ,
372489 offset : offsetInt ,
490+ hasMore :
491+ offsetInt + items . length <
492+ ( Number . parseInt ( data ?. topartists ?. [ "@attr" ] ?. total , 10 ) || items . length ) ,
373493 items,
374494 } ;
375495 }
376496
377- const discoveryCache = getDiscoveryCache ( ) ;
378- const tagLower = normalizeSearchText ( tag ) ;
379- const pool = [
497+ const sourceMap = getTagSourceMap ( recommendedMatches ) ;
498+ const poolItems = dedupeTagArtists ( [
380499 ...( Array . isArray ( discoveryCache . recommendations )
381500 ? discoveryCache . recommendations
382501 : [ ] ) ,
383502 ...( Array . isArray ( discoveryCache . globalTop ) ? discoveryCache . globalTop : [ ] ) ,
384503 ...( Array . isArray ( discoveryCache . basedOn ) ? discoveryCache . basedOn : [ ] ) ,
385- ] ;
386- const seen = new Set ( ) ;
387- const matches = pool . filter ( ( artist ) => {
388- const artistId = String ( artist ?. id || artist ?. mbid || "" ) . trim ( ) . toLowerCase ( ) ;
389- const artistName = String ( artist ?. name || "" ) . trim ( ) . toLowerCase ( ) ;
390- const key = artistId || artistName ;
391- if ( ! key || seen . has ( key ) ) return false ;
392- const tags = Array . isArray ( artist . tags ) ? artist . tags : [ ] ;
393- const genres = Array . isArray ( artist . genres ) ? artist . genres : [ ] ;
394- const matched = [ ...tags , ...genres ] . some (
395- ( entry ) => normalizeSearchText ( entry ) === tagLower ,
504+ ] )
505+ . filter ( ( artist ) => matchesTagSearch ( artist , tagLower ) )
506+ . map ( ( artist ) =>
507+ normalizeTagArtistItem (
508+ {
509+ ...artist ,
510+ tagResultSource : sourceMap . get ( getTagArtistKey ( artist ) ) || "all" ,
511+ } ,
512+ tag ,
513+ ) ,
396514 ) ;
397- if ( ! matched ) return false ;
398- seen . add ( key ) ;
399- return true ;
400- } ) ;
401515
402516 return {
403517 scope : "tag" ,
404518 query : tag ,
405- count : matches . length ,
519+ count : poolItems . length ,
406520 offset : offsetInt ,
407- items : matches
408- . slice ( offsetInt , offsetInt + limitInt )
409- . map ( ( artist ) => normalizeTagArtistItem ( artist , tag ) ) ,
521+ hasMore : offsetInt + limitInt < poolItems . length ,
522+ items : poolItems . slice ( offsetInt , offsetInt + limitInt ) ,
410523 } ;
411524 }
412525
413- const discoveryCache = getDiscoveryCache ( ) ;
414- const tagLower = normalizeSearchText ( tag ) ;
415- const matches = ( discoveryCache . recommendations || [ ] ) . filter ( ( artist ) => {
416- const tags = Array . isArray ( artist . tags ) ? artist . tags : [ ] ;
417- const genres = Array . isArray ( artist . genres ) ? artist . genres : [ ] ;
418- return [ ...tags , ...genres ] . some (
419- ( entry ) => normalizeSearchText ( entry ) === tagLower ,
526+ if ( getLastfmApiKey ( ) ) {
527+ const merged = await fetchMergedLastfmTagArtists (
528+ tag ,
529+ limitInt ,
530+ offsetInt ,
531+ recommendedMatches ,
532+ ) ;
533+ const items = merged . items . slice ( offsetInt , offsetInt + limitInt ) ;
534+ return {
535+ scope : "tag" ,
536+ query : tag ,
537+ count : merged . exhausted
538+ ? merged . items . length
539+ : offsetInt + items . length + ( merged . items . length > offsetInt + items . length ? 1 : 0 ) ,
540+ offset : offsetInt ,
541+ hasMore : ! merged . exhausted || offsetInt + items . length < merged . items . length ,
542+ items,
543+ } ;
544+ }
545+
546+ const mergedItems = dedupeTagArtists ( [
547+ ...recommendedMatches ,
548+ ...( Array . isArray ( discoveryCache . globalTop ) ? discoveryCache . globalTop : [ ] ) ,
549+ ...( Array . isArray ( discoveryCache . basedOn ) ? discoveryCache . basedOn : [ ] ) ,
550+ ] )
551+ . filter ( ( artist ) => matchesTagSearch ( artist , tagLower ) )
552+ . map ( ( artist ) =>
553+ normalizeTagArtistItem (
554+ {
555+ ...artist ,
556+ tagResultSource :
557+ artist . tagResultSource === "recommended" ? "recommended" : "all" ,
558+ } ,
559+ tag ,
560+ ) ,
420561 ) ;
421- } ) ;
422562
423563 return {
424564 scope : "tag" ,
425565 query : tag ,
426- count : matches . length ,
566+ count : mergedItems . length ,
427567 offset : offsetInt ,
428- items : matches
429- . slice ( offsetInt , offsetInt + limitInt )
430- . map ( ( artist ) => normalizeTagArtistItem ( artist , tag ) ) ,
568+ hasMore : offsetInt + limitInt < mergedItems . length ,
569+ items : mergedItems . slice ( offsetInt , offsetInt + limitInt ) ,
431570 } ;
432571}
0 commit comments