Skip to content

Commit 463c85c

Browse files
committed
feat(android): implement getTracksByAlbumAsync
1 parent 5d73dae commit 463c85c

File tree

11 files changed

+391
-85
lines changed

11 files changed

+391
-85
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.musiclibrary.albums.GetAlbums
1313
import com.musiclibrary.artists.GetArtists
1414
import com.musiclibrary.genres.GetGenres
1515
import com.musiclibrary.tracks.GetTrackMetadataQuery
16+
import com.musiclibrary.albums.GetTracksByAlbum
1617

1718
@ReactModule(name = MusicLibraryModule.NAME)
1819
class MusicLibraryModule(reactContext: ReactApplicationContext) :
@@ -67,6 +68,15 @@ class MusicLibraryModule(reactContext: ReactApplicationContext) :
6768
}
6869
}
6970

71+
override fun getTracksByAlbumAsync(albumId: String, promise: Promise) {
72+
throwUnlessPermissionsGranted(reactApplicationContext, isWrite = false) {
73+
withModuleScope(promise) {
74+
GetTracksByAlbum(reactApplicationContext, albumId, promise)
75+
.execute()
76+
}
77+
}
78+
}
79+
7080
companion object {
7181
const val NAME = "MusicLibrary"
7282
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.musiclibrary.albums
2+
3+
import android.content.Context
4+
import com.facebook.react.bridge.Promise
5+
import com.facebook.react.bridge.Arguments
6+
import com.musiclibrary.utils.DataConverter
7+
8+
internal class GetTracksByAlbum(
9+
private val context: Context,
10+
private val albumId: String,
11+
private val promise: Promise
12+
) {
13+
14+
fun execute() {
15+
try {
16+
val contentResolver = context.contentResolver
17+
val tracks = GetTracksByAlbumQuery.getTracksByAlbum(contentResolver, albumId)
18+
19+
// Convert tracks to React Native bridge format
20+
val tracksArray = Arguments.createArray()
21+
tracks.forEach { track ->
22+
tracksArray.pushMap(DataConverter.trackToWritableMap(track))
23+
}
24+
25+
promise.resolve(tracksArray)
26+
} catch (e: Exception) {
27+
promise.reject("QUERY_ERROR", "Failed to query tracks by album: ${e.message}", e)
28+
}
29+
}
30+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.musiclibrary.albums
2+
3+
import android.content.ContentResolver
4+
import android.provider.MediaStore
5+
import com.musiclibrary.models.Track
6+
7+
object GetTracksByAlbumQuery {
8+
fun getTracksByAlbum(
9+
contentResolver: ContentResolver,
10+
albumId: String,
11+
): List<Track> {
12+
val projection = arrayOf(
13+
MediaStore.Audio.Media._ID,
14+
MediaStore.Audio.Media.TITLE,
15+
MediaStore.Audio.Media.ARTIST,
16+
MediaStore.Audio.Media.ALBUM,
17+
MediaStore.Audio.Media.DURATION,
18+
MediaStore.Audio.Media.DATA,
19+
MediaStore.Audio.Media.DATE_ADDED,
20+
MediaStore.Audio.Media.SIZE,
21+
MediaStore.Audio.Media.TRACK,
22+
)
23+
24+
val selection = "${MediaStore.Audio.Media.ALBUM_ID} = ? AND ${MediaStore.Audio.Media.IS_MUSIC} = 1 AND ${MediaStore.Audio.Media.DURATION} > 0"
25+
val selectionArgs = arrayOf(albumId)
26+
val sortOrder = "${MediaStore.Audio.Media.TRACK} ASC, ${MediaStore.Audio.Media.TITLE} ASC"
27+
28+
val cursor = contentResolver.query(
29+
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
30+
projection,
31+
selection,
32+
selectionArgs,
33+
sortOrder
34+
) ?: throw RuntimeException("Failed to query tracks: cursor is null")
35+
36+
val tracks = mutableListOf<Track>()
37+
38+
cursor.use { c ->
39+
val idColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
40+
val titleColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
41+
val artistColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
42+
val albumColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)
43+
val durationColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
44+
val dataColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)
45+
val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED)
46+
val sizeColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
47+
48+
while (c.moveToNext()) {
49+
try {
50+
val id = c.getLong(idColumn)
51+
val data = c.getString(dataColumn) ?: ""
52+
53+
// Skip invalid data
54+
if (data.isEmpty()) {
55+
continue
56+
}
57+
58+
val title = c.getString(titleColumn) ?: ""
59+
val artist = c.getString(artistColumn)
60+
val album = c.getString(albumColumn)
61+
val duration = c.getLong(durationColumn) / 1000.0 // Convert to seconds
62+
val dateAdded = c.getLong(dateAddedColumn)
63+
val fileSize = c.getLong(sizeColumn)
64+
val artworkUri = "content://media/external/audio/media/${id}/albumart"
65+
66+
// Create a Track
67+
val track = Track(
68+
id = id.toString(),
69+
title = title,
70+
artist = artist,
71+
artwork = artworkUri,
72+
album = album,
73+
duration = duration,
74+
url = "file://$data",
75+
createdAt = dateAdded * 1000, // Convert to milliseconds
76+
modifiedAt = dateAdded * 1000, // Convert to milliseconds
77+
fileSize = fileSize
78+
)
79+
80+
tracks.add(track)
81+
} catch (e: Exception) {
82+
continue
83+
}
84+
}
85+
}
86+
87+
return tracks
88+
}
89+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
2+
import type { Track } from '@nodefinity/react-native-music-library';
3+
4+
interface TrackItemProps {
5+
track: Track;
6+
onPress: (track: Track) => void;
7+
}
8+
9+
export default function TrackItem({ track, onPress }: TrackItemProps) {
10+
const formatDuration = (totalSeconds: number) => {
11+
const minutes = Math.floor(totalSeconds / 60);
12+
const seconds = Math.floor(totalSeconds % 60);
13+
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
14+
};
15+
16+
return (
17+
<TouchableOpacity style={styles.trackItem} onPress={() => onPress(track)}>
18+
<Image
19+
source={
20+
track.artwork
21+
? {
22+
uri: track.artwork,
23+
}
24+
: require('../assets/default_artwork.png')
25+
}
26+
style={styles.cover}
27+
defaultSource={require('../assets/default_artwork.png')}
28+
/>
29+
<View style={styles.trackInfo}>
30+
<Text style={styles.trackTitle} numberOfLines={1}>
31+
{track.title}
32+
</Text>
33+
<Text style={styles.trackArtist} numberOfLines={1}>
34+
{track.artist} - {track.album}
35+
</Text>
36+
</View>
37+
<Text style={styles.duration}>{formatDuration(track.duration)}</Text>
38+
</TouchableOpacity>
39+
);
40+
}
41+
42+
const styles = StyleSheet.create({
43+
trackItem: {
44+
flexDirection: 'row',
45+
alignItems: 'center',
46+
padding: 12,
47+
borderBottomWidth: 1,
48+
borderBottomColor: '#eee',
49+
},
50+
cover: {
51+
width: 50,
52+
height: 50,
53+
borderRadius: 4,
54+
marginRight: 12,
55+
},
56+
trackInfo: {
57+
flex: 1,
58+
marginRight: 8,
59+
},
60+
trackTitle: {
61+
fontSize: 16,
62+
fontWeight: 'bold',
63+
marginBottom: 4,
64+
},
65+
trackArtist: {
66+
fontSize: 14,
67+
color: '#666',
68+
},
69+
duration: {
70+
fontSize: 14,
71+
color: '#666',
72+
marginLeft: 8,
73+
},
74+
});

