Skip to content

Commit ccdf4bf

Browse files
committed
refactor: unify audio header and tag fields into TrackMetadata
1 parent ce1cf46 commit ccdf4bf

File tree

11 files changed

+300
-103
lines changed

11 files changed

+300
-103
lines changed

README.md

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -95,23 +95,54 @@ Promise that resolves to `TrackResult` containing:
9595
- `endCursor`: String cursor for pagination
9696
- `totalCount`: Total number of tracks (optional)
9797

98-
#### Example
98+
### getTrackMetadataAsync(trackId)
9999

100-
```js
101-
import { getTracksAsync } from '@nodefinity/react-native-music-library';
100+
Retrieves detailed metadata for a specific track, including lyrics and additional metadata from JAudioTagger.
102101

103-
// Get first 20 tracks (default)
104-
const result = await getTracksAsync();
102+
#### Parameters
105103

106-
// Get tracks with custom options
107-
const customResult = await getTracksAsync({
108-
first: 50,
109-
sortBy: ['title', true], // Sort by title ascending
110-
directory: '/Music/Favorites'
111-
});
104+
- `trackId`: string - The ID of the track to get metadata for
105+
106+
#### Returns
107+
108+
Promise that resolves to `TrackMetadata` containing:
109+
110+
```typescript
111+
interface TrackMetadata {
112+
id: string; // Track ID
113+
114+
// audio header
115+
duration?: number; // Duration in seconds
116+
bitrate?: number; // Bitrate in kbps
117+
sampleRate?: number; // Sample rate in Hz
118+
channels?: number; // Number of channels
119+
format?: string; // Audio format
120+
121+
// tag info
122+
title?: string; // Track title
123+
artist?: string; // Artist name
124+
album?: string; // Album name
125+
year?: number; // Release year
126+
genre?: string; // Music genre
127+
track?: number; // Track number
128+
disc?: number; // Disc number
129+
composer?: string; // Composer
130+
lyricist?: string; // Lyricist
131+
lyrics?: string; // Lyrics content
132+
albumArtist?: string; // Album artist
133+
comment?: string; // Comment
134+
}
135+
```
112136

113-
console.log('Tracks:', customResult.items);
114-
console.log('Has more:', customResult.hasNextPage);
137+
#### Example
138+
139+
```js
140+
import { getTrackMetadataAsync } from '@nodefinity/react-native-music-library';
141+
142+
// Get metadata for a specific track
143+
const metadata = await getTrackMetadataAsync('track-id-123');
144+
console.log('Lyrics:', metadata.lyrics);
145+
console.log('Additional metadata:', metadata.additionalMetadata);
115146
```
116147

117148
## Type Definitions
@@ -125,7 +156,6 @@ interface Track {
125156
artist?: string; // Artist name
126157
artwork?: string; // Artwork file URI
127158
album?: string; // Album name
128-
genre?: string; // Music genre
129159
lyrics?: string // Lyrics
130160
duration: number; // Duration in seconds
131161
url: string; // File URL or path
@@ -158,7 +188,6 @@ type SortByKey =
158188
| 'duration'
159189
| 'createdAt'
160190
| 'modifiedAt'
161-
| 'genre'
162191
| 'trackCount';
163192
```
164193

README_ZH.md

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ iOS 实现尚未完成。现在可以先添加权限到 `Info.plist` 以便将
7474
在使用此库之前,你需要请求访问音乐库的权限。我们推荐使用以下库之一:
7575

