Skip to content

Commit 070346a

Browse files
committed
feat(metadata): Add album artist, track number, and multi-artist support
- Embed album artist (aART) and track number (trkn) in M4A files - Join all song artists for the Artist field (e.g., "Lady Gaga, Bruno Mars") - Fetch missing metadata (year, album artist, track position) from YouTube - Update native JNI and Kotlin wrapper signatures
1 parent 20a00e3 commit 070346a

File tree

4 files changed

+71
-14
lines changed

4 files changed

+71
-14
lines changed

app/src/main/cpp/coverart

app/src/main/kotlin/com/metrolist/music/utils/CoverArtEmbedder.kt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class CoverArtEmbedder @Inject constructor(
3939
* @param artist Artist name, can be null
4040
* @param album Album name, can be null
4141
* @param year Year string, can be null
42+
* @param albumArtist Album artist name, can be null
43+
* @param trackNumber Track number (0 to skip)
44+
* @param totalTracks Total tracks in album (0 if unknown)
4245
* @return true if successful, false otherwise
4346
*/
4447
suspend fun embedMetadataIntoFile(
@@ -47,11 +50,15 @@ class CoverArtEmbedder @Inject constructor(
4750
title: String?,
4851
artist: String?,
4952
album: String?,
50-
year: String?
53+
year: String?,
54+
albumArtist: String? = null,
55+
trackNumber: Int = 0,
56+
totalTracks: Int = 0
5157
): Boolean = withContext(Dispatchers.IO) {
5258
Timber.tag(TAG).d("=== Starting metadata embedding ===")
5359
Timber.tag(TAG).d("File URI: $fileUri")
5460
Timber.tag(TAG).d("Title: $title, Artist: $artist, Album: $album, Year: $year")
61+
Timber.tag(TAG).d("Album Artist: $albumArtist, Track: $trackNumber/$totalTracks")
5562
Timber.tag(TAG).d("Artwork size: ${artworkData?.size ?: 0} bytes")
5663

5764
val tempDir = File(context.cacheDir, "coverart_temp")
@@ -90,7 +97,10 @@ class CoverArtEmbedder @Inject constructor(
9097
title = title,
9198
artist = artist,
9299
album = album,
93-
year = year
100+
year = year,
101+
albumArtist = albumArtist,
102+
trackNumber = trackNumber,
103+
totalTracks = totalTracks
94104
)
95105

96106
if (!success) {
@@ -140,6 +150,9 @@ class CoverArtEmbedder @Inject constructor(
140150
* @param artist Artist name, can be null
141151
* @param album Album name, can be null
142152
* @param year Year string, can be null
153+
* @param albumArtist Album artist name, can be null
154+
* @param trackNumber Track number (0 to skip)
155+
* @param totalTracks Total tracks in album (0 if unknown)
143156
* @return true if successful, false otherwise
144157
*/
145158
suspend fun embedMetadataIntoLocalFile(
@@ -148,7 +161,10 @@ class CoverArtEmbedder @Inject constructor(
148161
title: String?,
149162
artist: String?,
150163
album: String?,
151-
year: String?
164+
year: String?,
165+
albumArtist: String? = null,
166+
trackNumber: Int = 0,
167+
totalTracks: Int = 0
152168
): Boolean = withContext(Dispatchers.IO) {
153169
Timber.tag(TAG).d("=== Starting local file metadata embedding ===")
154170
Timber.tag(TAG).d("File path: $filePath")
@@ -170,7 +186,10 @@ class CoverArtEmbedder @Inject constructor(
170186
title = title,
171187
artist = artist,
172188
album = album,
173-
year = year
189+
year = year,
190+
albumArtist = albumArtist,
191+
trackNumber = trackNumber,
192+
totalTracks = totalTracks
174193
)
175194

176195
if (!success) {

app/src/main/kotlin/com/metrolist/music/utils/CoverArtNative.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ object CoverArtNative {
1616
}
1717

1818
/**
19-
* Embed metadata (cover art, title, artist, album, year) into an M4A/MP4 file.
19+
* Embed metadata (cover art, title, artist, album, year, album artist, track number) into an M4A/MP4 file.
2020
* All text is stored as UTF-8 (supports Hebrew, Arabic, and all Unicode).
2121
*
2222
* @param inputPath Path to the input M4A/MP4 file
@@ -26,6 +26,9 @@ object CoverArtNative {
2626
* @param artist Artist name (can be null)
2727
* @param album Album name (can be null)
2828
* @param year Year string (can be null)
29+
* @param albumArtist Album artist name (can be null)
30+
* @param trackNumber Track number (0 or negative to skip)
31+
* @param totalTracks Total tracks in album (0 if unknown)
2932
* @return true if successful, false otherwise
3033
*/
3134
external fun embedMetadata(
@@ -35,7 +38,10 @@ object CoverArtNative {
3538
title: String?,
3639
artist: String?,
3740
album: String?,
38-
year: String?
41+
year: String?,
42+
albumArtist: String?,
43+
trackNumber: Int,
44+
totalTracks: Int
3945
): Boolean
4046

4147
/**

app/src/main/kotlin/com/metrolist/music/utils/DownloadExportHelper.kt

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ class DownloadExportHelper @Inject constructor(
6464
Timber.tag(TAG).d("Format: ${format?.mimeType ?: "unknown"}, Extension: $extension")
6565

6666
// Build folder structure: downloadFolder/Artist/Title.ext
67-
val artistName = song.artists.firstOrNull()?.name ?: "Unknown Artist"
67+
val firstArtist = song.artists.firstOrNull()?.name ?: "Unknown Artist"
68+
val allArtists = song.artists.joinToString(", ") { it.name }
69+
.ifEmpty { "Unknown Artist" }
6870
val title = song.song.title
69-
val sanitizedArtistFolder = sanitizeFilename(artistName)
71+
val sanitizedArtistFolder = sanitizeFilename(firstArtist)
7072
val sanitizedFilename = sanitizeFilename("$title.$extension")
7173
Timber.tag(TAG).d("Artist folder: $sanitizedArtistFolder, Filename: $sanitizedFilename")
7274

@@ -155,17 +157,47 @@ class DownloadExportHelper @Inject constructor(
155157
Timber.tag(TAG).d("M4A file detected (${bitrateKbps}kbps), embedding metadata...")
156158
try {
157159
val artworkData = fetchArtworkData(song.song.thumbnailUrl)
158-
val albumName = song.album?.title
159-
// Extract year from album if available
160-
val year = song.album?.year?.toString()
160+
161+
// Try album relationship first, fall back to song entity fields
162+
var albumName = song.album?.title ?: song.song.albumName
163+
var year = (song.album?.year ?: song.song.year)?.toString()
164+
var albumArtist: String? = null
165+
var trackNumber = 0
166+
var totalTracks = 0
167+
168+
// If missing album or year info and we have albumId, try fetching from YouTube
169+
if ((albumName == null || year == null) && song.song.albumId != null) {
170+
Timber.tag(TAG).d("Album/year info incomplete (album=$albumName, year=$year), fetching from YouTube...")
171+
try {
172+
val albumPage = com.metrolist.innertube.YouTube.album(song.song.albumId!!).getOrNull()
173+
if (albumPage != null) {
174+
if (albumName == null) albumName = albumPage.album.title
175+
if (year == null) year = albumPage.album.year?.toString()
176+
// Get album artist (first artist of album)
177+
albumArtist = albumPage.album.artists?.firstOrNull()?.name
178+
// Find track position in album
179+
totalTracks = albumPage.songs.size
180+
val trackIndex = albumPage.songs.indexOfFirst { it.id == songId }
181+
if (trackIndex >= 0) {
182+
trackNumber = trackIndex + 1
183+
}
184+
Timber.tag(TAG).d("Fetched from YouTube - album: $albumName, year: $year, albumArtist: $albumArtist, track: $trackNumber/$totalTracks")
185+
}
186+
} catch (e: Exception) {
187+
Timber.tag(TAG).w(e, "Failed to fetch album info from YouTube")
188+
}
189+
}
161190

162191
val embedSuccess = coverArtEmbedder.embedMetadataIntoFile(
163192
fileUri = newFile.uri,
164193
artworkData = artworkData,
165194
title = song.song.title,
166-
artist = artistName,
195+
artist = allArtists,
167196
album = albumName,
168-
year = year
197+
year = year,
198+
albumArtist = albumArtist,
199+
trackNumber = trackNumber,
200+
totalTracks = totalTracks
169201
)
170202

171203
if (embedSuccess) {

0 commit comments

Comments
 (0)