example/src/navigation/index.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import HomeScreen from '../pages/HomeScreen';
44
import TrackListScreen from '../pages/TrackListScreen';
55
import PlayerScreen from '../pages/PlayerScreen';
66
import AlbumListScreen from '../pages/AlbumListScreen';
7+
import AlbumTrackListScreen from '../pages/AlbumTrackListScreen';
8+
import type { Album } from '@nodefinity/react-native-music-library';
79

810
export type RootStackParamList = {
911
Home: undefined;
1012
TrackList: undefined;
1113
AlbumList: undefined;
14+
AlbumTrackList: { album: Album };
1215
Player: undefined;
1316
};
1417

@@ -39,6 +42,14 @@ export default function Navigation() {
3942
headerBackTitle: 'Back',
4043
}}
4144
/>
45+
<Stack.Screen
46+
name="AlbumTrackList"
47+
component={AlbumTrackListScreen}
48+
options={{
49+
title: 'Album Tracks',
50+
headerBackTitle: 'Back',
51+
}}
52+
/>
4253
<Stack.Screen
4354
name="Player"
4455
component={PlayerScreen}

example/src/pages/AlbumListScreen.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ import type {
1717
import { usePermission } from '../hooks/usePermission';
1818
import { pickDirectory } from '@react-native-documents/picker';
1919
import { SafeAreaView } from 'react-native-safe-area-context';
20+
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
21+
import type { RootStackParamList } from '../navigation';
2022

21-
export default function AlbumListScreen() {
23+
type Props = NativeStackScreenProps<RootStackParamList, 'AlbumList'>;
24+
25+
export default function AlbumListScreen({ navigation }: Props) {
2226
const [albums, setAlbums] = useState<Album[]>([]);
2327
const [loading, setLoading] = useState(false);
2428
const { permissionStatus, requestPermissions } = usePermission();
@@ -96,12 +100,24 @@ export default function AlbumListScreen() {
96100
}
97101
};
98102

103+
const handleAlbumPress = async (album: Album) => {
104+
try {
105+
console.log('Loading album tracks for:', album.title);
106+
107+
// Navigate to album track list with only album info
108+
navigation.navigate('AlbumTrackList', {
109+
album: album,
110+
});
111+
} catch (error) {
112+
console.error('Failed to load album tracks:', error);
113+
Alert.alert('Error', 'Failed to load album tracks');
114+
}
115+
};
116+
99117
const renderAlbum = ({ item }: { item: Album }) => (
100118
<TouchableOpacity
101119
style={styles.albumItem}
102-
onPress={() => {
103-
console.log('item', item);
104-
}}
120+
onPress={() => handleAlbumPress(item)}
105121
>
106122
<Image
107123
source={
@@ -120,7 +136,8 @@ export default function AlbumListScreen() {
120136
{item.artist}
121137
</Text>
122138
<Text style={styles.albumDetails}>
123-
{item.trackCount} tracks, year: {item.year ? `${item.year}` : '-'}
139+
{item.trackCount} tracks
140+
{item.year && ` • ${item.year}`}
124141
</Text>
125142
</View>
126143
</TouchableOpacity>

0 commit comments

Comments
 (0)