Skip to content

Commit

Permalink
Merge pull request #78 from BobbyESP/master
Browse files Browse the repository at this point in the history
feat: embed lyrics in file
  • Loading branch information
Lambada10 authored Sep 1, 2024
2 parents 3dc410d + 2dfbbed commit edc31ba
Show file tree
Hide file tree
Showing 17 changed files with 311 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ dependencies {
implementation(libs.androidx.preference)
implementation(libs.ktor.core)
implementation(libs.ktor.cio)
implementation(libs.taglib)
implementation(libs.datastore.preferences)
debugImplementation(libs.ui.tooling)
debugImplementation(libs.ui.tooling.preview)
Expand Down
20 changes: 14 additions & 6 deletions app/src/main/java/pl/lambada/songsync/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import java.io.File
*/
class MainActivity : ComponentActivity() {
val viewModel: MainViewModel by viewModels()

/**
* Called when the activity is starting.
*
Expand Down Expand Up @@ -100,7 +101,8 @@ class MainActivity : ComponentActivity() {
viewModel.sdCardPath = sdCardPath
}

val includeTranslation = dataStore.get(booleanPreferencesKey("include_translation"), false)
val includeTranslation =
dataStore.get(booleanPreferencesKey("include_translation"), false)
viewModel.includeTranslation = includeTranslation

val blacklist = dataStore.get(stringPreferencesKey("blacklist"), null)
Expand All @@ -111,9 +113,13 @@ class MainActivity : ComponentActivity() {
val hideLyrics = dataStore.get(booleanPreferencesKey("hide_lyrics"), false)
viewModel.hideLyrics = hideLyrics

val provider = dataStore.get(stringPreferencesKey("provider"), Providers.SPOTIFY.displayName)
val provider =
dataStore.get(stringPreferencesKey("provider"), Providers.SPOTIFY.displayName)
viewModel.provider = Providers.entries.find { it.displayName == provider }!!

val embedLyrics = dataStore.get(booleanPreferencesKey("embed_lyrics"), false)
viewModel.embedLyricsInFile = embedLyrics

// Get token upon app start
launch(Dispatchers.IO) {
try {
Expand Down Expand Up @@ -157,7 +163,7 @@ class MainActivity : ComponentActivity() {
}
)

Surface( modifier = Modifier.fillMaxSize() ) {
Surface(modifier = Modifier.fillMaxSize()) {
if (!hasLoadedPermissions) {
LoadingScreen()
} else if (!hasPermissions) {
Expand Down Expand Up @@ -191,14 +197,15 @@ class MainActivity : ComponentActivity() {
NoInternetDialog(
onConfirm = { finishAndRemoveTask() },
onIgnore = {
internetConnection = true // assume connected (if spotify is down, can use other providers)
internetConnection =
true // assume connected (if spotify is down, can use other providers)
}
)
}
}
}
}
}
}

override fun onResume() {
val notificationManager =
Expand Down Expand Up @@ -229,7 +236,8 @@ class MainActivity : ComponentActivity() {
fun RequestPermissions(onGranted: () -> Unit, context: Context, onDone: () -> Unit) {
var storageManager: ActivityResultLauncher<Intent>? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
storageManager = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
storageManager =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (Environment.isExternalStorageManager()) {
onGranted()
}
Expand Down
105 changes: 102 additions & 3 deletions app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package pl.lambada.songsync.data

import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.RecoverableSecurityException
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kyant.taglib.TagLib
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -70,6 +77,8 @@ class MainViewModel : ViewModel() {
private var appleID = 0L
// TODO: Use values from SongInfo object returned by search instead of storing them here

var embedLyricsInFile = false

/**
* Refreshes the access token by sending a request to the Spotify API.
*/
Expand Down Expand Up @@ -124,7 +133,11 @@ class MainViewModel : ViewModel() {
when (this.provider) {
Providers.SPOTIFY -> SpotifyLyricsAPI().getSyncedLyrics(songLink, version)
Providers.LRCLIB -> LRCLibAPI().getSyncedLyrics(this.lrcLibID)
Providers.NETEASE -> NeteaseAPI().getSyncedLyrics(this.neteaseID, includeTranslation)
Providers.NETEASE -> NeteaseAPI().getSyncedLyrics(
this.neteaseID,
includeTranslation
)

Providers.APPLE -> AppleAPI().getSyncedLyrics(this.appleID)
}
} catch (e: Exception) {
Expand All @@ -150,6 +163,37 @@ class MainViewModel : ViewModel() {
return latestVersion > currentVersion
}

@SuppressLint("Range")
private fun getFileDescriptorFromPath(
context: Context, filePath: String, mode: String = "r"
): ParcelFileDescriptor? {
val resolver: ContentResolver = context.contentResolver
val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI

val projection = arrayOf(MediaStore.Files.FileColumns._ID)
val selection = "${MediaStore.Files.FileColumns.DATA}=?"
val selectionArgs = arrayOf(filePath)

resolver.query(uri, projection, selection, selectionArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val fileId: Int =
cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID))
if (fileId == -1) {
return null
} else {
val fileUri: Uri = Uri.withAppendedPath(uri, fileId.toString())
try {
return resolver.openFileDescriptor(fileUri, mode)
} catch (e: FileNotFoundException) {
Log.e("MainViewModel", "File not found: ${e.message}")
}
}
}
}

return null
}

