11package com.musiclibrary.tracks
22
33import android.content.ContentResolver
4- import android.content.Context
5- import android.media.MediaMetadataRetriever
4+ import android.content.ContentUris
65import android.os.Build
76import android.provider.MediaStore
8- import android.util.Base64
7+ import android.net.Uri
98import 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
159
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- */
2410object GetTracksQuery {
2511
2612 fun getTracks (
2713 contentResolver : ContentResolver ,
2814 options : AssetsOptions ,
29- context : Context
3015 ): PaginatedResult <Track > {
3116 val projection = arrayOf(
3217 MediaStore .Audio .Media ._ID ,
@@ -37,7 +22,8 @@ object GetTracksQuery {
3722 MediaStore .Audio .Media .DATA ,
3823 MediaStore .Audio .Media .DATE_ADDED ,
3924 MediaStore .Audio .Media .SIZE ,
40- MediaStore .Audio .Media .ALBUM_ID
25+ MediaStore .Audio .Media .ALBUM_ID ,
26+ MediaStore .Audio .Media .GENRE
4127 )
4228
4329 val selection = buildSelection(options)
@@ -52,7 +38,7 @@ object GetTracksQuery {
5238 sortOrder
5339 ) ? : throw RuntimeException (" Failed to query MediaStore: cursor is null" )
5440
55- val basicTracks = mutableListOf<Track >()
41+ val tracks = mutableListOf<Track >()
5642 var hasNextPage = false
5743 var endCursor: String? = null
5844 val totalCount = cursor.count
@@ -66,6 +52,8 @@ object GetTracksQuery {
6652 val dataColumn = c.getColumnIndexOrThrow(MediaStore .Audio .Media .DATA )
6753 val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore .Audio .Media .DATE_ADDED )
6854 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 )
6957
7058 // Jump to the specified start position
7159 if (options.after != null ) {
@@ -95,27 +83,30 @@ object GetTracksQuery {
9583 val data = c.getString(dataColumn) ? : " "
9684 val dateAdded = c.getLong(dateAddedColumn)
9785 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" )
9889
9990 // Skip invalid data
10091 if (data.isEmpty()) {
10192 continue
10293 }
10394
104- val basicTrack = Track (
95+ val track = Track (
10596 id = id.toString(),
10697 title = title,
107- cover = " " ,
98+ cover = artworkUri.toString() ,
10899 artist = artist,
109100 album = album,
110- genre = " " ,
101+ genre = genre ,
111102 duration = duration,
112103 uri = " file://$data " ,
113104 createdAt = dateAdded * 1000 , // Convert to milliseconds
114105 modifiedAt = dateAdded * 1000 , // Convert to milliseconds
115106 fileSize = fileSize
116107 )
117108
118- basicTracks .add(basicTrack )
109+ tracks .add(track )
119110 endCursor = id.toString()
120111 count++
121112 } catch (e: Exception ) {
@@ -128,9 +119,6 @@ object GetTracksQuery {
128119 hasNextPage = c.moveToNext()
129120 }
130121
131- // Use multi-threaded parallel processing of metadata extraction
132- val tracks = processTracksMetadata(basicTracks, context)
133-
134122 return PaginatedResult (
135123 items = tracks,
136124 hasNextPage = hasNextPage,
@@ -156,142 +144,6 @@ object GetTracksQuery {
156144 return conditions.joinToString(" AND " )
157145 }
158146
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-
295147 private fun buildSelectionArgs (options : AssetsOptions ): Array <String >? {
296148 val args = mutableListOf<String >()
297149
0 commit comments