Skip to content

Commit bff16e2

Browse files
committed
feat: use multi-threaded parallel processing of metadata extraction
1 parent c6741bd commit bff16e2

File tree

1 file changed

+111
-11
lines changed

1 file changed

+111
-11
lines changed

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

Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,19 @@ import android.os.Build
66
import android.provider.MediaStore
77
import android.util.Base64
88
import com.musiclibrary.models.*
9-
9+
import java.util.concurrent.Executors
10+
import java.util.concurrent.Future
11+
import java.util.concurrent.ThreadPoolExecutor
12+
import java.util.concurrent.TimeUnit
13+
14+
/**
15+
* GetTracksQuery
16+
*
17+
* 1. Split the query and metadata extraction of audio files
18+
* 2. Use multi-threaded parallel processing of metadata extraction to significantly improve performance
19+
* 3. Reasonably control the size of the thread pool to avoid resource waste
20+
* 4. Add timeout mechanism to avoid a single file blocking the entire process
21+
*/
1022
object GetTracksQuery {
1123

1224
fun getTracks(
@@ -37,7 +49,7 @@ object GetTracksQuery {
3749
sortOrder
3850
) ?: throw RuntimeException("Failed to query MediaStore: cursor is null")
3951

40-
val tracks = mutableListOf<Track>()
52+
val basicTracks = mutableListOf<Track>()
4153
var hasNextPage = false
4254
var endCursor: String? = null
4355
val totalCount = cursor.count
@@ -86,23 +98,21 @@ object GetTracksQuery {
8698
continue
8799
}
88100

89-
val meta = getTrackMeta(data)
90-
91-
val track = Track(
101+
val basicTrack = Track(
92102
id = id.toString(),
93103
title = title,
94-
cover = meta.cover,
104+
cover = "",
95105
artist = artist,
96106
album = album,
97-
genre = meta.genre,
107+
genre = "",
98108
duration = duration,
99109
uri = "file://$data",
100110
createdAt = dateAdded * 1000, // Convert to milliseconds
101111
modifiedAt = dateAdded * 1000, // Convert to milliseconds
102112
fileSize = fileSize
103113
)
104114

105-
tracks.add(track)
115+
basicTracks.add(basicTrack)
106116
endCursor = id.toString()
107117
count++
108118
} catch (e: Exception) {
@@ -115,6 +125,9 @@ object GetTracksQuery {
115125
hasNextPage = c.moveToNext()
116126
}
117127

128+
// Use multi-threaded parallel processing of metadata extraction
129+
val tracks = processTracksMetadata(basicTracks)
130+
118131
return PaginatedResult(
119132
items = tracks,
120133
hasNextPage = hasNextPage,
@@ -140,6 +153,93 @@ object GetTracksQuery {
140153
return conditions.joinToString(" AND ")
141154
}
142155

156+
/**
157+
* Use multi-threaded parallel processing of metadata extraction
158+
*
159+
* @param basicTracks Track list
160+
* @return Track list with complete metadata
161+
*/
162+
private fun processTracksMetadata(basicTracks: List<Track>): List<Track> {
163+
if (basicTracks.isEmpty()) {
164+
return emptyList()
165+
}
166+
167+
// Create thread pool, optimized for I/O intensive tasks
168+
val threadCount = minOf(16, maxOf(4, Runtime.getRuntime().availableProcessors() * 4))
169+
val executor = Executors.newFixedThreadPool(threadCount) as ThreadPoolExecutor
170+
171+
// Pre-warm thread pool
172+
executor.prestartAllCoreThreads()
173+
174+
try {
175+
// Create Future task list
176+
val futures = mutableListOf<Future<Track>>()
177+
178+
// Create asynchronous task for each track
179+
for (basicTrack in basicTracks) {
180+
val future = executor.submit<Track> {
181+
try {
182+
val meta = getTrackMeta(basicTrack.uri.replace("file://", ""))
183+
Track(
184+
id = basicTrack.id,
185+
title = basicTrack.title,
186+
cover = meta.cover,
187+
artist = basicTrack.artist,
188+
album = basicTrack.album,
189+
genre = meta.genre,
190+
duration = basicTrack.duration,
191+
uri = basicTrack.uri,
192+
createdAt = basicTrack.createdAt,
193+
modifiedAt = basicTrack.modifiedAt,
194+
fileSize = basicTrack.fileSize
195+
)
196+
} catch (e: Exception) {
197+
// If metadata extraction fails, return track without metadata
198+
Track(
199+
id = basicTrack.id,
200+
title = basicTrack.title,
201+
cover = "",
202+
artist = basicTrack.artist,
203+
album = basicTrack.album,
204+
genre = "",
205+
duration = basicTrack.duration,
206+
uri = basicTrack.uri,
207+
createdAt = basicTrack.createdAt,
208+
modifiedAt = basicTrack.modifiedAt,
209+
fileSize = basicTrack.fileSize
210+
)
211+
}
212+
}
213+
futures.add(future)
214+
}
215+
216+
// Collect all results
217+
val tracks = mutableListOf<Track>()
218+
for (future in futures) {
219+
try {
220+
// Set shorter timeout (maximum 2 seconds per file)
221+
val track = future.get(2, TimeUnit.SECONDS)
222+
tracks.add(track)
223+
} catch (e: Exception) {
224+
// If the task times out or fails, skip this track
225+
continue
226+
}
227+
}
228+
229+
return tracks
230+
} finally {
231+
// Ensure the thread pool is closed correctly
232+
executor.shutdown()
233+
try {
234+
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
235+
executor.shutdownNow()
236+
}
237+
} catch (e: InterruptedException) {
238+
executor.shutdownNow()
239+
}
240+
}
241+
}
242+
143243
private fun buildSelectionArgs(options: AssetsOptions): Array<String>? {
144244
val args = mutableListOf<String>()
145245

@@ -189,7 +289,7 @@ object GetTracksQuery {
189289

190290
private fun getTrackMeta(data: String): TrackMeta {
191291
val retriever = MediaMetadataRetriever()
192-
try {
292+
return try {
193293
retriever.setDataSource(data)
194294

195295
val genre = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE) ?: ""
@@ -201,9 +301,9 @@ object GetTracksQuery {
201301
""
202302
}
203303

204-
return TrackMeta(cover, genre)
304+
TrackMeta(cover, genre)
205305
} catch (e: Exception) {
206-
return TrackMeta("", "")
306+
TrackMeta("", "")
207307
} finally {
208308
retriever.release()
209309
}

0 commit comments

Comments
 (0)