remove spotify as lyrics source

In order to stay on f-droid spotify has to be removed. Doing different flavors is not worth it in my opinion.
This commit is contained in:
2024-07-12 02:15:33 +02:00
committed by Illusion
parent 841629c6e3
commit 7d07433e95
16 changed files with 32 additions and 384 deletions
@@ -5,7 +5,6 @@ import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.adamratzman.spotify.auth.SpotifyDefaultCredentialStore
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
import com.illusionman1212.lyricsgrabbr.data.LyricsRepository
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
@@ -15,24 +14,15 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = PREFERENCES_NAME
)
const val SPOTIFY_REDIRECT_URI = "lyricsgrabbr://spotify-auth"
class LGApp: Application() {
lateinit var settingsPreferencesRepository: SettingsPreferencesRepository
lateinit var homeRepository: HomeRepository
lateinit var lyricsRepository: LyricsRepository
val credentialStore by lazy {
SpotifyDefaultCredentialStore(
clientId = "",
redirectUri = SPOTIFY_REDIRECT_URI,
applicationContext = applicationContext
)
}
override fun onCreate() {
super.onCreate()
settingsPreferencesRepository = SettingsPreferencesRepository(dataStore)
homeRepository = HomeRepository(applicationContext)
lyricsRepository = LyricsRepository(applicationContext)
lyricsRepository = LyricsRepository()
}
}
@@ -1,6 +1,5 @@
package com.illusionman1212.lyricsgrabbr
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -22,7 +21,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -60,10 +58,6 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
val context = LocalContext.current
val activity = context as Activity
val application = activity.application as LGApp
val settingsViewModel: SettingsViewModel = viewModel(
factory = SettingsViewModel.Factory
)
@@ -83,7 +77,6 @@ class MainActivity : ComponentActivity() {
}
runBlocking {
settingsViewModel.updateSpotifyAuthStatus(application.credentialStore.spotifyToken?.refreshToken != null)
appTheme.intValue = settingsViewModel.getAppTheme()
}
@@ -1,29 +0,0 @@
package com.illusionman1212.lyricsgrabbr
import android.widget.Toast
import com.adamratzman.spotify.SpotifyClientApi
import com.adamratzman.spotify.SpotifyScope
import com.adamratzman.spotify.auth.pkce.AbstractSpotifyPkceLoginActivity
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
class SpotifyPkceLoginActivityImpl: AbstractSpotifyPkceLoginActivity() {
override val clientId: String = CLIENT_ID
override val redirectUri: String = SPOTIFY_REDIRECT_URI
override val scopes: List<SpotifyScope> = listOf(SpotifyScope.UserReadCurrentlyPlaying)
override fun onFailure(exception: Exception) {
Toast.makeText(this, "Failed to authenticate", Toast.LENGTH_SHORT).show()
settingsViewModel?.updateSpotifyAuthStatus(false)
}
override fun onSuccess(api: SpotifyClientApi) {
(application as LGApp).credentialStore.setSpotifyApi(api)
Toast.makeText(this, "Successfully authenticated", Toast.LENGTH_SHORT).show()
settingsViewModel?.updateSpotifyAuthStatus(true)
}
companion object {
var CLIENT_ID = ""
var settingsViewModel: SettingsViewModel? = null
}
}
@@ -1,13 +1,6 @@
package com.illusionman1212.lyricsgrabbr.data
import android.content.Context
import android.net.Uri
import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.data.network.GET
import com.illusionman1212.lyricsgrabbr.data.network.NetworkHelper
import com.illusionman1212.lyricsgrabbr.utils.await
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.jsoup.Jsoup
import org.jsoup.internal.StringUtil
import org.jsoup.nodes.Element
@@ -15,58 +8,7 @@ import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
import org.jsoup.select.NodeVisitor
@Serializable
data class LyricLine(
val startTimeMs: ULong,
val words: String,
)
@Serializable
data class Lyrics(
val syncType: String,
val lines: List<LyricLine>,
val language: String,
)
@Serializable
data class SuccessResponse(
val lyrics: Lyrics,
)
class LyricsRepository(private val context: Context) {
private val networkHelper = NetworkHelper(context)
private val json = Json {
ignoreUnknownKeys = true
}
suspend fun fetchLyricsFromSpotify(trackId: String): Pair<String, Boolean> {
val builder = Uri.Builder()
val url = builder.scheme("https")
.authority("api.lyricstify.vercel.app")
.appendPath("v1")
.appendPath("lyrics")
.appendPath(trackId)
.build()
val res = networkHelper.client.newCall(
GET(url.toString())
).await()
val body = res.body?.string()
if (res.isSuccessful) {
return Pair(json.decodeFromString(SuccessResponse.serializer(), body!!)
.lyrics
.lines.joinToString(separator = "\n") { it.words }, true)
} else {
if (res.code == 404) {
return Pair(context.resources.getString(R.string.spotify_no_lyrics_error), false)
}
return Pair(context.resources.getString(R.string.generic_error), false)
}
}
class LyricsRepository {
fun fetchLyricsFromGenius(url: Uri): String {
val doc = Jsoup.connect(url.toString()).userAgent(USER_AGENT).get()
val divs = doc.select("div[data-lyrics-container]")
@@ -2,17 +2,16 @@ package com.illusionman1212.lyricsgrabbr.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.PlainTooltipBox
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.ModifierInfo
import androidx.compose.ui.layout.layoutId
import com.illusionman1212.lyricsgrabbr.utils.mirror
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -22,16 +21,19 @@ fun LGIconButton(
badge: (@Composable BoxScope.() -> Unit)? = null,
content: @Composable () -> Unit
) {
val tooltipState = rememberTooltipState()
Layout(
content = {
PlainTooltipBox(
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
Text(text = tooltip)
},
state = tooltipState,
) {
IconButton(
onClick = onClick,
Modifier.tooltipAnchor(),
content = content
)
}
@@ -73,4 +75,4 @@ fun LGIconButton(
// }
// }
// }
}
}
@@ -90,7 +90,6 @@ import com.illusionman1212.lyricsgrabbr.LGApp
import com.illusionman1212.lyricsgrabbr.ui.components.forwardingPainter
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
import kotlinx.coroutines.launch
@@ -125,7 +124,7 @@ fun HomePage(
) }
var shouldShowNotificationPermissionRationale by remember { mutableStateOf(
ActivityCompat.shouldShowRequestPermissionRationale(
context as android.app.Activity,
context,
POST_NOTIFICATIONS
)
)}
@@ -141,7 +140,6 @@ fun HomePage(
val song = lastNotification.collectAsStateWithLifecycle().value
val uiState = homeViewModel.uiState.collectAsStateWithLifecycle().value
var getLyricsFromSpotify by rememberSaveable { mutableStateOf(false)}
LaunchedEffect(Unit) {
if (!shouldShowNotificationPermissionRationale)
@@ -159,17 +157,9 @@ fun HomePage(
LaunchedEffect(song) {
if (song != null && song.title != uiState.notification?.title && song.artist != uiState.notification?.artist) {
val spotifyApi = application.credentialStore.getSpotifyClientPkceApi()
val useSpotifyApi = song.packageName == "com.spotify.music" && spotifyApi != null
getLyricsFromSpotify = useSpotifyApi
homeViewModel.setSong(song)
if (useSpotifyApi) {
homeViewModel.makeRequestToSpotify(spotifyApi!!)
} else {
homeViewModel.makeRequestToGenius(song.title, song.artist)
}
homeViewModel.makeRequestToGenius(song.title, song.artist)
}
}
@@ -335,7 +325,7 @@ fun HomePage(
},
navigateToLyrics = navigateToLyrics,
setLyricsTitle = { id, title, subtitle, url ->
lyricsViewModel.onSongChange(id, title, subtitle, url, getLyricsFromSpotify)
lyricsViewModel.onSongChange(id, title, subtitle, url)
},
)
}
@@ -54,11 +54,7 @@ fun LyricsPage(
val uiState = lyricsViewModel.uiState.collectAsStateWithLifecycle().value
LaunchedEffect(uiState.song, uiState.artist, uiState.url) {
if (uiState.useSpotifyApi) {
lyricsViewModel.fetchLyricsFromSpotify()
} else {
lyricsViewModel.fetchLyricsFromGenius()
}
lyricsViewModel.fetchLyricsFromGenius()
}
Scaffold(
@@ -1,6 +1,5 @@
package com.illusionman1212.lyricsgrabbr.ui.screens
import android.app.Activity
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -13,19 +12,16 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -36,35 +32,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.adamratzman.spotify.SpotifyClientApi
import com.adamratzman.spotify.SpotifyException
import com.adamratzman.spotify.auth.pkce.startSpotifyClientPkceLoginActivity
import com.illusionman1212.lyricsgrabbr.LGApp
import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.SpotifyPkceLoginActivityImpl
import com.illusionman1212.lyricsgrabbr.ui.components.LGAlertDialog
import com.illusionman1212.lyricsgrabbr.ui.components.LGRadioButton
import com.illusionman1212.lyricsgrabbr.ui.theme.ColorSecondaryLight
import com.illusionman1212.lyricsgrabbr.data.Theme
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
import com.illusionman1212.lyricsgrabbr.utils.mirror
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsState
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
import kotlinx.coroutines.runBlocking
private val Themes = hashMapOf(
Theme.LIGHT.ordinal to R.string.theme_light,
@@ -72,56 +53,6 @@ private val Themes = hashMapOf(
Theme.SYSTEM.ordinal to R.string.theme_system
)
// TODO: this is here as a reference because I think I will need a guard in the future
fun <T> Activity.guardValidSpotifyApi(
classBackTo: Class<out Activity>,
alreadyTriedToReauthenticate: Boolean = false,
block: suspend (api: SpotifyClientApi) -> T
): T? {
return runBlocking {
val app = (application as LGApp)
try {
val token = app.credentialStore.spotifyToken
?: throw SpotifyException.ReAuthenticationNeededException()
val usesPkceAuth = token.refreshToken != null
val api = (if (usesPkceAuth) app.credentialStore.getSpotifyClientPkceApi()
else app.credentialStore.getSpotifyImplicitGrantApi())
?: throw SpotifyException.ReAuthenticationNeededException()
block(api)
} catch (e: SpotifyException) {
e.printStackTrace()
val api = app.credentialStore.getSpotifyClientPkceApi()!!
if (!alreadyTriedToReauthenticate) {
try {
api.refreshToken()
app.credentialStore.spotifyToken = api.token
block(api)
} catch (e: SpotifyException.ReAuthenticationNeededException) {
e.printStackTrace()
return@runBlocking guardValidSpotifyApi(
classBackTo = classBackTo,
alreadyTriedToReauthenticate = true,
block = block
)
} catch (e: IllegalArgumentException) {
e.printStackTrace()
return@runBlocking guardValidSpotifyApi(
classBackTo = classBackTo,
alreadyTriedToReauthenticate = true,
block = block
)
}
} else {
//pkceClassBackTo = classBackTo
startSpotifyClientPkceLoginActivity(SpotifyPkceLoginActivityImpl::class.java)
null
}
}
}
}
@Composable
fun SettingsPage(
goBack: () -> Unit,
@@ -159,7 +90,6 @@ fun SettingsPage(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Appearance(viewModel, uiState)
Lyrics(viewModel, uiState)
}
}
}
@@ -229,84 +159,6 @@ fun ThemeDialog(
}
}
@Composable
fun Lyrics(viewModel: SettingsViewModel, uiState: SettingsState) {
val context = LocalContext.current
val activity = context as Activity
val application = activity.application as LGApp
var isDialogOpen by remember { mutableStateOf(false) }
var spotifyIdValue by remember { mutableStateOf("") }
SettingGroup(stringResource(id = R.string.lyrics)) {
ToggleSetting(
title = stringResource(id = R.string.use_spotify_api),
checked = uiState.isSpotifyAuthed,
icon = painterResource(id = R.drawable.spotify),
onClick = {
if (uiState.isSpotifyAuthed) {
application.credentialStore.clear()
viewModel.updateSpotifyAuthStatus(false)
} else {
isDialogOpen = true
}
},
)
}
if (isDialogOpen) {
LGAlertDialog(
onDismiss = { isDialogOpen = false },
title = stringResource(id = R.string.spotify_client_id),
buttons = {
TextButton(onClick = { isDialogOpen = false }) {
Text(stringResource(id = R.string.cancel))
}
TextButton(onClick = {
SpotifyPkceLoginActivityImpl.CLIENT_ID = spotifyIdValue
SpotifyPkceLoginActivityImpl.settingsViewModel = viewModel
activity.startSpotifyClientPkceLoginActivity(SpotifyPkceLoginActivityImpl::class.java)
isDialogOpen = false
}) {
Text(stringResource(id = R.string.confirm))
}
}
) {
val linkColor = MaterialTheme.colorScheme.surfaceTint
val spotifyDesc = annotatedStringResource(R.string.spotify_client_id_desc) {
when (it.key) {
"href" -> SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)
"weight" -> SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
else -> null
}
}
val textColor = LocalContentColor.current
val uriHandler = LocalUriHandler.current
ClickableText(
spotifyDesc,
onClick = { offset ->
spotifyDesc.getStringAnnotations(tag = "href", start = offset, end = offset)
.firstOrNull()
?.let {
uriHandler.openUri(it.item)
}
},
style = MaterialTheme.typography.bodyMedium.copy(color = textColor),
)
TextField(
value = spotifyIdValue,
onValueChange = { spotifyIdValue = it },
placeholder = {
Text(stringResource(id = R.string.not_set))
},
visualTransformation = PasswordVisualTransformation(),
)
}
}
}
@Composable
fun SettingGroup(title: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
@@ -2,17 +2,14 @@ package com.illusionman1212.lyricsgrabbr.viewmodels
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.adamratzman.spotify.SpotifyClientApi
import com.illusionman1212.lyricsgrabbr.LGApp
import com.illusionman1212.lyricsgrabbr.NotificationEvent
import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -103,32 +100,6 @@ class HomeViewModel(private val context: Context, private val homeRepository: Ho
_uiState.value = _uiState.value.copy(isLoading = true, notification = song, error = null)
}
fun makeRequestToSpotify(spotifyApi: SpotifyClientApi) {
viewModelScope.launch {
val track = spotifyApi.player.getCurrentlyPlaying()?.item
if (track?.id == null) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = context.resources.getString(R.string.no_spotify_song_playing)
)
} else {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = null,
results = listOf(SearchResult(
title = _uiState.value.notification!!.title,
artist = _uiState.value.notification!!.artist,
url = track.asTrack?.externalUrls?.spotify?.toUri() ?: Uri.EMPTY,
artistUrl = track.asTrack?.artists?.get(0)?.externalUrls?.spotify?.toUri() ?: Uri.EMPTY,
thumbnail = track.asTrack?.album?.images?.get(0)?.url?.toUri() ?: Uri.EMPTY,
id = track.id!!,
))
)
}
}
}
fun makeRequestToGenius(song: String, artist: String) {
viewModelScope.launch {
_uiState.update { HomeState(true, uiState.value.results, uiState.value.notification, null) }
@@ -22,7 +22,6 @@ data class LyricsState(
val artist: String = "",
val url: Uri = Uri.EMPTY,
val trackId: String = "",
val useSpotifyApi: Boolean = false,
// set by Lyrics screen
val lyrics: String = "",
@@ -35,24 +34,9 @@ class LyricsViewModel(private val lyricsRepository: LyricsRepository): ViewModel
private val _uiState = MutableStateFlow(LyricsState())
val uiState = _uiState.asStateFlow()
fun onSongChange(trackId: String, title: String, subtitle: String, url: Uri, useSpotifyApi: Boolean) {
fun onSongChange(trackId: String, title: String, subtitle: String, url: Uri) {
_uiState.update {
it.copy(song = title, artist = subtitle, url = url, useSpotifyApi = useSpotifyApi, trackId = trackId)
}
}
fun fetchLyricsFromSpotify() {
_uiState.value = _uiState.value.copy(isLoading = true, lyrics = "")
viewModelScope.launch {
withContext(Dispatchers.IO) {
val (lyrics, isSuccess) = lyricsRepository.fetchLyricsFromSpotify(uiState.value.trackId)
if (isSuccess) {
_uiState.value = _uiState.value.copy(isLoading = false, lyrics = lyrics)
} else {
_uiState.value = _uiState.value.copy(isLoading = false, error = lyrics, lyrics = "")
}
}
it.copy(song = title, artist = subtitle, url = url, trackId = trackId)
}
}
@@ -18,7 +18,6 @@ import kotlinx.coroutines.launch
data class SettingsState(
val appTheme: Int = Theme.SYSTEM.ordinal,
val isSpotifyAuthed: Boolean = false,
)
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() {
@@ -35,10 +34,6 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
}
}
fun updateSpotifyAuthStatus(isAuthed: Boolean) {
_uiState.value = _uiState.value.copy(isSpotifyAuthed = isAuthed)
}
init {
viewModelScope.launch {
settingsPrefsRepo.preferences.collectLatest {