Skip to content

Commit

Permalink
Feature/split synced lyrics (#22)
Browse files Browse the repository at this point in the history
* Add switch for selecting synced/normal lyrics

* Add provider icon to saved songs list
  • Loading branch information
teccheck authored Jan 20, 2024
1 parent ea46364 commit 7da8d2a
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 38 deletions.
5 changes: 2 additions & 3 deletions .idea/gradle.xml

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

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.github.teccheck.fastlyrics.model.SongWithLyrics
import io.github.teccheck.fastlyrics.utils.Utils
import java.util.concurrent.Executors
import dev.forkhandles.result4k.Result
import io.github.teccheck.fastlyrics.model.LyricsType

object LyricStorage {
private const val TAG = "LyricsStorage"
Expand Down Expand Up @@ -73,4 +74,7 @@ object LyricStorage {

fun findSong(title: String, artist: String): SongWithLyrics? =
database.songsDao().findSong(title, artist)

fun findSong(title: String, artist: String, type: LyricsType): SongWithLyrics? =
database.songsDao().findSong(title, artist, type)
}
31 changes: 25 additions & 6 deletions app/src/main/java/io/github/teccheck/fastlyrics/api/LyricsApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import androidx.lifecycle.MutableLiveData
import dev.forkhandles.result4k.Failure
import dev.forkhandles.result4k.Result
import dev.forkhandles.result4k.Success
import io.github.teccheck.fastlyrics.api.provider.Deezer
import io.github.teccheck.fastlyrics.api.provider.Genius
import io.github.teccheck.fastlyrics.api.provider.LyricsProvider
import io.github.teccheck.fastlyrics.exceptions.LyricsApiException
import io.github.teccheck.fastlyrics.exceptions.LyricsNotFoundException
import io.github.teccheck.fastlyrics.model.LyricsType
import io.github.teccheck.fastlyrics.model.SearchResult
import io.github.teccheck.fastlyrics.model.SongMeta
import io.github.teccheck.fastlyrics.model.SongWithLyrics
Expand All @@ -17,12 +20,17 @@ object LyricsApi {

private const val TAG = "LyricsApi"

private val executor = Executors.newSingleThreadExecutor()
private val executor = Executors.newFixedThreadPool(2)

private var providers: Array<LyricsProvider> = arrayOf(Genius)
private var providers_synced: Array<LyricsProvider> = arrayOf(Deezer)

private var providers: Array<LyricsProvider> = LyricsProvider.getAllProviders()
private val provider: LyricsProvider
get() = providers.first()

private val provider_synced: LyricsProvider
get() = providers_synced.first()

fun setProviderOrder(order: Array<String>) {
val all = LyricsProvider.getAllProviders()
providers = order.mapNotNull { name -> all.find { provider -> provider.getName() == name } }
Expand All @@ -31,16 +39,20 @@ object LyricsApi {

fun getLyricsAsync(
songMeta: SongMeta,
liveDataTarget: MutableLiveData<Result<SongWithLyrics, LyricsApiException>>
liveDataTarget: MutableLiveData<Result<SongWithLyrics, LyricsApiException>>,
synced: Boolean = false
) {
executor.submit {
val song = songMeta.artist?.let { LyricStorage.findSong(songMeta.title, it) }
Log.d(TAG, "getLyricsAsync($synced)")

val type = if (synced) LyricsType.LRC else LyricsType.RAW_TEXT
val song = songMeta.artist?.let { LyricStorage.findSong(songMeta.title, it, type) }
if (song != null) {
liveDataTarget.postValue(Success(song))
return@submit
}

val result = fetchLyrics(songMeta)
val result = fetchLyrics(songMeta, synced)
liveDataTarget.postValue(result)

if (result is Success) {
Expand Down Expand Up @@ -82,7 +94,8 @@ object LyricsApi {
}

private fun fetchLyrics(
songMeta: SongMeta
songMeta: SongMeta,
synced: Boolean = false
): Result<SongWithLyrics, LyricsApiException> {
var searchQuery = songMeta.title
if (songMeta.artist != null) {
Expand All @@ -92,6 +105,12 @@ object LyricsApi {
var bestResult: SearchResult? = null
var bestResultScore = 0.0

val providers = if (synced) {
providers_synced
} else {
providers
}

for (provider in providers) {
val search = provider.search(searchQuery)
if (search !is Success)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.teccheck.fastlyrics.api.storage
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import io.github.teccheck.fastlyrics.model.LyricsType
import io.github.teccheck.fastlyrics.model.SongWithLyrics

@Dao
Expand All @@ -14,6 +15,10 @@ interface SongsDao {
@Query("SELECT * FROM songs WHERE title = :title AND artist = :artist")
fun findSong(title: String, artist: String): SongWithLyrics?


@Query("SELECT * FROM songs WHERE title = :title AND artist = :artist AND type = :type")
fun findSong(title: String, artist: String, type: LyricsType): SongWithLyrics?

@Query("SELECT * FROM songs WHERE id = :id")
fun getSong(id: Long): SongWithLyrics?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.squareup.picasso.Picasso
import dev.forkhandles.result4k.Failure
import dev.forkhandles.result4k.Success
import dev.forkhandles.result4k.Result
import io.github.teccheck.fastlyrics.R
import io.github.teccheck.fastlyrics.Settings
import io.github.teccheck.fastlyrics.api.provider.LyricsProvider
Expand Down Expand Up @@ -58,10 +59,23 @@ class FastLyricsFragment : Fragment() {
}

lyricsViewModel.songWithLyrics.observe(viewLifecycleOwner) { result ->
binding.header.syncedLyricsSwitch.isChecked = false
binding.refreshLayout.isRefreshing = false
displayResult(result)
}

lyricsViewModel.songWithLyricsSynced.observe(viewLifecycleOwner) { result ->
binding.refreshLayout.isRefreshing = false

when (result) {
is Success -> displaySongWithLyrics(result.value)
is Failure -> displayError(result.reason)
is Success -> {
lyricsViewModel.syncedLyricsAvailable = true
binding.header.syncedLyricsAvailable.visibility = View.VISIBLE
}
is Failure -> {
binding.header.syncedLyricsAvailable.visibility = View.GONE
binding.header.syncedLyricsSwitch.isChecked = false
}
}
}

Expand All @@ -72,6 +86,16 @@ class FastLyricsFragment : Fragment() {
)
binding.refreshLayout.setOnRefreshListener { loadLyricsForCurrentSong() }

binding.header.syncedLyricsSwitch.setOnCheckedChangeListener { _, checked ->
val song = if (checked && lyricsViewModel.syncedLyricsAvailable) {
lyricsViewModel.songWithLyricsSynced.value
} else {
lyricsViewModel.songWithLyrics.value
}

song?.let { displayResult(it) }
}

val notificationAccess =
context?.let { DummyNotificationListenerService.canAccessNotifications(it) } ?: false

Expand Down Expand Up @@ -104,6 +128,13 @@ class FastLyricsFragment : Fragment() {
}
}

private fun displayResult(result: Result<SongWithLyrics, LyricsApiException>) {
when (result) {
is Success -> displaySongWithLyrics(result.value)
is Failure -> displayError(result.reason)
}
}

private fun displaySongMeta(songMeta: SongMeta) {
binding.header.container.visibility = View.VISIBLE
binding.errorView.container.visibility = View.GONE
Expand All @@ -128,7 +159,12 @@ class FastLyricsFragment : Fragment() {
val providerIconRes = Utils.getProviderIconRes(it)!!
val icon = AppCompatResources.getDrawable(requireContext(), providerIconRes)
binding.lyricsView.source.setIconResource(providerIconRes)
binding.lyricsView.textLyricsProvider.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
binding.lyricsView.textLyricsProvider.setCompoundDrawablesRelativeWithIntrinsicBounds(
icon,
null,
null,
null
)

val nameRes = Utils.getProviderNameRes(it)!!
val name = getString(nameRes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,25 @@ class FastLyricsViewModel : ViewModel() {

private val _songMeta = MutableLiveData<Result<SongMeta, LyricsApiException>>()
private val _songWithLyrics = MutableLiveData<Result<SongWithLyrics, LyricsApiException>>()
private val _songWithLyricsSynced = MutableLiveData<Result<SongWithLyrics, LyricsApiException>>()
private val _songPosition = MutableLiveData<Long>()

private var songPositionTimer: Timer? = null

val songMeta: LiveData<Result<SongMeta, LyricsApiException>> = _songMeta
val songWithLyrics: LiveData<Result<SongWithLyrics, LyricsApiException>> = _songWithLyrics
val songWithLyricsSynced: LiveData<Result<SongWithLyrics, LyricsApiException>> = _songWithLyricsSynced
val songPosition: LiveData<Long> = _songPosition

var syncedLyricsAvailable = false
var autoRefresh = false

private val songMetaCallback = MediaSession.SongMetaCallback {
if (!autoRefresh) return@SongMetaCallback

_songMeta.postValue(Success(it))
LyricsApi.getLyricsAsync(it, _songWithLyrics)
loadLyrics(it)

}

fun loadLyricsForCurrentSong(context: Context): Boolean {
Expand All @@ -47,12 +51,18 @@ class FastLyricsViewModel : ViewModel() {
_songMeta.value = songMetaResult

if (songMetaResult is Success) {
LyricsApi.getLyricsAsync(songMetaResult.value, _songWithLyrics)
loadLyrics(songMetaResult.value)
}

return songMetaResult is Success
}

private fun loadLyrics(songMeta: SongMeta) {
syncedLyricsAvailable = false
LyricsApi.getLyricsAsync(songMeta, _songWithLyrics)
LyricsApi.getLyricsAsync(songMeta, _songWithLyricsSynced, true)
}

fun setupSongMetaListener() {
MediaSession.registerSongMetaCallback(songMetaCallback)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso
import io.github.teccheck.fastlyrics.R
import io.github.teccheck.fastlyrics.api.provider.LyricsProvider
import io.github.teccheck.fastlyrics.model.SongWithLyrics
import io.github.teccheck.fastlyrics.utils.Utils

class RecyclerAdapter :
RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {
Expand All @@ -22,6 +24,7 @@ class RecyclerAdapter :
private val imageArt: ImageView
private val textTitle: TextView
private val textArtist: TextView
private val iconProvider: ImageView
private val selectionIcon: ImageView

private var song: SongWithLyrics? = null
Expand All @@ -30,13 +33,19 @@ class RecyclerAdapter :
imageArt = view.findViewById(R.id.image_song_art)
textTitle = view.findViewById(R.id.text_song_title)
textArtist = view.findViewById(R.id.text_song_artist)
iconProvider = view.findViewById(R.id.provider_icon)
selectionIcon = view.findViewById(R.id.selection_icon)
}

fun bind(song: SongWithLyrics, selected: Boolean) {
this.song = song
textTitle.text = song.title
textArtist.text = song.artist

LyricsProvider.getProviderByName(song.provider)?.let { provider ->
Utils.getProviderIconRes(provider)?.let { iconProvider.setImageResource(it) }
}

Picasso.get().load(song.artUrl).into(imageArt)
selectionIcon.visibility = if (selected) View.VISIBLE else View.GONE
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ object Utils {
return when(provider) {
Genius -> R.drawable.genius
Deezer -> R.drawable.deezer
else -> null
else -> R.drawable.fastlyrics
}
}

fun getProviderNameRes(provider: LyricsProvider): Int? {
return when(provider) {
Genius -> R.string.source_genius
Deezer -> R.string.source_deezer
else -> null
else -> R.string.app_name
}
}
}
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/fastlyrics.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="#ffffff"
android:pathData="M5,2 L2,22L5,22L6.5,12h8.051L15,9L6.951,9L7.551,5L15.551,5L16,2L8,2ZM19,2 L16.449,19h-6L10,22h8,1L22,2Z" />
</vector>
Loading

0 comments on commit 7da8d2a

Please sign in to comment.