@@ -37,6 +37,8 @@ import com.melodee.autoplayer.MelodeeApplication
3737import kotlinx.coroutines.*
3838import kotlinx.coroutines.flow.map
3939
40+ import android.provider.MediaStore
41+
4042class MusicService : MediaBrowserServiceCompat () {
4143 private var player: ExoPlayer ? = null
4244 private var currentSong: Song ? = null
@@ -477,7 +479,8 @@ class MusicService : MediaBrowserServiceCompat() {
477479 Log .d(" MusicService" , " Fetching songs for playlist: $playlistId " )
478480 // Set the current browsing playlist ID
479481 currentBrowsingPlaylistId = playlistId
480- val songs = fetchPlaylistSongs(playlistId)
482+ val response = fetchPlaylistSongs(playlistId)
483+ val songs = response.data
481484
482485 // Cache the songs for later use in onPlayFromMediaId
483486 currentBrowsingPlaylistSongs = songs
@@ -539,7 +542,7 @@ class MusicService : MediaBrowserServiceCompat() {
539542 }
540543 }
541544
542- private suspend fun fetchPlaylistSongs (playlistId : String ): List < Song > {
545+ private suspend fun fetchPlaylistSongs (playlistId : String ): SongPagedResponse {
543546 return withContext(Dispatchers .IO ) {
544547 try {
545548 Log .d(" MusicService" , " Fetching songs for playlist: $playlistId " )
@@ -560,7 +563,7 @@ class MusicService : MediaBrowserServiceCompat() {
560563 Log .d(" MusicService" , " Sample song: ${song.title} - Stream URL: ${song.streamUrl} " )
561564 }
562565
563- return @withContext songsResponse.data
566+ return @withContext songsResponse
564567
565568 } catch (e: Exception ) {
566569 Log .e(" MusicService" , " Failed to fetch playlist songs for $playlistId " , e)
@@ -617,6 +620,77 @@ class MusicService : MediaBrowserServiceCompat() {
617620 }
618621 }
619622
623+ private suspend fun performStructuredSearch (title : String , artistName : String ): List <MediaBrowserCompat .MediaItem > {
624+ return withContext(Dispatchers .IO ) {
625+ try {
626+ val musicApi = NetworkModule .getMusicApi()
627+
628+ // 1. Find the artist first to get their ID
629+ Log .d(" MusicService" , " Looking up artist: $artistName " )
630+ val artistResponse = musicApi.getArtists(query = artistName, page = 1 , pageSize = 5 )
631+ val artist = artistResponse.data.firstOrNull { it.name.equals(artistName, ignoreCase = true ) }
632+ ? : artistResponse.data.firstOrNull()
633+
634+ if (artist != null ) {
635+ Log .d(" MusicService" , " Found artist: ${artist.name} (${artist.id} )" )
636+ // 2. Search for the song filtering by this artist
637+ val songsResponse = musicApi.searchSongs(
638+ query = title,
639+ page = 1 ,
640+ pageSize = 10 ,
641+ artistId = artist.id.toString()
642+ )
643+
644+ if (songsResponse.data.isNotEmpty()) {
645+ Log .i(" MusicService" , " Found ${songsResponse.data.size} songs matching '$title ' by '${artist.name} '" )
646+ return @withContext songsResponse.data.map { createMediaItem(it, fromSearch = true ) }
647+ }
648+ }
649+
650+ // Fallback: If structured search fails (e.g. artist not found or exact match failed),
651+ // try a combined text search
652+ Log .d(" MusicService" , " Structured search yielded no results, falling back to combined query" )
653+ return @withContext performApiSearch(" $title $artistName " )
654+
655+ } catch (e: Exception ) {
656+ Log .e(" MusicService" , " Structured search failed" , e)
657+ return @withContext performApiSearch(" $title $artistName " )
658+ }
659+ }
660+ }
661+
662+ private suspend fun performArtistSearch (artistName : String ): List <MediaBrowserCompat .MediaItem > {
663+ return withContext(Dispatchers .IO ) {
664+ try {
665+ val musicApi = NetworkModule .getMusicApi()
666+ Log .d(" MusicService" , " Searching for artist: $artistName " )
667+
668+ // Get artist
669+ val artistResponse = musicApi.getArtists(query = artistName, page = 1 , pageSize = 1 )
670+ val artist = artistResponse.data.firstOrNull()
671+
672+ if (artist != null ) {
673+ Log .d(" MusicService" , " Found artist: ${artist.name} , fetching top songs" )
674+ // Get artist's songs
675+ val songsResponse = musicApi.getArtistSongs(artist.id.toString(), 1 , 50 )
676+ return @withContext songsResponse.data.map { createMediaItem(it, fromSearch = true ) }
677+ }
678+
679+ return @withContext emptyList()
680+ } catch (e: Exception ) {
681+ Log .e(" MusicService" , " Artist search failed" , e)
682+ return @withContext emptyList()
683+ }
684+ }
685+ }
686+
687+ private suspend fun performAlbumSearch (albumName : String , artistName : String? ): List <MediaBrowserCompat .MediaItem > {
688+ // For now, fall back to text search as we don't have a direct album search endpoint exposed easily
689+ // in the current MusicApi interface without searching artists first
690+ val query = if (artistName != null ) " $albumName $artistName " else albumName
691+ return performApiSearch(query)
692+ }
693+
620694 private suspend fun performApiSearch (query : String ): List <MediaBrowserCompat .MediaItem > {
621695 return withContext(Dispatchers .IO ) {
622696 try {
@@ -1590,6 +1664,16 @@ class MusicService : MediaBrowserServiceCompat() {
15901664 mediaSession = MediaSessionCompat (this , " Melodee" )
15911665 Log .d(" MusicService" , " MediaSession created: ${mediaSession?.sessionToken} " )
15921666
1667+ // Set the session activity (opens when user taps the notification or Now Playing card)
1668+ val sessionIntent = Intent (this , MainActivity ::class .java)
1669+ val sessionPendingIntent = PendingIntent .getActivity(
1670+ this ,
1671+ 0 ,
1672+ sessionIntent,
1673+ PendingIntent .FLAG_IMMUTABLE or PendingIntent .FLAG_UPDATE_CURRENT
1674+ )
1675+ mediaSession?.setSessionActivity(sessionPendingIntent)
1676+
15931677 // Log MediaSession details for debugging
15941678 Log .i(" MusicService" , " === MEDIASESSION DEBUG INFO ===" )
15951679 Log .i(" MusicService" , " Session tag: Melodee" )
@@ -1698,7 +1782,28 @@ class MusicService : MediaBrowserServiceCompat() {
16981782 Log .i(" MusicService" , " Android Auto voice search request: '$query '" )
16991783 serviceScope.launch {
17001784 try {
1701- val searchResults = performApiSearch(query)
1785+ // Check for structured extras first (e.g. "Play [Song] by [Artist]")
1786+ val focus = extras?.getString(MediaStore .EXTRA_MEDIA_FOCUS )
1787+ val artist = extras?.getString(MediaStore .EXTRA_MEDIA_ARTIST )
1788+ val title = extras?.getString(MediaStore .EXTRA_MEDIA_TITLE )
1789+ val album = extras?.getString(MediaStore .EXTRA_MEDIA_ALBUM )
1790+
1791+ Log .i(" MusicService" , " Voice extras - Focus: $focus , Artist: $artist , Title: $title , Album: $album " )
1792+
1793+ val searchResults = if (focus == MediaStore .Audio .Media .ENTRY_CONTENT_TYPE && ! artist.isNullOrBlank() && ! title.isNullOrBlank()) {
1794+ Log .i(" MusicService" , " Detected structured query: Song '$title ' by Artist '$artist '" )
1795+ performStructuredSearch(title, artist)
1796+ } else if (focus == MediaStore .Audio .Artists .ENTRY_CONTENT_TYPE && ! artist.isNullOrBlank()) {
1797+ Log .i(" MusicService" , " Detected structured query: Artist '$artist '" )
1798+ performArtistSearch(artist)
1799+ } else if (focus == MediaStore .Audio .Albums .ENTRY_CONTENT_TYPE && ! album.isNullOrBlank()) {
1800+ Log .i(" MusicService" , " Detected structured query: Album '$album '" )
1801+ performAlbumSearch(album, artist)
1802+ } else {
1803+ Log .i(" MusicService" , " Performing standard text search for '$query '" )
1804+ performApiSearch(query)
1805+ }
1806+
17021807 if (searchResults.isNotEmpty()) {
17031808 // Find the first playable song in search results
17041809 val firstSong = searchResults.find { item ->
@@ -1889,7 +1994,13 @@ class MusicService : MediaBrowserServiceCompat() {
18891994 serviceScope.launch {
18901995 try {
18911996 Log .d(" MusicService" , " Fetching playlist songs..." )
1892- val playlistSongs = fetchPlaylistSongs(playlistId)
1997+ val response = fetchPlaylistSongs(playlistId)
1998+ val playlistSongs = response.data
1999+
2000+ // Initialize pagination state for continuous playback
2001+ remotePlaylistId = playlistId
2002+ hasMorePlaylistPages = response.meta.hasNext
2003+ nextPlaylistPage = response.meta.currentPage + 1
18932004
18942005 if (playlistSongs.isNotEmpty()) {
18952006 Log .i(" MusicService" , " Fetched ${playlistSongs.size} songs from playlist" )
0 commit comments