7676
- [react-native-permissions](https://github.com/zoontek/react-native-permissions)
77-
- [expo-media-library](https://docs.expo.dev/versions/latest/sdk/media-library/)(用于 Expo 项目)
77+
- [expo-media-library](https://docs.expo.dev/versions/latest/sdk/media-library/)
7878

7979
## API 参考
8080

@@ -95,23 +95,54 @@ iOS 实现尚未完成。现在可以先添加权限到 `Info.plist` 以便将
9595
- `endCursor`:用于分页的字符串游标
9696
- `totalCount`:曲目总数(可选)
9797

98-
#### 示例
98+
### getTrackMetadataAsync(trackId)
9999

100-
```js
101-
import { getTracksAsync } from '@nodefinity/react-native-music-library';
100+
获取特定曲目的详细元数据,包括歌词和来自 JAudioTagger 的额外元数据。
102101

103-
// 获取前 20 首曲目(默认)
104-
const result = await getTracksAsync();
102+
#### 参数
105103

106-
// 使用自定义选项获取曲目
107-
const customResult = await getTracksAsync({
108-
first: 50,
109-
sortBy: ['title', true], // 按标题升序排序
110-
directory: '/Music/Favorites'
111-
});
104+
- `trackId`:string - 要获取元数据的曲目 ID
105+
106+
#### 返回值
107+
108+
返回一个 Promise,解析为包含以下内容的 `TrackMetadata`
109+
110+
```typescript
111+
interface TrackMetadata {
112+
id: string; // 曲目 ID
113+
114+
// audio header
115+
duration?: number; // 持续时间(秒)
116+
bitrate?: number; // 比特率(kbps)
117+
sampleRate?: number; // 采样率(Hz)
118+
channels?: number; // 通道数
119+
format?: string; // 音频格式
120+
121+
// tag info
122+
title?: string; // 曲目标题
123+
artist?: string; // 艺术家名称
124+
album?: string; // 专辑名称
125+
year?: number; // 发行年份
126+
genre?: string; // 音乐流派
127+
track?: number; // 曲目编号
128+
disc?: number; // 碟片编号
129+
composer?: string; // 作曲家
130+
lyricist?: string; // 作词家
131+
lyrics?: string; // 歌词内容
132+
albumArtist?: string; // 专辑艺术家
133+
comment?: string; // 注释
134+
}
135+
```
112136

113-
console.log('曲目:', customResult.items);
114-
console.log('是否还有更多:', customResult.hasNextPage);
137+
#### 示例
138+
139+
```js
140+
import { getTrackMetadataAsync } from '@nodefinity/react-native-music-library';
141+
142+
// 获取特定曲目的元数据
143+
const metadata = await getTrackMetadataAsync('track-id-123');
144+
console.log('歌词:', metadata.lyrics);
145+
console.log('额外元数据:', metadata.additionalMetadata);
115146
```
116147

117148
## 类型定义
@@ -125,8 +156,6 @@ interface Track {
125156
artist?: string; // 艺术家名称
126157
artwork?: string; // 专辑封面 URI
127158
album?: string; // 专辑名称
128-
genre?: string; // 音乐流派
129-
lyrics?: string; // 歌词
130159
duration: number; // 持续时间(秒)
131160
url: string; // 文件 URL 或路径
132161
createdAt?: number; // 添加日期(Unix 时间戳)
@@ -158,7 +187,6 @@ type SortByKey =
158187
| 'duration'
159188
| 'createdAt'
160189
| 'modifiedAt'
161-
| 'genre'
162190
| 'trackCount';
163191
```
164192

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.musiclibrary
33
import com.facebook.react.bridge.ReactApplicationContext
44
import com.facebook.react.bridge.Promise
55
import com.facebook.react.bridge.ReadableMap
6+
import com.facebook.react.bridge.Arguments
67
import com.facebook.react.module.annotations.ReactModule
78
import com.musiclibrary.utils.ReadableMapMapper.toAssetsOptions
89
import com.musiclibrary.utils.ModuleUtils.throwUnlessPermissionsGranted
@@ -11,6 +12,7 @@ import com.musiclibrary.tracks.GetTracks
1112
import com.musiclibrary.albums.GetAlbums
1213
import com.musiclibrary.artists.GetArtists
1314
import com.musiclibrary.genres.GetGenres
15+
import com.musiclibrary.tracks.GetTrackMetadataQuery
1416

1517
@ReactModule(name = MusicLibraryModule.NAME)
1618
class MusicLibraryModule(reactContext: ReactApplicationContext) :
@@ -56,6 +58,15 @@ class MusicLibraryModule(reactContext: ReactApplicationContext) :
5658
}
5759
}
5860

61+
override fun getTrackMetadataAsync(trackId: String, promise: Promise) {
62+
throwUnlessPermissionsGranted(reactApplicationContext, isWrite = false) {
63+
withModuleScope(promise) {
64+
GetTrackMetadataQuery(reactApplicationContext, trackId, promise)
65+
.execute()
66+
}
67+
}
68+
}
69+
5970
companion object {
6071
const val NAME = "MusicLibrary"
6172
}

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,38 @@ data class Track(
66
val artist: String? = null,
77
val artwork: String? = null,
88
val album: String? = null,
9-
val genre: String? = null,
10-
val lyrics: String? = null,
119
val duration: Double,
1210
val url: String,
1311
val createdAt: Long? = null,
1412
val modifiedAt: Long? = null,
1513
val fileSize: Long,
1614
)
1715

16+
data class TrackMetadata(
17+
val id: String,
18+
19+
// AudioHeader
20+
val duration: Double? = null,
21+
val bitrate: Long? = null,
22+
val sampleRate: Int? = null,
23+
val channels: String? = null,
24+
val format: String? = null,
25+
26+
// Tag
27+
val title: String? = null,
28+
val artist: String? = null,
29+
val album: String? = null,
30+
val year: Int? = null,
31+
val genre: String? = null,
32+
val track: Int? = null,
33+
val disc: Int? = null,
34+
val composer: String? = null,
35+
val lyricist: String? = null,
36+
val lyrics: String? = null,
37+
val albumArtist: String? = null,
38+
val comment: String? = null
39+
)
40+
1841
data class Album(
1942
val id: String,
2043
val title: String,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.musiclibrary.tracks
2+
3+
import android.content.Context
4+
import android.content.ContentResolver
5+
import android.provider.MediaStore
6+
import com.facebook.react.bridge.Promise
7+
import com.musiclibrary.models.TrackMetadata
8+
import com.musiclibrary.utils.DataConverter
9+
import java.io.File
10+
import org.jaudiotagger.audio.AudioFileIO
11+
import org.jaudiotagger.tag.FieldKey
12+
13+
internal class GetTrackMetadataQuery(
14+
private val context: Context,
15+
private val trackId: String,
16+
private val promise: Promise
17+
) {
18+
fun execute() {
19+
try {
20+
val contentResolver = context.contentResolver
21+
val result = getTrackMetadata(contentResolver, trackId)
22+
val writableMap = DataConverter.trackMetadataToWritableMap(result)
23+
promise.resolve(writableMap)
24+
} catch (e: Exception) {
25+
promise.reject("QUERY_ERROR", "Failed to get track metadata: ${e.message}", e)
26+
}
27+
}
28+
29+
private fun getTrackMetadata(
30+
contentResolver: ContentResolver,
31+
trackId: String
32+
): TrackMetadata {
33+
// First try to get the file path from MediaStore
34+
val projection = arrayOf(MediaStore.Audio.Media.DATA)
35+
val selection = "${MediaStore.Audio.Media._ID} = ?"
36+
val selectionArgs = arrayOf(trackId)
37+
38+
val cursor = contentResolver.query(
39+
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
40+
projection,
41+
selection,
42+
selectionArgs,
43+
null
44+
)
45+
46+
cursor?.use { c ->
47+
if (c.moveToFirst()) {
48+
val dataColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)
49+
val filePath = c.getString(dataColumn)
50+
51+
if (!filePath.isNullOrEmpty()) {
52+
try {
53+
val audioFile = AudioFileIO.read(File(filePath))
54+
val header = audioFile.audioHeader
55+
val tag = audioFile.tag
56+
57+
return TrackMetadata(
58+
id = trackId,
59+
// AudioHeader
60+
duration = header.trackLength.toDouble(),
61+
bitrate = header.bitRateAsNumber,
62+
sampleRate = header.sampleRateAsNumber,
63+
channels = header.channels,
64+
format = header.format,
65+
// Tag
66+
title = tag?.getFirst(FieldKey.TITLE),
67+
artist = tag?.getFirst(FieldKey.ARTIST),
68+
album = tag?.getFirst(FieldKey.ALBUM),
69+
year = tag?.getFirst(FieldKey.YEAR)?.toIntOrNull(),
70+
genre = tag?.getFirst(FieldKey.GENRE),
71+
track = tag?.getFirst(FieldKey.TRACK)?.toIntOrNull(),
72+
disc = tag?.getFirst(FieldKey.DISC_NO)?.toIntOrNull(),
73+
composer = tag?.getFirst(FieldKey.COMPOSER),
74+
lyricist = tag?.getFirst(FieldKey.LYRICIST),
75+
lyrics = tag?.getFirst(FieldKey.LYRICS),
76+
albumArtist = tag?.getFirst(FieldKey.ALBUM_ARTIST),
77+
comment = tag?.getFirst(FieldKey.COMMENT)
78+
)
79+
} catch (e: Exception) {
80+
// If reading the file fails, return empty metadata
81+
}
82+
}
83+
}
84+
}
85+
86+
return TrackMetadata(id = trackId)
87+
}
88+
}

0 commit comments

Comments
 (0)