Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bc8417a
feat(downloads): add format picker and custom download path support
alltechdev Feb 15, 2026
505cb22
fix(downloads): resolve format mismatch errors and improve playback
alltechdev Feb 15, 2026
8fa747b
fix(build): disable LTO for coverart to fix CI linker issues
alltechdev Feb 15, 2026
60a343e
fix(strings): move download strings to metrolist_strings.xml
alltechdev Feb 16, 2026
07098ff
refactor: Extract coverart library to separate repository
mostafaalagamy Feb 16, 2026
548faea
feat(metadata): Add album artist, track number, and multi-artist support
alltechdev Feb 16, 2026
d6fb074
feat(ui): Redesign download format dialog with Material 3 expressive …
alltechdev Feb 16, 2026
47cbaa7
feat(download): Add format picker to YouTube song menu and fix releas…
alltechdev Feb 16, 2026
684c12a
fix(release): Add ProGuard keep rules for metadata embedding classes
alltechdev Feb 16, 2026
f0f5f30
feat(download): Add format picker and swap download to album menu
alltechdev Feb 16, 2026
6fa4267
fix(playback): Add fallback for legacy downloads without downloadUri
alltechdev Feb 16, 2026
06025b5
fix(export): Use .ogg extension for WebM audio exports
alltechdev Feb 16, 2026
0366551
fix(settings): Show full download path instead of just folder name
alltechdev Feb 16, 2026
2fdd105
fix(rebase): fix PoToken API and schema after rebase
alltechdev Feb 25, 2026
6cf48fe
fix(download): improve metadata embedding and format selection
alltechdev Feb 25, 2026
a337804
refactor: use prebuilt coverart library from separate repo
alltechdev Mar 2, 2026
91e9c1a
feat(download): organize downloads by Artist/Album/Title structure
alltechdev Mar 2, 2026
3df82a4
feat(download): cleanup empty Album/Artist folders after file deletion
alltechdev Mar 2, 2026
e947523
fix: pass customPathUri to enable empty folder cleanup on deletion
alltechdev Mar 2, 2026
1f38ded
fix: update PoTokenGenerator API usage in getAllAvailableAudioFormats
alltechdev Mar 2, 2026
4044c1e
fix: remove unnecessary null assertions and dead code
alltechdev Mar 2, 2026
9d6107e
fix: restore schemas 33 and 34 to match main
alltechdev Mar 2, 2026
9f4cf39
fix: update schema 36 with downloadUri field
alltechdev Mar 15, 2026
9c3fc7b
fix(download): ensure metadata embedding works for home page downloads
alltechdev Mar 15, 2026
0c3855d
fix(download): use 'Singles' folder for songs without album
alltechdev Mar 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/actions/setup-coverart/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Setup Coverart Native Library
description: Download prebuilt coverart native library from workflow artifacts

runs:
using: composite
steps:
- name: Download coverart library
run: |
echo "Downloading coverart library..."

# Use nightly.link to get public URL for the artifact
DOWNLOAD_URL="https://nightly.link/MetrolistGroup/metrolist-coverart-lib/workflows/build-release/main/libcoverart-jniLibs.zip"

curl -L -o /tmp/libcoverart-jniLibs.zip "$DOWNLOAD_URL"

# Extract to app/src/main/jniLibs
mkdir -p app/src/main/jniLibs
unzip -o /tmp/libcoverart-jniLibs.zip -d app/src/main/jniLibs
Comment on lines +14 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for the curl download.

Unlike the companion script app/setup_coverart.sh which checks curl's exit code, this action proceeds even if the download fails (e.g., 404, network error). This can cause confusing unzip errors later.

