diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ad9a8a8..988471b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -35,6 +35,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/pl/lambada/songsync/data/remote/UserSettingsController.kt b/app/src/main/java/pl/lambada/songsync/data/remote/UserSettingsController.kt
index 7ef190b..346f694 100644
--- a/app/src/main/java/pl/lambada/songsync/data/remote/UserSettingsController.kt
+++ b/app/src/main/java/pl/lambada/songsync/data/remote/UserSettingsController.kt
@@ -35,6 +35,9 @@ class UserSettingsController(private val dataStore: DataStore) {
var multiPersonWordByWord by mutableStateOf(dataStore.get(multiPersonWordByWordKey, false))
private set
+ var syncedMusixmatch by mutableStateOf(dataStore.get(syncedMusixmatchKey, true))
+ private set
+
var pureBlack by mutableStateOf(dataStore.get(pureBlackKey, false))
private set
@@ -74,6 +77,11 @@ class UserSettingsController(private val dataStore: DataStore) {
multiPersonWordByWord = to
}
+ fun updateSyncedMusixmatch(to: Boolean) {
+ dataStore.set(syncedMusixmatchKey, to)
+ syncedMusixmatch = to
+ }
+
fun updateDisableMarquee(to: Boolean) {
dataStore.set(disableMarqueeKey, to)
disableMarquee = to
@@ -96,6 +104,7 @@ private val blacklistedFoldersKey = stringPreferencesKey("blacklist")
private val hideLyricsKey = booleanPreferencesKey("hide_lyrics")
private val includeTranslationKey = booleanPreferencesKey("include_translation")
private val multiPersonWordByWordKey = booleanPreferencesKey("multi_person_word_by_word")
+private val syncedMusixmatchKey = booleanPreferencesKey("synced_lyrics")
private val disableMarqueeKey = booleanPreferencesKey("marquee_disable")
private val pureBlackKey = booleanPreferencesKey("pure_black")
private val sdCardPathKey = stringPreferencesKey("sd_card_path")
\ No newline at end of file
diff --git a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/LyricsProviderService.kt b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/LyricsProviderService.kt
index fb02bbe..87de702 100644
--- a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/LyricsProviderService.kt
+++ b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/LyricsProviderService.kt
@@ -31,8 +31,8 @@ class LyricsProviderService {
// Apple Track ID
private var appleID = 0L
- // Musixmatch Track ID
- private var musixmatchID = 0L
+ // Musixmatch Song Info
+ private var musixmatchSongInfo: SongInfo? = null
// TODO: Use values from SongInfo object returned by search instead of storing them here
/**
@@ -69,7 +69,7 @@ class LyricsProviderService {
} ?: throw NoTrackFoundException()
Providers.MUSIXMATCH -> MusixmatchAPI().getSongInfo(query, offset).also {
- musixmatchID = it?.musixmatchID ?: 0
+ musixmatchSongInfo = it
} ?: throw NoTrackFoundException()
}
} catch (e: InternalErrorException) {
@@ -94,7 +94,8 @@ class LyricsProviderService {
provider: Providers,
// TODO providers could be a sealed interface to include such parameters
includeTranslationNetEase: Boolean = false,
- multiPersonWordByWord: Boolean = false
+ multiPersonWordByWord: Boolean = false,
+ syncedMusixmatch: Boolean = true
): String? {
return try {
when (provider) {
@@ -108,7 +109,10 @@ class LyricsProviderService {
appleID,
multiPersonWordByWord
)
- Providers.MUSIXMATCH -> MusixmatchAPI().getSyncedLyrics(musixmatchID)
+ Providers.MUSIXMATCH -> MusixmatchAPI().getLyrics(
+ musixmatchSongInfo,
+ syncedMusixmatch
+ )
}
} catch (e: Exception) {
null
diff --git a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/MusixmatchAPI.kt b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/MusixmatchAPI.kt
index 6de0b1f..ffb4e4f 100644
--- a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/MusixmatchAPI.kt
+++ b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/MusixmatchAPI.kt
@@ -5,7 +5,6 @@ import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import pl.lambada.songsync.domain.model.SongInfo
-import pl.lambada.songsync.domain.model.lyrics_providers.others.MusixmatchLyricsResponse
import pl.lambada.songsync.domain.model.lyrics_providers.others.MusixmatchSearchResponse
import pl.lambada.songsync.util.EmptyQueryException
import pl.lambada.songsync.util.networking.Ktor.client
@@ -21,25 +20,32 @@ class MusixmatchAPI {
* @return Search result as a SongInfo object.
*/
suspend fun getSongInfo(query: SongInfo, offset: Int = 0): SongInfo? {
- val search = withContext(Dispatchers.IO) {
+ val artistName = withContext(Dispatchers.IO) {
URLEncoder.encode(
- "${query.songName} ${query.artistName}",
+ "${query.artistName}",
Charsets.UTF_8.toString()
)
}
- if (search == " ")
+ val songName = withContext(Dispatchers.IO) {
+ URLEncoder.encode(
+ "${query.songName}",
+ Charsets.UTF_8.toString()
+ )
+ }
+
+ if (artistName == "" || songName == "")
throw EmptyQueryException()
val response = client.get(
- "$baseURL/search?q=$search"
+ "$baseURL/full?artist=$artistName&track=$songName"
)
val responseBody = response.bodyAsText(Charsets.UTF_8)
- if (response.status.value !in 200..299 || responseBody == "[]")
+ if (response.status.value !in 200..299)
return null
- val json = json.decodeFromString>(responseBody)
+ val json = json.decodeFromString(responseBody)
val result = try {
json[offset]
@@ -53,24 +59,20 @@ class MusixmatchAPI {
songLink = result.url,
albumCoverLink = result.artwork,
musixmatchID = result.id,
+ hasSyncedLyrics = result.hasSyncedLyrics,
+ hasUnsyncedLyrics = result.hasSyncedLyrics,
+ syncedLyrics = result.syncedLyrics?.lyrics,
+ unsyncedLyrics = result.unsyncedLyrics?.lyrics
)
}
/**
- * Searches for synced lyrics using the song name and artist name.
- * @param id The ID of the song from search results.
- * @return The synced lyrics as a string.
+ * Returns the lyrics.
+ * @param songInfo The SongInfo of the song from search results.
+ * @return The lyrics as a string or null if the lyrics were not found.
*/
- suspend fun getSyncedLyrics(id: Long): String? {
- val response = client.get(
- "$baseURL/lyrics?id=$id"
- )
- val responseBody = response.bodyAsText(Charsets.UTF_8)
-
- if (response.status.value !in 200..299)
- return null
-
- val json = json.decodeFromString(responseBody)
- return json.lyrics
+ fun getLyrics(songInfo: SongInfo?, synced: Boolean = true): String? {
+ return if(synced) songInfo?.syncedLyrics
+ else songInfo?.unsyncedLyrics
}
}
\ No newline at end of file
diff --git a/app/src/main/java/pl/lambada/songsync/domain/model/SongInfo.kt b/app/src/main/java/pl/lambada/songsync/domain/model/SongInfo.kt
index 598c8fc..1767b8a 100644
--- a/app/src/main/java/pl/lambada/songsync/domain/model/SongInfo.kt
+++ b/app/src/main/java/pl/lambada/songsync/domain/model/SongInfo.kt
@@ -23,4 +23,8 @@ data class SongInfo(
var neteaseID: Long? = null, // Netease-only
var appleID: Long? = null, // Apple-only
var musixmatchID: Long? = null, // Musixmatch-only
+ var hasSyncedLyrics: Boolean? = null, // Musixmatch-only
+ var hasUnsyncedLyrics: Boolean? = null, // Musixmatch-only
+ var syncedLyrics: String? = null, // Musixmatch-only
+ var unsyncedLyrics: String? = null, // Musixmatch-only
) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Musixmatch.kt b/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Musixmatch.kt
index 223ce36..917052a 100644
--- a/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Musixmatch.kt
+++ b/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Musixmatch.kt
@@ -14,14 +14,24 @@ data class MusixmatchSearchResponse(
val url: String,
val albumId: Long,
val hasSyncedLyrics: Boolean,
- val hasUnsyncedLyrics: Boolean
+ val hasUnsyncedLyrics: Boolean,
+ val syncedLyrics: SyncedLyricsResponse? = null,
+ val unsyncedLyrics: UnsyncedLyricsResponse? = null
)
@Serializable
-data class MusixmatchLyricsResponse(
+data class SyncedLyricsResponse(
val id: Long,
val duration: Int,
val language: String,
val updatedTime: String,
+ val lyrics: String
+)
+
+@Serializable
+data class UnsyncedLyricsResponse(
+ val id: Long,
+ val language: String,
+ val updatedTime: String,
val lyrics: String,
)
diff --git a/app/src/main/java/pl/lambada/songsync/ui/components/SongCard.kt b/app/src/main/java/pl/lambada/songsync/ui/components/SongCard.kt
index 2b82b5f..0c34bfc 100644
--- a/app/src/main/java/pl/lambada/songsync/ui/components/SongCard.kt
+++ b/app/src/main/java/pl/lambada/songsync/ui/components/SongCard.kt
@@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -28,6 +29,7 @@ import coil.compose.rememberAsyncImagePainter
import coil.imageLoader
import coil.request.ImageRequest
import pl.lambada.songsync.R
+import pl.lambada.songsync.util.openFileFromPath
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
@@ -41,11 +43,16 @@ fun SharedTransitionScope.SongCard(
animatedVisibilityScope: AnimatedVisibilityScope,
) {
val unknownArtistString = stringResource(R.string.unknown)
+ val context = LocalContext.current
OutlinedCard(
shape = RoundedCornerShape(10.dp),
modifier = CombinedModifier(
- outer = Modifier.fillMaxWidth(),
+ outer = Modifier
+ .fillMaxWidth()
+ .clickable(filePath != null) {
+ openFileFromPath(context, filePath!!)
+ },
inner = modifier
)
) {
diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/about/AboutScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/about/AboutScreen.kt
index 3099d88..57f1f06 100644
--- a/app/src/main/java/pl/lambada/songsync/ui/screens/about/AboutScreen.kt
+++ b/app/src/main/java/pl/lambada/songsync/ui/screens/about/AboutScreen.kt
@@ -29,6 +29,7 @@ import pl.lambada.songsync.ui.screens.about.components.MultiPersonSwitch
import pl.lambada.songsync.ui.screens.about.components.PureBlackThemeSwitch
import pl.lambada.songsync.ui.screens.about.components.SdCardPathSetting
import pl.lambada.songsync.ui.screens.about.components.SupportSection
+import pl.lambada.songsync.ui.screens.about.components.SyncedLyricsSwitch
import pl.lambada.songsync.ui.screens.about.components.TranslationSwitch
import pl.lambada.songsync.ui.screens.about.components.UpdateAvailableDialog
import pl.lambada.songsync.util.ext.getVersion
@@ -89,6 +90,13 @@ fun AboutScreen(
)
}
+ item {
+ SyncedLyricsSwitch(
+ selected = userSettingsController.syncedMusixmatch,
+ onToggle = { userSettingsController.updateSyncedMusixmatch(it) }
+ )
+ }
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
item {
SdCardPathSetting(
diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/about/components/SyncedLyricsSwitch.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/about/components/SyncedLyricsSwitch.kt
new file mode 100644
index 0000000..3c0aa1d
--- /dev/null
+++ b/app/src/main/java/pl/lambada/songsync/ui/screens/about/components/SyncedLyricsSwitch.kt
@@ -0,0 +1,18 @@
+package pl.lambada.songsync.ui.screens.about.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import pl.lambada.songsync.R
+import pl.lambada.songsync.ui.components.AboutItem
+import pl.lambada.songsync.ui.components.SwitchItem
+
+@Composable
+fun SyncedLyricsSwitch(selected: Boolean, onToggle: (Boolean) -> Unit) {
+ AboutItem(label = stringResource(id = R.string.synced_lyrics)) {
+ SwitchItem(
+ label = stringResource(id = R.string.synced_lyrics_summary),
+ selected = selected,
+ onClick = { onToggle(!selected) }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeViewModel.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeViewModel.kt
index fa742a4..0090cc3 100644
--- a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeViewModel.kt
+++ b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeViewModel.kt
@@ -278,7 +278,8 @@ class HomeViewModel(
version,
provider = userSettingsController.selectedProvider,
includeTranslationNetEase = userSettingsController.includeTranslation,
- multiPersonWordByWord = userSettingsController.multiPersonWordByWord
+ multiPersonWordByWord = userSettingsController.multiPersonWordByWord,
+ syncedMusixmatch = userSettingsController.syncedMusixmatch
)
}
diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/lyricsFetch/LyricsFetchViewModel.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/lyricsFetch/LyricsFetchViewModel.kt
index 4ff1f27..027baea 100644
--- a/app/src/main/java/pl/lambada/songsync/ui/screens/lyricsFetch/LyricsFetchViewModel.kt
+++ b/app/src/main/java/pl/lambada/songsync/ui/screens/lyricsFetch/LyricsFetchViewModel.kt
@@ -48,7 +48,8 @@ class LyricsFetchViewModel(
version,
userSettingsController.selectedProvider,
userSettingsController.includeTranslation,
- userSettingsController.multiPersonWordByWord
+ userSettingsController.multiPersonWordByWord,
+ userSettingsController.syncedMusixmatch
)
fun loadSongInfo(context: Context, tryingAgain: Boolean = false) {
diff --git a/app/src/main/java/pl/lambada/songsync/util/MiscelaneousUtils.kt b/app/src/main/java/pl/lambada/songsync/util/MiscelaneousUtils.kt
index 40d293b..bde2b66 100644
--- a/app/src/main/java/pl/lambada/songsync/util/MiscelaneousUtils.kt
+++ b/app/src/main/java/pl/lambada/songsync/util/MiscelaneousUtils.kt
@@ -1,8 +1,12 @@
package pl.lambada.songsync.util
import android.content.Context
+import android.content.Intent
+import android.net.Uri
import android.os.Build
import android.widget.Toast
+import androidx.core.content.FileProvider
+import java.io.File
fun isLegacyFileAccessRequired(filePath: String?): Boolean {
// Before Android 11, not in internal storage
@@ -10,6 +14,30 @@ fun isLegacyFileAccessRequired(filePath: String?): Boolean {
&& filePath?.contains("/storage/emulated/0/") == false
}
+fun openFileFromPath(context: Context, filePath: String) {
+ val file = File(filePath)
+ if (!file.exists()) {
+ showToast(context, "File does not exist")
+ return
+ }
+
+ val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ FileProvider.getUriForFile(context, context.packageName + ".provider", file)
+ } else {
+ Uri.fromFile(file)
+ }
+
+ val intent = Intent(Intent.ACTION_VIEW)
+ .setDataAndType(uri, "audio/mp3")
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+
+ if (intent.resolveActivity(context.packageManager) != null) {
+ context.startActivity(intent)
+ } else {
+ Toast.makeText(context, "No app found to open the music file.", Toast.LENGTH_SHORT).show()
+ }
+}
+
fun showToast(context: Context, messageResId: Int, vararg args: Any, long: Boolean = true) {
Toast
.makeText(
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c20f643..9fe76ea 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -125,4 +125,6 @@
You tried to embed the lyrics to a non-local file. Aborting operation.
Multi-person word by word lyrics
Use multi-person lyrics format when getting lyrics from Apple Music
+ Get Synced Lyrics
+ Toggle to switch between synced and unsynced lyrics from Musixmatch.
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..746a0cf
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file