Skip to content

Commit c7e4dfe

Browse files
committed
feat: support to parse lyrics
1 parent 543e0b0 commit c7e4dfe

File tree

12 files changed

+328
-184
lines changed

12 files changed

+328
-184
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ A powerful React Native library for accessing local music files with full metada
1111

1212
## Features & Roadmap
1313

14-
- [x] 🎵 Access local music library with rich metadata
14+
- [x] 🎵 Access local music library with rich metadata(including lyrics)
1515
- [x] 🚀 Built with TurboModules for maximum performance
1616
- [x] 📄 Pagination support for large music collections
1717
- [x] 🔍 Flexible sorting and filtering options
@@ -122,10 +122,11 @@ console.log('Has more:', customResult.hasNextPage);
122122
interface Track {
123123
id: string;
124124
title: string; // Track title
125-
artwork: string; // Artwork file URI
126-
artist: string; // Artist name
127-
album: string; // Album name
128-
genre: string; // Music genre
125+
artist?: string; // Artist name
126+
artwork?: string; // Artwork file URI
127+
album?: string; // Album name
128+
genre?: string; // Music genre
129+
lyrics?: string // Lyrics
129130
duration: number; // Duration in seconds
130131
url: string; // File URL or path
131132
createdAt?: number; // Date added (Unix timestamp)

README_ZH.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
## 特性与路线图
1313

14-
- [x] 🎵 访问本地音乐库并获取丰富的元数据
14+
- [x] 🎵 访问本地音乐库并获取丰富的元数据(包括歌词)
1515
- [x] 🚀 基于 TurboModules 构建,性能卓越
1616
- [x] 📄 支持大型音乐集合的分页功能
1717
- [x] 🔍 灵活的排序和过滤选项
@@ -122,10 +122,11 @@ console.log('是否还有更多:', customResult.hasNextPage);
122122
interface Track {
123123
id: string;
124124
title: string; // 曲目标题
125-
artwork: string; // 专辑封面 URI
126-
artist: string; // 艺术家名称
127-
album: string; // 专辑名称
128-
genre: string; // 音乐类型
125+
artist?: string; // 艺术家名称
126+
artwork?: string; // 专辑封面 URI
127+
album?: string; // 专辑名称
128+
genre?: string; // 音乐流派
129+
lyrics?: string; // 歌词
129130
duration: number; // 持续时间(秒)
130131
url: string; // 文件 URL 或路径
131132
createdAt?: number; // 添加日期(Unix 时间戳)

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
7474
dependencies {
7575
implementation "com.facebook.react:react-android"
7676
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77+
implementation("net.jthink:jaudiotagger:3.0.1")
7778
}
7879

7980
react {

android/src/main/java/com/musiclibrary/models/Assets.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ package com.musiclibrary.models
33
data class Track(
44
val id: String,
55
val title: String,
6-
val artwork: String,
7-
val artist: String,
8-
val album: String,
9-
val genre: String,
6+
val artist: String? = null,
7+
val artwork: String? = null,
8+
val album: String? = null,
9+
val genre: String? = null,
10+
val lyrics: String? = null,
1011
val duration: Double,
1112
val url: String,
1213
val createdAt: Long? = null,

android/src/main/java/com/musiclibrary/tracks/GetTracksQuery.kt

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,30 @@ import android.net.Uri
77
import android.provider.DocumentsContract
88
import com.musiclibrary.models.*
99
import androidx.core.net.toUri
10+
import java.io.File
11+
import org.jaudiotagger.audio.AudioFileIO
12+
import org.jaudiotagger.tag.FieldKey
13+
import java.util.concurrent.Executors
14+
import java.util.concurrent.TimeUnit
1015

1116
object GetTracksQuery {
17+
// Create a thread pool
18+
private val executor = Executors.newFixedThreadPool(4)
19+
20+
private fun getAudioTagInfo(filePath: String): Pair<String?, String?> {
21+
return try {
22+
val audioFile = AudioFileIO.read(File(filePath))
23+
val tag = audioFile.tag
24+
25+
// Try to read the genre and lyrics from the tag
26+
val genre = tag?.getFirst(FieldKey.GENRE)
27+
val lyrics = tag?.getFirst(FieldKey.LYRICS)
28+
29+
Pair(genre, lyrics)
30+
} catch (e: Exception) {
31+
Pair(null, null)
32+
}
33+
}
1234

1335
fun getTracks(
1436
contentResolver: ContentResolver,
@@ -23,8 +45,7 @@ object GetTracksQuery {
2345
MediaStore.Audio.Media.DATA,
2446
MediaStore.Audio.Media.DATE_ADDED,
2547
MediaStore.Audio.Media.SIZE,
26-
MediaStore.Audio.Media.ALBUM_ID,
27-
MediaStore.Audio.Media.GENRE
48+
MediaStore.Audio.Media.ALBUM_ID
2849
)
2950

3051
val selection = buildSelection(options)
@@ -53,8 +74,6 @@ object GetTracksQuery {
5374
val dataColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)
5475
val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED)
5576
val sizeColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
56-
val genreColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.GENRE)
57-
val albumIdColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
5877

5978
// Jump to the specified start position
6079
val foundAfter = if (options.after == null) {
@@ -78,50 +97,80 @@ object GetTracksQuery {
7897
var count = 0
7998
val maxItems = options.first.coerceAtMost(1000) // Limit the maximum number of queries
8099

100+
val trackPaths = mutableListOf<Pair<String, String>>() // (id, path) pairs
101+
81102
while (foundAfter && count < maxItems) {
82103
try {
83104
val id = c.getLong(idColumn)
84-
val title = c.getString(titleColumn) ?: ""
85-
val artist = c.getString(artistColumn) ?: "Unknown Artist"
86-
val album = c.getString(albumColumn) ?: "Unknown Album"
87-
val duration = c.getLong(durationColumn) / 1000.0 // Convert to seconds
88105
val data = c.getString(dataColumn) ?: ""
89-
val dateAdded = c.getLong(dateAddedColumn)
90-
val fileSize = c.getLong(sizeColumn)
91-
val genre = c.getString(genreColumn) ?: ""
92-
val albumId = c.getLong(albumIdColumn)
93-
val artworkUri: Uri = "content://media/external/audio/media/${id}/albumart".toUri()
94106

95107
// Skip invalid data
96108
if (data.isEmpty()) {
97109
continue
98110
}
99111

112+
val title = c.getString(titleColumn) ?: ""
113+
val artist = c.getString(artistColumn)
114+
val album = c.getString(albumColumn)
115+
val duration = c.getLong(durationColumn) / 1000.0 // Convert to seconds
116+
val dateAdded = c.getLong(dateAddedColumn)
117+
val fileSize = c.getLong(sizeColumn)
118+
val artworkUri: Uri = "content://media/external/audio/media/${id}/albumart".toUri()
119+
120+
// Create a Track without lyrics and genre
100121
val track = Track(
101122
id = id.toString(),
102123
title = title,
103-
artwork = artworkUri.toString(),
104124
artist = artist,
125+
artwork = artworkUri.toString(),
105126
album = album,
106-
genre = genre,
127+
genre = null,
107128
duration = duration,
108129
url = "file://$data",
109130
createdAt = dateAdded * 1000, // Convert to milliseconds
110131
modifiedAt = dateAdded * 1000, // Convert to milliseconds
111-
fileSize = fileSize
132+
fileSize = fileSize,
133+
lyrics = null
112134
)
113135

114136
tracks.add(track)
137+
trackPaths.add(id.toString() to data)
115138
endCursor = id.toString()
116139
count++
117140
} catch (e: Exception) {
118-
// Continue processing other tracks if a single track fails to parse
119141
continue
120142
}
121143

122144
if (!cursor.moveToNext()) break
123145
}
124146

147+
// Batch process tag information loading
148+
val futures = trackPaths.map { (id, path) ->
149+
executor.submit<Pair<String, Pair<String?, String?>>> {
150+
val tagInfo = getAudioTagInfo(path)
151+
Pair(id, tagInfo)
152+
}
153+
}
154+
155+
// Wait for all tag information to be loaded
156+
futures.forEach { future ->
157+
try {
158+
val result = future.get(5, TimeUnit.SECONDS)
159+
val id = result.first
160+
val (genre, lyrics) = result.second
161+
val index = tracks.indexOfFirst { it.id == id }
162+
if (index != -1) {
163+
val track = tracks[index]
164+
tracks[index] = track.copy(
165+
genre = genre,
166+
lyrics = lyrics
167+
)
168+
}
169+
} catch (e: Exception) {
170+
// If the loading times out or fails, keep the original value
171+
}
172+
}
173+
125174
// Check if there are more data
126175
hasNextPage = !c.isAfterLast
127176
}
@@ -199,12 +248,6 @@ object GetTracksQuery {
199248
"duration" -> MediaStore.Audio.Media.DURATION
200249
"created_at" -> MediaStore.Audio.Media.DATE_ADDED
201250
"modified_at" -> MediaStore.Audio.Media.DATE_MODIFIED
202-
"genre" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
203-
MediaStore.Audio.Media.GENRE
204-
} else {
205-
null
206-
}
207-
208251
"track_count" -> MediaStore.Audio.Media.TRACK
209252
else -> throw IllegalArgumentException("Unsupported SortKey: ${parts[0]}")
210253
}

android/src/main/java/com/musiclibrary/utils/DataConverter.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ object DataConverter {
1010
val map = Arguments.createMap()
1111
map.putString("id", track.id)
1212
map.putString("title", track.title)
13-
map.putString("artwork", track.artwork)
14-
map.putString("artist", track.artist)
15-
map.putString("album", track.album)
16-
map.putString("genre", track.genre)
13+
track.artist?.let { map.putString("artist", it) }
14+
track.artwork?.let { map.putString("artwork", it) }
15+
track.album?.let { map.putString("album", it) }
16+
track.genre?.let { map.putString("genre", it) }
17+
track.lyrics?.let { map.putString("lyrics", it) }
1718
map.putDouble("duration", track.duration)
1819
map.putString("url", track.url)
1920
map.putLong("fileSize", track.fileSize)

0 commit comments

Comments
 (0)