🛠️ Proposed fix
-        curl -L -o /tmp/libcoverart-jniLibs.zip "$DOWNLOAD_URL"
+        curl -fL -o /tmp/libcoverart-jniLibs.zip "$DOWNLOAD_URL" || {
+          echo "Failed to download coverart library from $DOWNLOAD_URL"
+          exit 1
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
curl -L -o /tmp/libcoverart-jniLibs.zip "$DOWNLOAD_URL"
# Extract to app/src/main/jniLibs
mkdir -p app/src/main/jniLibs
unzip -o /tmp/libcoverart-jniLibs.zip -d app/src/main/jniLibs
curl -fL -o /tmp/libcoverart-jniLibs.zip "$DOWNLOAD_URL" || {
echo "Failed to download coverart library from $DOWNLOAD_URL"
exit 1
}
# Extract to app/src/main/jniLibs
mkdir -p app/src/main/jniLibs
unzip -o /tmp/libcoverart-jniLibs.zip -d app/src/main/jniLibs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/setup-coverart/action.yml around lines 14 - 18, The curl
download step using DOWNLOAD_URL currently doesn't check for failures before
attempting to unzip /tmp/libcoverart-jniLibs.zip; update the action to validate
the download succeeded (check curl's exit status or verify the file exists and
is non-empty) and fail fast with a clear error message if the download failed,
e.g., after the curl to /tmp/libcoverart-jniLibs.zip, check the exit code or
file size and abort (do not proceed to unzip -o) so subsequent unzip operations
won't run on a missing or invalid archive.


# Cleanup
rm /tmp/libcoverart-jniLibs.zip

echo "Coverart library installed:"
find app/src/main/jniLibs -name "*.so" -exec ls -la {} \;
shell: bash
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
- name: Setup and Generate Protobuf
uses: ./.github/actions/setup-protobuf

- name: Setup Coverart Native Library
uses: ./.github/actions/setup-coverart

- name: Grant execute permission for gradlew
run: chmod +x gradlew

Expand Down Expand Up @@ -123,6 +126,9 @@ jobs:
- name: Setup and Generate Protobuf
uses: ./.github/actions/setup-protobuf

- name: Setup Coverart Native Library
uses: ./.github/actions/setup-coverart

- name: Grant execute permission for gradlew
run: chmod +x gradlew

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
- name: Setup and Generate Protobuf
uses: ./.github/actions/setup-protobuf

- name: Setup Coverart Native Library
uses: ./.github/actions/setup-coverart

- name: Grant execute permission for gradlew
run: chmod +x gradlew

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build_quick.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ jobs:
- name: Setup and Generate Protobuf
uses: ./.github/actions/setup-protobuf

- name: Setup Coverart Native Library
uses: ./.github/actions/setup-coverart

- name: Grant execute permission for gradlew
run: chmod +x gradlew

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ jobs:
- name: Setup and Generate Protobuf
uses: ./.github/actions/setup-protobuf

- name: Setup Coverart Native Library
uses: ./.github/actions/setup-coverart

- name: Grant execute permission for gradlew
run: chmod +x gradlew

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,6 @@ app/src/main/java/com/metrolist/music/listentogether/proto/*
# FFTW third-party library build artifacts
.build-fftw
app/src/main/cpp/coverart/

# Prebuilt native libraries (downloaded during build)
app/src/main/jniLibs/
8 changes: 8 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@
-keep class com.metrolist.music.listentogether.proto.** { *; }
-keepclassmembers class com.metrolist.music.listentogether.proto.** { *; }

## CoverArt Native JNI and metadata embedding
-keep class com.metrolist.music.utils.CoverArtNative { *; }
-keepclassmembers class com.metrolist.music.utils.CoverArtNative {
native <methods>;
}
-keep class com.metrolist.music.utils.CoverArtEmbedder { *; }
-keep class com.metrolist.music.utils.DownloadExportHelper { *; }

## Shazam Models
-keep class com.metrolist.shazamkit.models.** { *; }
-keepclassmembers class com.metrolist.shazamkit.models.** {
Expand Down
14 changes: 10 additions & 4 deletions app/schemas/com.metrolist.music.db.InternalDatabase/35.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 35,
"identityHash": "73924a5ef1b9fb713b5e197988a0c633",
"identityHash": "bc001f6b5493d53559519d1df971006d",
"entities": [
{
"tableName": "song",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, `isEpisode` INTEGER NOT NULL DEFAULT false, `playbackPosition` INTEGER DEFAULT NULL, `uploadEntityId` TEXT DEFAULT NULL, PRIMARY KEY(`id`))",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, `isEpisode` INTEGER NOT NULL DEFAULT false, `playbackPosition` INTEGER DEFAULT NULL, `uploadEntityId` TEXT DEFAULT NULL, `downloadUri` TEXT DEFAULT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
Expand Down Expand Up @@ -160,6 +160,12 @@
"columnName": "uploadEntityId",
"affinity": "TEXT",
"defaultValue": "NULL"
},
{
"fieldPath": "downloadUri",
"columnName": "downloadUri",
"affinity": "TEXT",
"defaultValue": "NULL"
}
],
"primaryKey": {
Expand Down Expand Up @@ -1335,7 +1341,7 @@
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '73924a5ef1b9fb713b5e197988a0c633')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc001f6b5493d53559519d1df971006d')"
]
}
}
}
12 changes: 9 additions & 3 deletions app/schemas/com.metrolist.music.db.InternalDatabase/36.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 36,
"identityHash": "afcd734f45bc50034a6692f5255e7b92",
"identityHash": "35dab5008e4f24f10662df49b1e52193",
"entities": [
{
"tableName": "song",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, `isEpisode` INTEGER NOT NULL DEFAULT false, `playbackPosition` INTEGER DEFAULT NULL, `uploadEntityId` TEXT DEFAULT NULL, `isCached` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, `isEpisode` INTEGER NOT NULL DEFAULT false, `playbackPosition` INTEGER DEFAULT NULL, `uploadEntityId` TEXT DEFAULT NULL, `isCached` INTEGER NOT NULL DEFAULT 0, `downloadUri` TEXT DEFAULT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
Expand Down Expand Up @@ -167,6 +167,12 @@
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "downloadUri",
"columnName": "downloadUri",
"affinity": "TEXT",
"defaultValue": "NULL"
}
],
"primaryKey": {
Expand Down Expand Up @@ -1342,7 +1348,7 @@
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'afcd734f45bc50034a6692f5255e7b92')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '35dab5008e4f24f10662df49b1e52193')"
]
}
}
28 changes: 28 additions & 0 deletions app/setup_coverart.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash
# Downloads the prebuilt coverart native library for local development

DOWNLOAD_URL="https://nightly.link/MetrolistGroup/metrolist-coverart-lib/workflows/build-release/main/libcoverart-jniLibs.zip"

echo "Downloading coverart library from latest build..."

# Download the zip file
curl -L -o /tmp/libcoverart-jniLibs.zip "$DOWNLOAD_URL"

if [ $? -ne 0 ]; then
echo "Failed to download from ${DOWNLOAD_URL}"
echo "The workflow may not have run yet. Please check:"
echo "https://github.com/MetrolistGroup/metrolist-coverart-lib/actions"
exit 1
fi

# Create jniLibs directory
mkdir -p src/main/jniLibs

# Extract
unzip -o /tmp/libcoverart-jniLibs.zip -d src/main/jniLibs

# Cleanup
rm /tmp/libcoverart-jniLibs.zip

echo "Coverart library installed successfully:"
find src/main/jniLibs -name "*.so" -exec ls -la {} \;
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ val MaxImageCacheSizeKey = intPreferencesKey("maxImageCacheSize")
val MaxSongCacheSizeKey = intPreferencesKey("maxSongCacheSize")
val EnableSongCacheKey = booleanPreferencesKey("enableSongCache")

// Custom download path
val CustomDownloadPathEnabledKey = booleanPreferencesKey("customDownloadPathEnabled")
val CustomDownloadPathUriKey = stringPreferencesKey("customDownloadPathUri")

val PauseListenHistoryKey = booleanPreferencesKey("pauseListenHistory")
val PauseSearchHistoryKey = booleanPreferencesKey("pauseSearchHistory")
val DisableScreenshotKey = booleanPreferencesKey("disableScreenshot")
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/kotlin/com/metrolist/music/db/DatabaseDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,12 @@ interface DatabaseDao {
@Query("SELECT playbackPosition FROM song WHERE id = :songId")
fun playbackPositionFlow(songId: String): Flow<Long?>

@Query("UPDATE song SET downloadUri = :uri WHERE id = :songId")
fun updateDownloadUri(songId: String, uri: String?)

@Query("SELECT downloadUri FROM song WHERE id = :songId")
fun getDownloadUri(songId: String): String?

@Transaction
@Query("SELECT * FROM song WHERE isUploaded = 1 ORDER BY dateDownload")
fun uploadedSongsByCreateDateAsc(): Flow<List<Song>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ data class SongEntity(
@ColumnInfo(name = "uploadEntityId", defaultValue = "NULL")
val uploadEntityId: String? = null,
@ColumnInfo(name = "isCached", defaultValue = "0")
val isCached: Boolean = false
val isCached: Boolean = false,
@ColumnInfo(name = "downloadUri", defaultValue = "NULL")
val downloadUri: String? = null
) {
fun localToggleLike() = copy(
liked = !liked,
Expand Down
Loading
Loading