Skip to content

Commit bc56749

Browse files
committed
update version to 1.7.2; refactor fetchPlaylistSongs to return SongPagedResponse; add structured search methods for songs, artists, and albums; enhance voice search handling in MusicService
1 parent 7dbba40 commit bc56749

3 files changed

Lines changed: 118 additions & 7 deletions

File tree

src/app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ android {
1616
minSdk = 21
1717
targetSdk = 35
1818
versionCode = 11
19-
versionName = "1.7.1"
19+
versionName = "1.7.2"
2020

2121
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2222
vectorDrawables {

src/app/src/main/java/com/melodee/autoplayer/data/api/NetworkModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ object NetworkModule {
154154

155155
// Handle 401 Unauthorized responses with a single refresh attempt
156156
if (!skipAuth && response.code == 401) {
157-
response.close()
158157
val refreshed = attemptTokenRefresh()
159158
if (refreshed) {
159+
response.close()
160160
val retry = original.newBuilder()
161161
.method(original.method, original.body)
162162
.header("Authorization", "Bearer $authToken")

src/app/src/main/java/com/melodee/autoplayer/service/MusicService.kt

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import com.melodee.autoplayer.MelodeeApplication
3737
import kotlinx.coroutines.*
3838
import kotlinx.coroutines.flow.map
3939

40+
import android.provider.MediaStore
41+
4042
class 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

Comments
 (0)