Skip to content

Commit 3ff8e6c

Browse files
committed
feat: read cover by albumart
1 parent ca524d1 commit 3ff8e6c

File tree

2 files changed

+15
-163
lines changed

2 files changed

+15
-163
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal class GetTracks(
1515
try {
1616
val contentResolver = context.contentResolver
1717

18-
val result = GetTracksQuery.getTracks(contentResolver, options, context)
18+
val result = GetTracksQuery.getTracks(contentResolver, options)
1919
val writableMap = DataConverter.paginatedResultToWritableMap(result) { track ->
2020
DataConverter.trackToWritableMap(track)
2121
}

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

Lines changed: 14 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,17 @@
11
package com.musiclibrary.tracks
22

33
import android.content.ContentResolver
4-
import android.content.Context
5-
import android.media.MediaMetadataRetriever
4+
import android.content.ContentUris
65
import android.os.Build
76
import android.provider.MediaStore
8-
import android.util.Base64
7+
import android.net.Uri
98
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
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-
*/
2410
object 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

Comments
 (0)