/**
* Loads all songs from the MediaStore.
* @param context The application context.
Expand Down Expand Up @@ -223,17 +267,20 @@ class MainViewModel : ViewModel() {
val data: List<Song> = when {
cachedFilteredSongs.value.isNotEmpty() -> cachedFilteredSongs.value
cachedSongs != null -> cachedSongs!!
else -> { return@launch }
else -> {
return@launch
}
}

val results = data.filter {
it.title?.contains(query, ignoreCase = true) == true ||
it.artist?.contains(query, ignoreCase = true) == true
it.artist?.contains(query, ignoreCase = true) == true
}

_searchResults.value = results
}
}

/**
* Loads all songs' folders
* @param context The application context.
Expand Down Expand Up @@ -273,10 +320,12 @@ class MainViewModel : ViewModel() {
)
}
}

hideLyrics -> {
_cachedFilteredSongs?.value = cachedSongs!!
.filter { it.filePath.toLrcFile()?.exists() != true }
}

hideFolders -> {
_cachedFilteredSongs?.value = cachedSongs!!.filter {
!blacklistedFolders.contains(
Expand All @@ -287,11 +336,61 @@ class MainViewModel : ViewModel() {
)
}
}

else -> {
_cachedFilteredSongs?.value = emptyList()
}
}
}

fun embedLyricsInFile(
context: Context,
filePath: String,
lyrics: String,
securityExceptionHandler: (PendingIntent) -> Unit = {}
): Boolean {
return try {
val fd = getFileDescriptorFromPath(context, filePath, mode = "w")
?: throw IllegalStateException("File descriptor is null")

val fileDescriptor = fd.dup().detachFd()

val metadata = TagLib.getMetadata(fileDescriptor, false) ?: throw IllegalStateException(
"Metadata is null"
)

fd.dup().detachFd().let {
TagLib.savePropertyMap(
it, propertyMap = metadata.propertyMap.apply {
put("LYRICS", arrayOf(lyrics))
}
)
}

true
} catch (securityException: SecurityException) {
handleSecurityException(securityException, securityExceptionHandler)
false
} catch (e: Exception) {
Log.e("MainViewModel", "Error embedding lyrics: ${e.message}")
false
}
}

private fun handleSecurityException(
securityException: SecurityException, intentPassthrough: (PendingIntent) -> Unit
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException =
securityException as? RecoverableSecurityException ?: throw RuntimeException(
securityException.message, securityException
)

intentPassthrough(recoverableSecurityException.userAction.actionIntent)
} else {
throw RuntimeException(securityException.message, securityException)
}
}
}

class NoTrackFoundException : Exception()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class AppleAPI {
songName = result.songName,
artistName = result.artistName,
songLink = result.url,
albumCoverLink = result.artwork.replace("{w}", "100").replace("{h}", "100").replace("{f}", "png"),
albumCoverLink = result.artwork.replace("{w}", "100").replace("{h}", "100")
.replace("{f}", "png"),
appleID = result.id
)
}
Expand Down Expand Up @@ -80,11 +81,13 @@ class AppleAPI {
syncedLyrics.append("<${line.endtime.toLrcTimestamp()}>\n")
}
}

"Line" -> {
for (line in lines) {
syncedLyrics.append("[${line.timestamp.toLrcTimestamp()}]${line.text[0].text}\n")
}
}

else -> return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.parameter
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import pl.lambada.songsync.data.EmptyQueryException
import pl.lambada.songsync.data.InternalErrorException
Expand All @@ -15,7 +13,6 @@ import pl.lambada.songsync.domain.model.lyrics_providers.others.NeteaseLyricsRes
import pl.lambada.songsync.domain.model.lyrics_providers.others.NeteaseResponse
import pl.lambada.songsync.util.networking.Ktor.client
import pl.lambada.songsync.util.networking.Ktor.json
import java.net.URLEncoder

class NeteaseAPI {
private val baseURL = "http://music.163.com/api/"
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/pl/lambada/songsync/ui/Navigator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import kotlinx.serialization.Serializable
import pl.lambada.songsync.data.MainViewModel
import pl.lambada.songsync.domain.model.Song
import pl.lambada.songsync.ui.screens.AboutScreen
import pl.lambada.songsync.ui.screens.SearchScreen
import pl.lambada.songsync.ui.screens.HomeScreen
import pl.lambada.songsync.ui.screens.SearchScreen

/**
* Composable function for handling navigation within the app.
Expand Down
22 changes: 17 additions & 5 deletions app/src/main/java/pl/lambada/songsync/ui/components/CommonTexts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import pl.lambada.songsync.data.MainViewModel

@Composable
fun AnimatedText(
Expand All @@ -52,10 +51,23 @@ fun AnimatedText(
modifier: Modifier = Modifier,
) {
if (animate) {
MarqueeText(text = text, fontSize = fontSize, fontWeight = fontWeight, modifier = modifier, color = color)
}
else {
Text(text = text, fontSize = fontSize, fontWeight = fontWeight, modifier = modifier, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis)
MarqueeText(
text = text,
fontSize = fontSize,
fontWeight = fontWeight,
modifier = modifier,
color = color
)
} else {
Text(
text = text,
fontSize = fontSize,
fontWeight = fontWeight,
modifier = modifier,
color = color,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}

Expand Down
Loading

0 comments on commit edc31ba

Please sign in to comment.