1
1
package com.musiclibrary.tracks
2
2
3
3
import android.content.ContentResolver
4
- import android.content.Context
5
- import android.media.MediaMetadataRetriever
4
+ import android.content.ContentUris
6
5
import android.os.Build
7
6
import android.provider.MediaStore
8
- import android.util.Base64
7
+ import android.net.Uri
9
8
import com.musiclibrary.models.*
10
- import java.io.File
11
- import java.util.concurrent.Executors
12
- import java.util.concurrent.Future
13
- import java.util.concurrent.ThreadPoolExecutor
14
- import java.util.concurrent.TimeUnit
15
9
16
- /* *
17
- * GetTracksQuery
18
- *
19
- * 1. Split the query and metadata extraction of audio files
20
- * 2. Use multi-threaded parallel processing of metadata extraction to significantly improve performance
21
- * 3. Reasonably control the size of the thread pool to avoid resource waste
22
- * 4. Add timeout mechanism to avoid a single file blocking the entire process
23
- */
24
10
object GetTracksQuery {
25
11
26
12
fun getTracks (
27
13
contentResolver : ContentResolver ,
28
14
options : AssetsOptions ,
29
- context : Context
30
15
): PaginatedResult <Track > {
31
16
val projection = arrayOf(
32
17
MediaStore .Audio .Media ._ID ,
@@ -37,7 +22,8 @@ object GetTracksQuery {
37
22
MediaStore .Audio .Media .DATA ,
38
23
MediaStore .Audio .Media .DATE_ADDED ,
39
24
MediaStore .Audio .Media .SIZE ,
40
- MediaStore .Audio .Media .ALBUM_ID
25
+ MediaStore .Audio .Media .ALBUM_ID ,
26
+ MediaStore .Audio .Media .GENRE
41
27
)
42
28
43
29
val selection = buildSelection(options)
@@ -52,7 +38,7 @@ object GetTracksQuery {
52
38
sortOrder
53
39
) ? : throw RuntimeException (" Failed to query MediaStore: cursor is null" )
54
40
55
- val basicTracks = mutableListOf<Track >()
41
+ val tracks = mutableListOf<Track >()
56
42
var hasNextPage = false
57
43
var endCursor: String? = null
58
44
val totalCount = cursor.count
@@ -66,6 +52,8 @@ object GetTracksQuery {
66
52
val dataColumn = c.getColumnIndexOrThrow(MediaStore .Audio .Media .DATA )
67
53
val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore .Audio .Media .DATE_ADDED )
68
54
val sizeColumn = c.getColumnIndexOrThrow(MediaStore .Audio .Media .SIZE )
55
+ val genreColumn = c.getColumnIndexOrThrow(MediaStore .Audio .Media .GENRE )
56
+ val albumIdColumn = c.getColumnIndexOrThrow(MediaStore .Audio .Media .ALBUM_ID )
69
57
70
58
// Jump to the specified start position
71
59
if (options.after != null ) {
@@ -95,27 +83,30 @@ object GetTracksQuery {
95
83
val data = c.getString(dataColumn) ? : " "
96
84
val dateAdded = c.getLong(dateAddedColumn)
97
85
val fileSize = c.getLong(sizeColumn)
86
+ val genre = c.getString(genreColumn) ? : " "
87
+ val albumId = c.getLong(albumIdColumn)
88
+ val artworkUri: Uri = Uri .parse(" content://media/external/audio/media/${id} /albumart" )
98
89
99
90
// Skip invalid data
100
91
if (data.isEmpty()) {
101
92
continue
102
93
}
103
94
104
- val basicTrack = Track (
95
+ val track = Track (
105
96
id = id.toString(),
106
97
title = title,
107
- cover = " " ,
98
+ cover = artworkUri.toString() ,
108
99
artist = artist,
109
100
album = album,
110
- genre = " " ,
101
+ genre = genre ,
111
102
duration = duration,
112
103
uri = " file://$data " ,
113
104
createdAt = dateAdded * 1000 , // Convert to milliseconds
114
105
modifiedAt = dateAdded * 1000 , // Convert to milliseconds
115
106
fileSize = fileSize
116
107
)
117
108
118
- basicTracks .add(basicTrack )
109
+ tracks .add(track )
119
110
endCursor = id.toString()
120
111
count++
121
112
} catch (e: Exception ) {
@@ -128,9 +119,6 @@ object GetTracksQuery {
128
119
hasNextPage = c.moveToNext()
129
120
}
130
121
131
- // Use multi-threaded parallel processing of metadata extraction
132
- val tracks = processTracksMetadata(basicTracks, context)
133
-
134
122
return PaginatedResult (
135
123
items = tracks,
136
124
hasNextPage = hasNextPage,
@@ -156,142 +144,6 @@ object GetTracksQuery {
156
144
return conditions.joinToString(" AND " )
157
145
}
158
146
159
- /* *
160
- * Use multi-threaded parallel processing of metadata extraction
161
- *
162
- * @param basicTracks Track list
163
- * @return Track list with complete metadata
164
- */
165
- private fun processTracksMetadata (basicTracks : List <Track >, context : Context ): List <Track > {
166
- if (basicTracks.isEmpty()) {
167
- return emptyList()
168
- }
169
-
170
- // Create thread pool, optimized for I/O intensive tasks
171
- val threadCount = minOf(16 , maxOf(4 , Runtime .getRuntime().availableProcessors() * 4 ))
172
- val executor = Executors .newFixedThreadPool(threadCount) as ThreadPoolExecutor
173
-
174
- // Pre-warm thread pool
175
- executor.prestartAllCoreThreads()
176
-
177
- // Create a MediaMetadataRetriever instance for each thread
178
- val retrievers = Array (threadCount) { MediaMetadataRetriever () }
179
- val threadLocalRetriever = ThreadLocal <MediaMetadataRetriever >()
180
-
181
- try {
182
- // Create Future task list
183
- val futures = mutableListOf<Future <Track >>()
184
-
185
- // Create asynchronous task for each track
186
- for ((index, basicTrack) in basicTracks.withIndex()) {
187
- val future = executor.submit<Track > {
188
- // Get the retriever for the current thread
189
- var retriever = threadLocalRetriever.get()
190
- if (retriever == null ) {
191
- retriever = retrievers[index % threadCount]
192
- threadLocalRetriever.set(retriever)
193
- }
194
-
195
- try {
196
- val data = basicTrack.uri.replace(" file://" , " " )
197
- retriever.setDataSource(data)
198
-
199
- val genre = retriever.extractMetadata(MediaMetadataRetriever .METADATA_KEY_GENRE ) ? : " "
200
-
201
- val embeddedPicture = retriever.embeddedPicture
202
- val cover = if (embeddedPicture != null ) {
203
- // Use the application's private directory to store the image
204
- val coverDir = File (context.filesDir, " covers" )
205
- if (! coverDir.exists()) {
206
- coverDir.mkdirs()
207
- }
208
-
209
- // Use the hash value of the file path as the file name to avoid duplication
210
- val fileName = " cover_${data.hashCode()} .jpg"
211
- val coverFile = File (coverDir, fileName)
212
-
213
- // If the file does not exist, save it
214
- if (! coverFile.exists()) {
215
- coverFile.writeBytes(embeddedPicture)
216
- }
217
-
218
- // Return the complete file URI
219
- " file://${coverFile.absolutePath} "
220
- } else {
221
- " "
222
- }
223
-
224
- Track (
225
- id = basicTrack.id,
226
- title = basicTrack.title,
227
- cover = cover,
228
- artist = basicTrack.artist,
229
- album = basicTrack.album,
230
- genre = genre,
231
- duration = basicTrack.duration,
232
- uri = basicTrack.uri,
233
- createdAt = basicTrack.createdAt,
234
- modifiedAt = basicTrack.modifiedAt,
235
- fileSize = basicTrack.fileSize
236
- )
237
- } catch (e: Exception ) {
238
- // If metadata extraction fails, return track without metadata
239
- Track (
240
- id = basicTrack.id,
241
- title = basicTrack.title,
242
- cover = " " ,
243
- artist = basicTrack.artist,
244
- album = basicTrack.album,
245
- genre = " " ,
246
- duration = basicTrack.duration,
247
- uri = basicTrack.uri,
248
- createdAt = basicTrack.createdAt,
249
- modifiedAt = basicTrack.modifiedAt,
250
- fileSize = basicTrack.fileSize
251
- )
252
- }
253
- }
254
- futures.add(future)
255
- }
256
-
257
- // Collect all results
258
- val tracks = mutableListOf<Track >()
259
- for (future in futures) {
260
- try {
261
- // Set shorter timeout (maximum 2 seconds per file)
262
- val track = future.get(2 , TimeUnit .SECONDS )
263
- tracks.add(track)
264
- } catch (e: Exception ) {
265
- // If the task times out or fails, skip this track
266
- continue
267
- }
268
- }
269
-
270
- return tracks
271
- } finally {
272
- // Ensure the thread pool is closed correctly
273
- executor.shutdown()
274
- try {
275
- if (! executor.awaitTermination(30 , TimeUnit .SECONDS )) {
276
- executor.shutdownNow()
277
- }
278
- } catch (e: InterruptedException ) {
279
- executor.shutdownNow()
280
- }
281
-
282
- // Release all MediaMetadataRetriever instances
283
- retrievers.forEach { retriever ->
284
- try {
285
- retriever.release()
286
- } catch (e: Exception ) {
287
- }
288
- }
289
-
290
- // Clean up ThreadLocal
291
- threadLocalRetriever.remove()
292
- }
293
- }
294
-
295
147
private fun buildSelectionArgs (options : AssetsOptions ): Array <String >? {
296
148
val args = mutableListOf<String >()
297
149
0 commit comments