Skip to content

Commit 398ca02

Browse files
committed
feat(android): implement getArtistsAsync and remove getGenresAsync
1 parent e30d40a commit 398ca02

File tree

10 files changed

+455
-85
lines changed

10 files changed

+455
-85
lines changed

android/src/main/java/com/musiclibrary/MusicLibraryModule.kt

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import com.musiclibrary.tracks.GetTracks
1212
import com.musiclibrary.tracks.GetTracksByAlbum
1313
import com.musiclibrary.albums.GetAlbums
1414
import com.musiclibrary.artists.GetArtists
15-
import com.musiclibrary.genres.GetGenres
1615
import com.musiclibrary.tracks.GetTrackMetadataQuery
1716

1817
@ReactModule(name = MusicLibraryModule.NAME)
@@ -32,46 +31,37 @@ class MusicLibraryModule(reactContext: ReactApplicationContext) :
3231
}
3332
}
3433

35-
override fun getAlbumsAsync(options: ReadableMap, promise: Promise) {
34+
override fun getTrackMetadataAsync(trackId: String, promise: Promise) {
3635
throwUnlessPermissionsGranted(reactApplicationContext, isWrite = false) {
3736
withModuleScope(promise) {
38-
GetAlbums(reactApplicationContext, options.toAssetsOptions(), promise)
39-
.execute()
40-
}
41-
}
42-
}
43-
44-
override fun getArtistsAsync(options: ReadableMap, promise: Promise) {
45-
throwUnlessPermissionsGranted(reactApplicationContext, isWrite = false) {
46-
withModuleScope(promise) {
47-
GetArtists(reactApplicationContext, options.toAssetsOptions(), promise)
37+
GetTrackMetadataQuery(reactApplicationContext, trackId, promise)
4838
.execute()
4939
}
5040
}
5141
}
5242

53-
override fun getGenresAsync(options: ReadableMap, promise: Promise) {
43+
override fun getTracksByAlbumAsync(albumId: String, promise: Promise) {
5444
throwUnlessPermissionsGranted(reactApplicationContext, isWrite = false) {
5545
withModuleScope(promise) {
56-
GetGenres(reactApplicationContext, options.toAssetsOptions(), promise)
46+
GetTracksByAlbum(reactApplicationContext, albumId, promise)
5747
.execute()
5848
}
5949
}
6050
}
6151

62-
override fun getTrackMetadataAsync(trackId: String, promise: Promise) {
52+
override fun getAlbumsAsync(options: ReadableMap, promise: Promise) {
6353
throwUnlessPermissionsGranted(reactApplicationContext, isWrite = false) {
6454
withModuleScope(promise) {
65-
GetTrackMetadataQuery(reactApplicationContext, trackId, promise)
55+
GetAlbums(reactApplicationContext, options.toAssetsOptions(), promise)
6656
.execute()
6757
}
6858
}
6959
}
7060

71-
override fun getTracksByAlbumAsync(albumId: String, promise: Promise) {
61+
override fun getArtistsAsync(options: ReadableMap, promise: Promise) {
7262
throwUnlessPermissionsGranted(reactApplicationContext, isWrite = false) {
7363
withModuleScope(promise) {
74-
GetTracksByAlbum(reactApplicationContext, albumId, promise)
64+
GetArtists(reactApplicationContext, options.toAssetsOptions(), promise)
7565
.execute()
7666
}
7767
}

android/src/main/java/com/musiclibrary/artists/GetArtists.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.musiclibrary.artists
33
import android.content.Context
44
import com.facebook.react.bridge.Promise
55
import com.musiclibrary.models.AssetsOptions
6+
import com.musiclibrary.utils.DataConverter
67

78
internal class GetArtists(
89
private val context: Context,
@@ -12,8 +13,15 @@ internal class GetArtists(
1213

1314
fun execute() {
1415
try {
15-
// TODO: implement
16-
promise.resolve(null)
16+
val contentResolver = context.contentResolver
17+
val result = GetArtistsQuery.getArtists(contentResolver, options)
18+
19+
// Convert result to React Native bridge format
20+
val resultMap = DataConverter.paginatedResultToWritableMap(result) { artist ->
21+
DataConverter.artistToWritableMap(artist)
22+
}
23+
24+
promise.resolve(resultMap)
1725
} catch (e: Exception) {
1826
promise.reject("QUERY_ERROR", "Failed to query artists: ${e.message}", e)
1927
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package com.musiclibrary.artists
2+
3+
import android.content.ContentResolver
4+
import android.provider.MediaStore
5+
import android.net.Uri
6+
import android.provider.DocumentsContract
7+
import com.musiclibrary.models.*
8+
import androidx.core.net.toUri
9+
10+
object GetArtistsQuery {
11+
fun getArtists(
12+
contentResolver: ContentResolver,
13+
options: AssetsOptions,
14+
): PaginatedResult<Artist> {
15+
val projection = arrayOf(
16+
MediaStore.Audio.Artists._ID,
17+
MediaStore.Audio.Artists.ARTIST,
18+
MediaStore.Audio.Artists.NUMBER_OF_ALBUMS,
19+
MediaStore.Audio.Artists.NUMBER_OF_TRACKS,
20+
)
21+
22+
val selection = buildSelection(options)
23+
val selectionArgs = buildSelectionArgs(options)
24+
val sortOrder = buildSortOrder(options.sortBy)
25+
26+
val cursor = contentResolver.query(
27+
MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
28+
projection,
29+
selection,
30+
selectionArgs,
31+
sortOrder
32+
) ?: throw RuntimeException("Failed to query MediaStore: cursor is null")
33+
34+
val artists = mutableListOf<Artist>()
35+
var hasNextPage: Boolean
36+
var endCursor: String? = null
37+
val totalCount = cursor.count
38+
39+
cursor.use { c ->
40+
val idColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Artists._ID)
41+
val artistColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST)
42+
val albumCountColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Artists.NUMBER_OF_ALBUMS)
43+
val trackCountColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Artists.NUMBER_OF_TRACKS)
44+
45+
// Jump to the specified start position
46+
val foundAfter = if (options.after == null) {
47+
cursor.moveToFirst() // Move to the first record
48+
true
49+
} else {
50+
var found = false
51+
if (cursor.moveToFirst()) {
52+
do {
53+
val id = cursor.getLong(idColumn).toString()
54+
if (id == options.after) {
55+
found = true
56+
break
57+
}
58+
} while (cursor.moveToNext())
59+
}
60+
// Move to the next record after the specified after if found
61+
found && cursor.moveToNext()
62+
}
63+
64+
var count = 0
65+
val maxItems = options.first.coerceAtMost(1000) // Limit the maximum number of queries
66+
67+
while (foundAfter && count < maxItems) {
68+
try {
69+
val id = c.getLong(idColumn)
70+
val artistName = c.getString(artistColumn) ?: ""
71+
val albumCount = c.getInt(albumCountColumn)
72+
val trackCount = c.getInt(trackCountColumn)
73+
74+
// Skip invalid artists
75+
if (artistName.isEmpty() || trackCount == 0) {
76+
continue
77+
}
78+
79+
// Create an Artist
80+
val artist = Artist(
81+
id = id.toString(),
82+
title = artistName,
83+
albumCount = albumCount,
84+
trackCount = trackCount
85+
)
86+
87+
artists.add(artist)
88+
endCursor = id.toString()
89+
count++
90+
} catch (e: Exception) {
91+
continue
92+
}
93+
94+
if (!cursor.moveToNext()) break
95+
}
96+
97+
// Check if there are more data
98+
hasNextPage = !c.isAfterLast
99+
}
100+
101+
return PaginatedResult(
102+
items = artists,
103+
hasNextPage = hasNextPage,
104+
endCursor = endCursor,
105+
totalCount = totalCount
106+
)
107+
}
108+
109+
private fun buildSelection(options: AssetsOptions): String {
110+
val conditions = mutableListOf<String>()
111+
112+
// Only query artists that have tracks
113+
conditions.add("${MediaStore.Audio.Artists.NUMBER_OF_TRACKS} > 0")
114+
115+
// Directory filtering for artists is more complex since artists don't have direct path
116+
// We'll need to filter based on tracks by the artist
117+
if (!options.directory.isNullOrEmpty()) {
118+
// Filter artists that have tracks in the specified directory
119+
conditions.add("EXISTS (SELECT 1 FROM ${MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} WHERE ${MediaStore.Audio.Media.ARTIST_ID} = ${MediaStore.Audio.Artists._ID} AND ${MediaStore.Audio.Media.DATA} LIKE ?)")
120+
}
121+
122+
return conditions.joinToString(" AND ")
123+
}
124+
125+
private fun buildSelectionArgs(options: AssetsOptions): Array<String>? {
126+
val args = mutableListOf<String>()
127+
128+
if (!options.directory.isNullOrEmpty()) {
129+
val dir = if (options.directory.startsWith("content://")) {
130+
uriToFullPath(options.directory.toUri())
131+
} else {
132+
options.directory
133+
}
134+
135+
if (!dir.isNullOrEmpty()) {
136+
args.add("$dir%")
137+
}
138+
}
139+
140+
return if (args.isEmpty()) null else args.toTypedArray()
141+
}
142+
143+
private fun uriToFullPath(treeUri: Uri): String? {
144+
val docId = DocumentsContract.getTreeDocumentId(treeUri) // "primary:Music/abc"
145+
val parts = docId.split(":")
146+
if (parts.size < 2) return null
147+
148+
val type = parts[0]
149+
val relativePath = parts[1]
150+
151+
return when (type) {
152+
"primary" -> "/storage/emulated/0/$relativePath"
153+
else -> "/storage/$type/$relativePath"
154+
}
155+
}
156+
157+
private fun buildSortOrder(sortBy: List<String>): String {
158+
if (sortBy.isEmpty()) {
159+
return "${MediaStore.Audio.Artists.ARTIST} ASC"
160+
}
161+
162+
return sortBy.joinToString(", ") { sortOption ->
163+
val parts = sortOption.split(" ")
164+
require(parts.size == 2) { "sortBy should be 'key order'" }
165+
166+
val column = when (parts[0].lowercase()) {
167+
"default" -> MediaStore.Audio.Artists.ARTIST
168+
"artist" -> MediaStore.Audio.Artists.ARTIST
169+
"album_count" -> MediaStore.Audio.Artists.NUMBER_OF_ALBUMS
170+
"track_count" -> MediaStore.Audio.Artists.NUMBER_OF_TRACKS
171+
else -> throw IllegalArgumentException("Unsupported SortKey: ${parts[0]}")
172+
}
173+
174+
val order = parts[1].uppercase()
175+
require(order == "ASC" || order == "DESC") { "Sort By must be ASC or DESC" }
176+
177+
"$column $order"
178+
}
179+
}
180+
}

android/src/main/java/com/musiclibrary/genres/GetGenres.kt

Lines changed: 0 additions & 21 deletions
This file was deleted.

example/src/navigation/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import TrackListScreen from '../pages/TrackListScreen';
55
import PlayerScreen from '../pages/PlayerScreen';
66
import AlbumListScreen from '../pages/AlbumListScreen';
77
import AlbumTrackListScreen from '../pages/AlbumTrackListScreen';
8+
import ArtistListScreen from '../pages/ArtistListScreen';
89
import type { Album } from '@nodefinity/react-native-music-library';
910

1011
export type RootStackParamList = {
1112
Home: undefined;
1213
TrackList: undefined;
1314
AlbumList: undefined;
1415
AlbumTrackList: { album: Album };
16+
ArtistList: undefined;
1517
Player: undefined;
1618
};
1719

@@ -50,6 +52,14 @@ export default function Navigation() {
5052
headerBackTitle: 'Back',
5153
}}
5254
/>
55+
<Stack.Screen
56+
name="ArtistList"
57+
component={ArtistListScreen}
58+
options={{
59+
title: 'Artist List',
60+
headerBackTitle: 'Back',
61+
}}
62+
/>
5363
<Stack.Screen
5464
name="Player"
5565
component={PlayerScreen}

0 commit comments

Comments
 (0)