diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 81336232..f567a74a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -184,6 +184,7 @@ android { dependencies { + implementation(project(":lyricsProviders")) // Compose val composeBom = platform(libs.compose.bom) implementation(composeBom) diff --git a/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt b/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt index ad9241f5..91ae8fc0 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt @@ -562,6 +562,7 @@ class DataStoreManager( settingsDataStore.data.map { preferences -> preferences[USING_PROXY] ?: FALSE } + suspend fun setUsingProxy(usingProxy: Boolean) { withContext(Dispatchers.IO) { if (usingProxy) { @@ -575,30 +576,36 @@ class DataStoreManager( } } } + val proxyType = - settingsDataStore.data.map { preferences -> - preferences[PROXY_TYPE] - }.map { - when (it) { - PROXY_TYPE_HTTP -> ProxyType.PROXY_TYPE_HTTP - PROXY_TYPE_SOCKS -> ProxyType.PROXY_TYPE_SOCKS - else -> ProxyType.PROXY_TYPE_HTTP + settingsDataStore.data + .map { preferences -> + preferences[PROXY_TYPE] + }.map { + when (it) { + PROXY_TYPE_HTTP -> ProxyType.PROXY_TYPE_HTTP + PROXY_TYPE_SOCKS -> ProxyType.PROXY_TYPE_SOCKS + else -> ProxyType.PROXY_TYPE_HTTP + } } - } + suspend fun setProxyType(proxyType: ProxyType) { withContext(Dispatchers.IO) { settingsDataStore.edit { settings -> - settings[PROXY_TYPE] = when (proxyType) { - ProxyType.PROXY_TYPE_HTTP -> PROXY_TYPE_HTTP - ProxyType.PROXY_TYPE_SOCKS -> PROXY_TYPE_SOCKS - } + settings[PROXY_TYPE] = + when (proxyType) { + ProxyType.PROXY_TYPE_HTTP -> PROXY_TYPE_HTTP + ProxyType.PROXY_TYPE_SOCKS -> PROXY_TYPE_SOCKS + } } } } + val proxyHost = settingsDataStore.data.map { preferences -> preferences[PROXY_HOST] ?: "" } + suspend fun setProxyHost(proxyHost: String) { withContext(Dispatchers.IO) { settingsDataStore.edit { settings -> @@ -606,10 +613,12 @@ class DataStoreManager( } } } + val proxyPort = settingsDataStore.data.map { preferences -> preferences[PROXY_PORT] ?: 8000 } + suspend fun setProxyPort(proxyPort: Int) { withContext(Dispatchers.IO) { settingsDataStore.edit { settings -> @@ -630,7 +639,7 @@ class DataStoreManager( ProxyType.PROXY_TYPE_HTTP -> Proxy.Type.HTTP ProxyType.PROXY_TYPE_SOCKS -> Proxy.Type.SOCKS }, - java.net.InetSocketAddress(proxyHost, proxyPort) + java.net.InetSocketAddress(proxyHost, proxyPort), ) } else { return@runBlocking null @@ -659,6 +668,7 @@ class DataStoreManager( val MUSIXMATCH_LOGGED_IN = stringPreferencesKey("musixmatch_logged_in") const val YOUTUBE = "youtube" const val MUSIXMATCH = "musixmatch" + const val LRCLIB = "lrclib" val LYRICS_PROVIDER = stringPreferencesKey("lyrics_provider") val TRANSLATION_LANGUAGE = stringPreferencesKey("translation_language") val USE_TRANSLATION_LANGUAGE = stringPreferencesKey("use_translation_language") @@ -688,10 +698,11 @@ class DataStoreManager( const val FALSE = "FALSE" const val PROXY_TYPE_HTTP = "http" const val PROXY_TYPE_SOCKS = "socks" + // Proxy type enum class ProxyType { PROXY_TYPE_HTTP, - PROXY_TYPE_SOCKS + PROXY_TYPE_SOCKS, } } } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt index a014a729..f8e9d03c 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt @@ -12,18 +12,18 @@ import com.maxrave.kotlinytmusicscraper.models.SearchSuggestions import com.maxrave.kotlinytmusicscraper.models.SongItem import com.maxrave.kotlinytmusicscraper.models.WatchEndpoint import com.maxrave.kotlinytmusicscraper.models.YouTubeLocale -import com.maxrave.kotlinytmusicscraper.models.musixmatch.MusixmatchCredential -import com.maxrave.kotlinytmusicscraper.models.musixmatch.MusixmatchTranslationLyricsResponse -import com.maxrave.kotlinytmusicscraper.models.musixmatch.SearchMusixmatchResponse import com.maxrave.kotlinytmusicscraper.models.response.LikeStatus import com.maxrave.kotlinytmusicscraper.models.response.SearchResponse -import com.maxrave.spotify.model.response.spotify.CanvasResponse import com.maxrave.kotlinytmusicscraper.models.simpmusic.GithubResponse import com.maxrave.kotlinytmusicscraper.models.sponsorblock.SkipSegments import com.maxrave.kotlinytmusicscraper.models.youtube.YouTubeInitialPage import com.maxrave.kotlinytmusicscraper.pages.BrowseResult import com.maxrave.kotlinytmusicscraper.pages.PlaylistPage import com.maxrave.kotlinytmusicscraper.pages.SearchPage +import com.maxrave.lyricsproviders.LyricsClient +import com.maxrave.lyricsproviders.models.response.MusixmatchCredential +import com.maxrave.lyricsproviders.models.response.MusixmatchTranslationLyricsResponse +import com.maxrave.lyricsproviders.models.response.SearchMusixmatchResponse import com.maxrave.simpmusic.R import com.maxrave.simpmusic.common.QUALITY import com.maxrave.simpmusic.common.VIDEO_QUALITY @@ -123,6 +123,7 @@ class MainRepository( private val dataStoreManager: DataStoreManager, private val youTube: YouTube, private val spotify: Spotify, + private val lyricsClient: LyricsClient, private val database: MusicDatabase, private val context: Context, ) { @@ -161,11 +162,15 @@ class MainRepository( combine(dataStoreManager.location, dataStoreManager.language) { location, language -> Pair(location, language) }.collectLatest { (location, language) -> - youTube.locale = YouTubeLocale(location, try { - language.substring(0..1) - } catch (e: Exception) { - "en" - }) + youTube.locale = + YouTubeLocale( + location, + try { + language.substring(0..1) + } catch (e: Exception) { + "en" + }, + ) } } val ytCookieJob = @@ -181,7 +186,7 @@ class MainRepository( val musixmatchCookieJob = launch { dataStoreManager.musixmatchCookie.collectLatest { cookie -> - youTube.musixMatchCookie = cookie + lyricsClient.musixmatchCookie = cookie } } val usingProxy = @@ -206,10 +211,16 @@ class MainRepository( data.second, data.third, ) + lyricsClient.setProxy( + data.first == DataStoreManager.Settings.ProxyType.PROXY_TYPE_HTTP, + data.second, + data.third, + ) } } else { youTube.removeProxy() spotify.removeProxy() + lyricsClient.removeProxy() } } } @@ -221,7 +232,7 @@ class MainRepository( } } - fun getMusixmatchCookie() = youTube.getMusixmatchCookie() + fun getMusixmatchCookie() = lyricsClient.musixmatchCookie fun getYouTubeCookie() = youTube.cookie @@ -2226,6 +2237,44 @@ class MainRepository( } } + fun getLrclibLyricsData( + sartist: String, + strack: String, + duration: Int? = null, + ): Flow> = + flow { + val qartist = + sartist + .replace( + Regex("\\((feat\\.|ft.|cùng với|con|mukana|com|avec|合作音乐人: ) "), + " ", + ).replace( + Regex("( và | & | и | e | und |, |和| dan)"), + " ", + ).replace(" ", " ") + .replace(Regex("([()])"), "") + .replace(".", " ") + val qtrack = + strack + .replace( + Regex("\\((feat\\.|ft.|cùng với|con|mukana|com|avec|合作音乐人: ) "), + " ", + ).replace( + Regex("( và | & | и | e | und |, |和| dan)"), + " ", + ).replace(" ", " ") + .replace(Regex("([()])"), "") + .replace(".", " ") + lyricsClient + .getLrclibLyrics(qtrack, qartist, duration) + .onSuccess { + it?.let { emit(Resource.Success(it.toLyrics())) } + }.onFailure { + it.printStackTrace() + emit(Resource.Error("Not found")) + } + }.flowOn(Dispatchers.IO) + suspend fun getLyricsData( sartist: String, strack: String, @@ -2260,12 +2309,12 @@ class MainRepository( .replace(".", " ") val q = "$qtrack $qartist" Log.d(tag, "query: $q") - var musixMatchUserToken = youTube.musixmatchUserToken + var musixMatchUserToken = lyricsClient.musixmatchUserToken if (musixMatchUserToken == null) { - youTube + lyricsClient .getMusixmatchUserToken() .onSuccess { usertoken -> - youTube.musixmatchUserToken = usertoken.message.body.user_token + lyricsClient.musixmatchUserToken = usertoken.message.body.user_token Log.d(tag, "musixMatchUserToken: ${usertoken.message.body.user_token}") musixMatchUserToken = usertoken.message.body.user_token }.onFailure { throwable -> @@ -2273,7 +2322,7 @@ class MainRepository( emit(Pair("", Resource.Error("Not found"))) } } - youTube + lyricsClient .searchMusixmatchTrackId(q, musixMatchUserToken!!) .onSuccess { searchResult -> Log.d( @@ -2318,12 +2367,12 @@ class MainRepository( } val closestIndex = trackLengthList.minByOrNull { - kotlin.math.abs( + abs( it - durationInt, ) } if (closestIndex != null && - kotlin.math.abs( + abs( closestIndex - durationInt, ) < 2 ) { @@ -2413,7 +2462,7 @@ class MainRepository( "item lyrics $track", ) if (id != "" && track != null) { - youTube + lyricsClient .getMusixmatchLyricsByQ(track, musixMatchUserToken!!) .onSuccess { if (it != null) { @@ -2429,7 +2478,7 @@ class MainRepository( } }.onFailure { throwable -> throwable.printStackTrace() - youTube + lyricsClient .getLrclibLyrics(qtrack, qartist, durationInt) .onSuccess { it?.let { emit(Pair(id, Resource.Success(it.toLyrics()))) } @@ -2439,7 +2488,7 @@ class MainRepository( } } } else { - youTube + lyricsClient .fixSearchMusixmatch( q_artist = qartist, q_track = qtrack, @@ -2449,7 +2498,7 @@ class MainRepository( val trackX = it.message.body.track Log.w(tag, "Fix Search Musixmatch: $trackX") if (trackX != null && (abs(trackX.track_length - (durationInt ?: 0)) <= 10)) { - youTube + lyricsClient .getMusixmatchLyricsByQ(trackX, musixMatchUserToken!!) .onSuccess { Log.w(tag, "Item lyrics ${it?.lyrics?.syncType}") @@ -2462,7 +2511,7 @@ class MainRepository( ) } else { Log.w("Lyrics", "Error: Lỗi getLyrics $it") - youTube.getLrclibLyrics(qtrack, qartist, durationInt) + lyricsClient.getLrclibLyrics(qtrack, qartist, durationInt) emit(Pair(id, Resource.Error("Not found"))) } }.onFailure { @@ -2470,7 +2519,7 @@ class MainRepository( emit(Pair(id, Resource.Error("Not found"))) } } else { - youTube + lyricsClient .getLrclibLyrics(qtrack, qartist, durationInt) .onSuccess { it?.let { emit(Pair(trackX?.track_id.toString(), Resource.Success(it.toLyrics()))) } @@ -2481,7 +2530,7 @@ class MainRepository( } }.onFailure { Log.e(tag, "Fix musixmatch search" + it.message.toString()) - youTube + lyricsClient .getLrclibLyrics(qtrack, qartist, durationInt) .onSuccess { Log.w(tag, "Liblrc Item lyrics ${it?.lyrics?.syncType}") @@ -2499,231 +2548,14 @@ class MainRepository( throwable.printStackTrace() emit(Pair("", Resource.Error("Not found"))) } - -// youTube.authentication().onSuccess { token -> -// if (token.accessToken != null) { -// youTube.getSongId(token.accessToken!!, q).onSuccess { spotifyResult -> -// Log.d("SongId", "id: ${spotifyResult.tracks?.items?.get(0)?.id}") -// if (!spotifyResult.tracks?.items.isNullOrEmpty()) { -// val list = arrayListOf() -// for (index in spotifyResult.tracks?.items!!.indices) { -// list.add( -// (spotifyResult.tracks?.items?.get(index)?.name ?: "") + " " + (spotifyResult.tracks?.items?.get( -// index -// )?.artists?.connectArtistsSpotify() ?: "") -// ) -// } -// Log.w("Lyrics", "list: $list") -// var id = "" -// val bestMatchingIndex = bestMatchingIndex(q, list) -// if (q.contains(spotifyResult.tracks?.items?.get(bestMatchingIndex)?.name.toString()) && q.contains(spotifyResult.tracks?.items?.get(bestMatchingIndex)?.artists?.firstOrNull()?.name.toString())) { -// id += spotifyResult.tracks?.items?.get(bestMatchingIndex)?.id -// Log.w("Lyrics", "item: ${spotifyResult.tracks?.items?.get(bestMatchingIndex)?.name}") -// } -// else { -// id += spotifyResult.tracks?.items?.get(0)?.id -// Log.w("Lyrics", "item: ${spotifyResult.tracks?.items?.get(0)?.name}") -// } -// if (id != "") { -// if (dataStoreManager.spotifyAccessTokenExpire.first() != 0L && dataStoreManager.spotifyAccessToken.first() != "" && (dataStoreManager.spotifyAccessTokenExpire.first() -// .toLong()) > Instant.now().toEpochMilli()) { -// Log.d( -// "Lyrics", -// "token: ${dataStoreManager.spotifyAccessToken.first()}" -// ) -// youTube.getLyrics( -// id, -// dataStoreManager.spotifyAccessToken.first() -// ).onSuccess { lyrics -> -// emit(Resource.Success(lyrics.toLyrics())) -// }.onFailure { throwable -> -// Log.d( -// "Lyrics", -// "Error: Lỗi getLyrics ${throwable.message}" -// ) -// spotifyResult.tracks?.items?.firstOrNull()?.id?.let { it2 -> -// youTube.getLyrics( -// it2, -// dataStoreManager.spotifyAccessToken.first() -// ).onSuccess { -// emit(Resource.Success(it.toLyrics())) -// } -// .onFailure { -// Log.d( -// "Lyrics", -// "Error: Lỗi getLyrics lần 2 ${it.message}" -// ) -// emit(Resource.Error("Not found")) -// } -// } -// emit(Resource.Error("Not found")) -// } -// } -// else { -// youTube.getAccessToken() -// .onSuccess { value: AccessToken -> -// dataStoreManager.setSpotifyAccessToken(value.accessToken!!) -// dataStoreManager.setSpotifyAccessTokenExpire( -// value.accessTokenExpirationTimestampMs!! -// ) -// Log.d( -// "Lyrics", -// "token: ${value.accessToken}" -// ) -// youTube.getLyrics(id, value.accessToken) -// .onSuccess { lyrics -> -// emit(Resource.Success(lyrics.toLyrics())) -// }.onFailure { throwable -> -// throwable.printStackTrace() -// spotifyResult.tracks?.items?.firstOrNull()?.id?.let { it2 -> -// youTube.getLyrics( -// it2, -// value.accessToken -// ).onSuccess { -// emit( -// Resource.Success( -// it.toLyrics() -// ) -// ) -// } -// .onFailure { -// Log.d( -// "Lyrics", -// "Error: Lỗi getLyrics lần 2 ${it.message}" -// ) -// emit( -// Resource.Error( -// "Not found" -// ) -// ) -// } -// } -// emit(Resource.Error("Not found")) -// } -// } -// .onFailure { e -> -// e.printStackTrace() -// emit(Resource.Error("Not found")) -// } -// } -// } -// else { -// Log.w("Lyrics", "Can't find song id") -// emit(Resource.Error("Not found")) -// } -// // bestMatchingIndex(q, list).let { -// // bestMatchingIndex(q, list).let { index -> -// // spotifyResult.tracks?.items?.get(index)?.let { item -> -// // if (list[index].contains(item.name.toString())) { -// // Log.w("Lyrics", "item: ${item.name}") -// // item.id?.let { it1 -> -// // Log.d("Lyrics", "id: $it1") -// // if (dataStoreManager.spotifyAccessTokenExpire.first() != 0L && dataStoreManager.spotifyAccessToken.first() != "" && (dataStoreManager.spotifyAccessTokenExpire.first() -// // .toLong()) > Instant.now().toEpochMilli() -// // ) { -// // Log.d( -// // "Lyrics", -// // "token: ${dataStoreManager.spotifyAccessToken.first()}" -// // ) -// // youTube.getLyrics( -// // it1, -// // dataStoreManager.spotifyAccessToken.first() -// // ).onSuccess { lyrics -> -// // emit(Resource.Success(lyrics.toLyrics())) -// // }.onFailure { -// // Log.d( -// // "Lyrics", -// // "Error: Lỗi getLyrics ${it.message}" -// // ) -// // spotifyResult.tracks?.items?.firstOrNull()?.id?.let { it2 -> -// // youTube.getLyrics( -// // it2, -// // dataStoreManager.spotifyAccessToken.first() -// // ).onSuccess { -// // emit(Resource.Success(it.toLyrics())) -// // } -// // .onFailure { -// // Log.d( -// // "Lyrics", -// // "Error: Lỗi getLyrics lần 2 ${it.message}" -// // ) -// // emit(Resource.Error("Not found")) -// // } -// // } -// // emit(Resource.Error("Not found")) -// // } -// // } else { -// // youTube.getAccessToken() -// // .onSuccess { value: AccessToken -> -// // dataStoreManager.setSpotifyAccessToken(value.accessToken!!) -// // dataStoreManager.setSpotifyAccessTokenExpire( -// // value.accessTokenExpirationTimestampMs!! -// // ) -// // Log.d( -// // "Lyrics", -// // "token: ${value.accessToken}" -// // ) -// // youTube.getLyrics(it1, value.accessToken) -// // .onSuccess { lyrics -> -// // emit(Resource.Success(lyrics.toLyrics())) -// // }.onFailure { -// // it.printStackTrace() -// // spotifyResult.tracks?.items?.firstOrNull()?.id?.let { it2 -> -// // youTube.getLyrics( -// // it2, -// // value.accessToken -// // ).onSuccess { -// // emit( -// // Resource.Success( -// // it.toLyrics() -// // ) -// // ) -// // } -// // .onFailure { -// // Log.d( -// // "Lyrics", -// // "Error: Lỗi getLyrics lần 2 ${it.message}" -// // ) -// // emit( -// // Resource.Error( -// // "Not found" -// // ) -// // ) -// // } -// // } -// // emit(Resource.Error("Not found")) -// // } -// // } -// // .onFailure { e -> -// // e.printStackTrace() -// // emit(Resource.Error("Not found")) -// // } -// // } -// // } -// // } -// // else { -// // -// // } -// // } -// // } -// -// } -// } -// } else { -// emit(Resource.Error("Not found")) -// } -// }.onFailure { -// Log.d("SongId", "Error: ${it.message}") -// emit(Resource.Error("Not found")) -// } } }.flowOn(Dispatchers.IO) suspend fun getTranslateLyrics(id: String): Flow = flow { runCatching { - youTube.musixmatchUserToken?.let { - youTube + lyricsClient.musixmatchUserToken?.let { + lyricsClient .getMusixmatchTranslateLyrics( id, it, @@ -3239,12 +3071,13 @@ class MainRepository( ): Flow = flow { runCatching { - if (youTube.musixmatchUserToken != null && youTube.musixmatchUserToken != "") { - youTube + val userToken = lyricsClient.musixmatchUserToken + if (!userToken.isNullOrEmpty()) { + lyricsClient .postMusixmatchCredentials( email, password, - youTube.musixmatchUserToken!!, + userToken, ).onSuccess { response -> emit(response) }.onFailure { @@ -3252,16 +3085,17 @@ class MainRepository( emit(null) } } else { - youTube + lyricsClient .getMusixmatchUserToken() .onSuccess { usertoken -> - youTube.musixmatchUserToken = usertoken.message.body.user_token + lyricsClient.musixmatchUserToken = usertoken.message.body.user_token + val newUserToken = usertoken.message.body.user_token delay(2000) - youTube + lyricsClient .postMusixmatchCredentials( email, password, - youTube.musixmatchUserToken!!, + newUserToken, ).onSuccess { response -> emit(response) }.onFailure { diff --git a/app/src/main/java/com/maxrave/simpmusic/di/DatabaseModule.kt b/app/src/main/java/com/maxrave/simpmusic/di/DatabaseModule.kt index 6eac24bd..390fc414 100644 --- a/app/src/main/java/com/maxrave/simpmusic/di/DatabaseModule.kt +++ b/app/src/main/java/com/maxrave/simpmusic/di/DatabaseModule.kt @@ -12,6 +12,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.maxrave.kotlinytmusicscraper.YouTube +import com.maxrave.lyricsproviders.LyricsClient import com.maxrave.simpmusic.common.DB_NAME import com.maxrave.simpmusic.data.dataStore.DataStoreManager import com.maxrave.simpmusic.data.db.Converters @@ -185,9 +186,21 @@ val databaseModule = Spotify() } + single(createdAtStart = true) { + LyricsClient(androidContext()) + } + // MainRepository single(createdAtStart = true) { - MainRepository(get(), get(), get(), get(), get(), androidContext()) + MainRepository( + get(), + get(), + get(), + get(), + get(), + get(), + androidContext(), + ) } // List of managers diff --git a/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt b/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt index cebd470d..fc58aca8 100644 --- a/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt +++ b/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt @@ -22,10 +22,10 @@ import androidx.navigation.NavController import androidx.sqlite.db.SimpleSQLiteQuery import com.maxrave.kotlinytmusicscraper.models.SongItem import com.maxrave.kotlinytmusicscraper.models.VideoItem -import com.maxrave.kotlinytmusicscraper.models.musixmatch.MusixmatchTranslationLyricsResponse import com.maxrave.kotlinytmusicscraper.models.response.PipedResponse import com.maxrave.kotlinytmusicscraper.models.youtube.Transcript import com.maxrave.kotlinytmusicscraper.models.youtube.YouTubeInitialPage +import com.maxrave.lyricsproviders.models.response.MusixmatchTranslationLyricsResponse import com.maxrave.simpmusic.R import com.maxrave.simpmusic.common.DownloadState import com.maxrave.simpmusic.common.SETTINGS_FILENAME @@ -593,7 +593,7 @@ fun Iterable.indexMap(): Map { return map } -fun com.maxrave.kotlinytmusicscraper.models.lyrics.Lyrics.toLyrics(): Lyrics { +fun com.maxrave.lyricsproviders.models.lyrics.Lyrics.toLyrics(): Lyrics { val lines: ArrayList = arrayListOf() if (this.lyrics != null) { this.lyrics?.lines?.forEach { @@ -865,7 +865,10 @@ fun LocalDateTime.formatTimeAgo(context: Context): String { } } -fun formatDuration(duration: Long, context: Context): String { +fun formatDuration( + duration: Long, + context: Context, +): String { if (duration < 0L) return context.getString(R.string.na_na) val minutes: Long = TimeUnit.MINUTES.convert(duration, TimeUnit.MILLISECONDS) val seconds: Long = ( diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt b/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt index 1928b165..366f28b7 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt @@ -2,7 +2,6 @@ package com.maxrave.simpmusic.ui.component import android.app.Activity import android.content.Intent -import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.util.Log @@ -74,7 +73,6 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.text.isDigitsOnly import androidx.media3.common.util.UnstableApi @@ -216,7 +214,12 @@ fun NowPlayingBottomSheet( if (mainLyricsProvider) { var selected by remember { mutableIntStateOf( - if (uiState.mainLyricsProvider == DataStoreManager.MUSIXMATCH) 0 else 1, + when (uiState.mainLyricsProvider) { + DataStoreManager.MUSIXMATCH -> 0 + DataStoreManager.YOUTUBE -> 1 + DataStoreManager.LRCLIB -> 2 + else -> 0 + }, ) } @@ -263,6 +266,22 @@ fun NowPlayingBottomSheet( Spacer(modifier = Modifier.size(10.dp)) Text(text = stringResource(id = R.string.youtube_transcript), style = typo.labelSmall) } + Row( + modifier = + Modifier + .padding(horizontal = 4.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = selected == 2, + onClick = { + selected = 2 + }, + ) + Spacer(modifier = Modifier.size(10.dp)) + Text(text = stringResource(id = R.string.lrclib), style = typo.labelSmall) + } } }, confirmButton = { @@ -270,7 +289,12 @@ fun NowPlayingBottomSheet( onClick = { viewModel.onUIEvent( NowPlayingBottomSheetUIEvent.ChangeLyricsProvider( - if (selected == 0) DataStoreManager.MUSIXMATCH else DataStoreManager.YOUTUBE, + when (selected) { + 0 -> DataStoreManager.MUSIXMATCH + 1 -> DataStoreManager.YOUTUBE + 2 -> DataStoreManager.LRCLIB + else -> DataStoreManager.MUSIXMATCH + }, ), ) mainLyricsProvider = false @@ -299,7 +323,7 @@ fun NowPlayingBottomSheet( contentColor = Color.Transparent, dragHandle = null, scrimColor = Color.Black.copy(alpha = .5f), - contentWindowInsets = { WindowInsets(0, 0, 0, 0) } + contentWindowInsets = { WindowInsets(0, 0, 0, 0) }, ) { Card( modifier = @@ -312,7 +336,7 @@ fun NowPlayingBottomSheet( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.verticalScroll(rememberScrollState()) + modifier = Modifier.verticalScroll(rememberScrollState()), ) { Spacer(modifier = Modifier.height(5.dp)) Card( @@ -719,7 +743,7 @@ fun AddToPlaylistModalBottomSheet( contentColor = Color.Transparent, dragHandle = null, scrimColor = Color.Black.copy(alpha = .5f), - contentWindowInsets = { WindowInsets(0, 0, 0, 0) } + contentWindowInsets = { WindowInsets(0, 0, 0, 0) }, ) { Card( modifier = @@ -1207,7 +1231,13 @@ fun EndOfModalBottomSheet() { modifier = Modifier .fillMaxWidth() - .height(WindowInsets.navigationBars.asPaddingValues() - .calculateBottomPadding().value.toInt().dp + 8.dp), + .height( + WindowInsets.navigationBars + .asPaddingValues() + .calculateBottomPadding() + .value + .toInt() + .dp + 8.dp, + ), ) {} } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/screen/home/SettingScreen.kt b/app/src/main/java/com/maxrave/simpmusic/ui/screen/home/SettingScreen.kt index d7409d9d..f2e1b06f 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/screen/home/SettingScreen.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/screen/home/SettingScreen.kt @@ -114,7 +114,6 @@ import com.maxrave.simpmusic.viewModel.SharedViewModel import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.LibsBuilder import kotlinx.coroutines.flow.map -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.koin.androidx.compose.koinViewModel import java.text.SimpleDateFormat import java.time.Instant @@ -235,12 +234,12 @@ fun SettingScreen( title = context.getString(R.string.update_available), message = context.getString(R.string.update_message, res.tagName, formatted, res.body), confirm = - context.getString(R.string.download) to { - uriHandler.openUri( - res.assets?.firstOrNull()?.browserDownloadUrl - ?: "https://github.com/maxrave-dev/SimpMusic/releases", - ) - }, + context.getString(R.string.download) to { + uriHandler.openUri( + res.assets?.firstOrNull()?.browserDownloadUrl + ?: "https://github.com/maxrave-dev/SimpMusic/releases", + ) + }, dismiss = context.getString(R.string.cancel), ), ) @@ -255,9 +254,9 @@ fun SettingScreen( LazyColumn( contentPadding = innerPadding, modifier = - Modifier - .padding(horizontal = 16.dp) - .padding(top = 64.dp), + Modifier + .padding(horizontal = 16.dp) + .padding(top = 64.dp), ) { item(key = "user_interface") { Column { @@ -290,29 +289,29 @@ fun SettingScreen( SettingAlertState( title = context.getString(R.string.language), selectOne = - SettingAlertState.SelectData( - listSelect = - SUPPORTED_LANGUAGE.items.map { - (it.toString() == SUPPORTED_LANGUAGE.getLanguageFromCode(language ?: "en-US")) to it.toString() - }, - ), - confirm = - context.getString(R.string.change) to { state -> - val code = SUPPORTED_LANGUAGE.getCodeFromLanguage(state.selectOne?.getSelected() ?: "English") - viewModel.setBasicAlertData( - SettingBasicAlertState( - title = context.getString(R.string.warning), - message = context.getString(R.string.change_language_warning), - confirm = - context.getString(R.string.change) to { - sharedViewModel.activityRecreate() - viewModel.setBasicAlertData(null) - viewModel.changeLanguage(code) + SettingAlertState.SelectData( + listSelect = + SUPPORTED_LANGUAGE.items.map { + (it.toString() == SUPPORTED_LANGUAGE.getLanguageFromCode(language ?: "en-US")) to it.toString() }, - dismiss = context.getString(R.string.cancel), - ), - ) - }, + ), + confirm = + context.getString(R.string.change) to { state -> + val code = SUPPORTED_LANGUAGE.getCodeFromLanguage(state.selectOne?.getSelected() ?: "English") + viewModel.setBasicAlertData( + SettingBasicAlertState( + title = context.getString(R.string.warning), + message = context.getString(R.string.change_language_warning), + confirm = + context.getString(R.string.change) to { + sharedViewModel.activityRecreate() + viewModel.setBasicAlertData(null) + viewModel.changeLanguage(code) + }, + dismiss = context.getString(R.string.cancel), + ), + ) + }, dismiss = context.getString(R.string.cancel), ), ) @@ -326,18 +325,18 @@ fun SettingScreen( SettingAlertState( title = context.getString(R.string.content_country), selectOne = - SettingAlertState.SelectData( - listSelect = - SUPPORTED_LOCATION.items.map { item -> - (item.toString() == location) to item.toString() - }, - ), + SettingAlertState.SelectData( + listSelect = + SUPPORTED_LOCATION.items.map { item -> + (item.toString() == location) to item.toString() + }, + ), confirm = - context.getString(R.string.change) to { state -> - viewModel.changeLocation( - state.selectOne?.getSelected() ?: "US", - ) - }, + context.getString(R.string.change) to { state -> + viewModel.changeLocation( + state.selectOne?.getSelected() ?: "US", + ) + }, ), ) }, @@ -351,16 +350,16 @@ fun SettingScreen( SettingAlertState( title = context.getString(R.string.quality), selectOne = - SettingAlertState.SelectData( - listSelect = - QUALITY.items.map { item -> - (item.toString() == quality) to item.toString() - }, - ), + SettingAlertState.SelectData( + listSelect = + QUALITY.items.map { item -> + (item.toString() == quality) to item.toString() + }, + ), confirm = - context.getString(R.string.change) to { state -> - viewModel.changeQuality(state.selectOne?.getSelected()) - }, + context.getString(R.string.change) to { state -> + viewModel.changeQuality(state.selectOne?.getSelected()) + }, dismiss = context.getString(R.string.cancel), ), ) @@ -396,16 +395,16 @@ fun SettingScreen( SettingAlertState( title = context.getString(R.string.video_quality), selectOne = - SettingAlertState.SelectData( - listSelect = - VIDEO_QUALITY.items.map { item -> - (item.toString() == videoQuality) to item.toString() - }, - ), + SettingAlertState.SelectData( + listSelect = + VIDEO_QUALITY.items.map { item -> + (item.toString() == videoQuality) to item.toString() + }, + ), confirm = - context.getString(R.string.change) to { state -> - viewModel.changeVideoQuality(state.selectOne?.getSelected() ?: "") - }, + context.getString(R.string.change) to { state -> + viewModel.changeVideoQuality(state.selectOne?.getSelected() ?: "") + }, dismiss = context.getString(R.string.cancel), ), ) @@ -414,10 +413,10 @@ fun SettingScreen( SettingItem( title = stringResource(R.string.send_back_listening_data_to_google), subtitle = - stringResource( - R.string - .upload_your_listening_history_to_youtube_music_server_it_will_make_yt_music_recommendation_system_better_working_only_if_logged_in, - ), + stringResource( + R.string + .upload_your_listening_history_to_youtube_music_server_it_will_make_yt_music_recommendation_system_better_working_only_if_logged_in, + ), smallSubtitle = true, switch = (sendData to { viewModel.setSendBackToGoogle(it) }), ) @@ -435,38 +434,38 @@ fun SettingScreen( SettingItem( title = stringResource(R.string.proxy_type), subtitle = - when (proxyType) { - DataStoreManager.Settings.ProxyType.PROXY_TYPE_HTTP -> stringResource(R.string.http) - DataStoreManager.Settings.ProxyType.PROXY_TYPE_SOCKS -> stringResource(R.string.socks) - }, + when (proxyType) { + DataStoreManager.Settings.ProxyType.PROXY_TYPE_HTTP -> stringResource(R.string.http) + DataStoreManager.Settings.ProxyType.PROXY_TYPE_SOCKS -> stringResource(R.string.socks) + }, onClick = { viewModel.setAlertData( SettingAlertState( title = context.getString(R.string.proxy_type), selectOne = - SettingAlertState.SelectData( - listSelect = - listOf( - (proxyType == DataStoreManager.Settings.ProxyType.PROXY_TYPE_HTTP) to - context.getString( - R.string.http, + SettingAlertState.SelectData( + listSelect = + listOf( + (proxyType == DataStoreManager.Settings.ProxyType.PROXY_TYPE_HTTP) to + context.getString( + R.string.http, + ), + (proxyType == DataStoreManager.Settings.ProxyType.PROXY_TYPE_SOCKS) to + context.getString(R.string.socks), ), - (proxyType == DataStoreManager.Settings.ProxyType.PROXY_TYPE_SOCKS) to - context.getString(R.string.socks), ), - ), confirm = - context.getString(R.string.change) to { state -> - viewModel.setProxy( - if (state.selectOne?.getSelected() == context.getString(R.string.socks)) { - DataStoreManager.Settings.ProxyType.PROXY_TYPE_SOCKS - } else { - DataStoreManager.Settings.ProxyType.PROXY_TYPE_HTTP - }, - proxyHost, - proxyPort, - ) - }, + context.getString(R.string.change) to { state -> + viewModel.setProxy( + if (state.selectOne?.getSelected() == context.getString(R.string.socks)) { + DataStoreManager.Settings.ProxyType.PROXY_TYPE_SOCKS + } else { + DataStoreManager.Settings.ProxyType.PROXY_TYPE_HTTP + }, + proxyHost, + proxyPort, + ) + }, dismiss = context.getString(R.string.cancel), ), ) @@ -481,21 +480,21 @@ fun SettingScreen( title = context.getString(R.string.proxy_host), message = context.getString(R.string.proxy_host_message), textField = - SettingAlertState.TextFieldData( - label = context.getString(R.string.proxy_host), - value = proxyHost, - verifyCodeBlock = { - isValidProxyHost(it) to context.getString(R.string.invalid_host) - }, - ), + SettingAlertState.TextFieldData( + label = context.getString(R.string.proxy_host), + value = proxyHost, + verifyCodeBlock = { + isValidProxyHost(it) to context.getString(R.string.invalid_host) + }, + ), confirm = - context.getString(R.string.change) to { state -> - viewModel.setProxy( - proxyType, - state.textField?.value ?: "", - proxyPort, - ) - }, + context.getString(R.string.change) to { state -> + viewModel.setProxy( + proxyType, + state.textField?.value ?: "", + proxyPort, + ) + }, dismiss = context.getString(R.string.cancel), ), ) @@ -510,21 +509,21 @@ fun SettingScreen( title = context.getString(R.string.proxy_port), message = context.getString(R.string.proxy_port_message), textField = - SettingAlertState.TextFieldData( - label = context.getString(R.string.proxy_port), - value = proxyPort.toString(), - verifyCodeBlock = { - (it.toIntOrNull() != null) to context.getString(R.string.invalid_port) - }, - ), + SettingAlertState.TextFieldData( + label = context.getString(R.string.proxy_port), + value = proxyPort.toString(), + verifyCodeBlock = { + (it.toIntOrNull() != null) to context.getString(R.string.invalid_port) + }, + ), confirm = - context.getString(R.string.change) to { state -> - viewModel.setProxy( - proxyType, - proxyHost, - state.textField?.value?.toIntOrNull() ?: 0, - ) - }, + context.getString(R.string.change) to { state -> + viewModel.setProxy( + proxyType, + proxyHost, + state.textField?.value?.toIntOrNull() ?: 0, + ) + }, dismiss = context.getString(R.string.cancel), ), ) @@ -588,50 +587,53 @@ fun SettingScreen( SettingItem( title = stringResource(R.string.main_lyrics_provider), subtitle = - when (mainLyricsProvider) { - DataStoreManager.MUSIXMATCH -> stringResource(R.string.musixmatch) - DataStoreManager.YOUTUBE -> stringResource(R.string.youtube_transcript) - else -> stringResource(R.string.unknown) - }, + when (mainLyricsProvider) { + DataStoreManager.MUSIXMATCH -> stringResource(R.string.musixmatch) + DataStoreManager.YOUTUBE -> stringResource(R.string.youtube_transcript) + DataStoreManager.LRCLIB -> stringResource(R.string.lrclib) + else -> stringResource(R.string.unknown) + }, onClick = { viewModel.setAlertData( SettingAlertState( title = context.getString(R.string.main_lyrics_provider), selectOne = - SettingAlertState.SelectData( - listSelect = - listOf( - (mainLyricsProvider == DataStoreManager.MUSIXMATCH) to context.getString(R.string.musixmatch), - (mainLyricsProvider == DataStoreManager.YOUTUBE) to context.getString(R.string.youtube_transcript), + SettingAlertState.SelectData( + listSelect = + listOf( + (mainLyricsProvider == DataStoreManager.MUSIXMATCH) to context.getString(R.string.musixmatch), + (mainLyricsProvider == DataStoreManager.YOUTUBE) to context.getString(R.string.youtube_transcript), + (mainLyricsProvider == DataStoreManager.LRCLIB) to context.getString(R.string.lrclib), + ), ), - ), confirm = - context.getString(R.string.change) to { state -> - viewModel.setLyricsProvider( - when (state.selectOne?.getSelected()) { - context.getString(R.string.musixmatch) -> DataStoreManager.MUSIXMATCH - context.getString(R.string.youtube_transcript) -> DataStoreManager.YOUTUBE - else -> DataStoreManager.MUSIXMATCH - }, - ) - }, + context.getString(R.string.change) to { state -> + viewModel.setLyricsProvider( + when (state.selectOne?.getSelected()) { + context.getString(R.string.musixmatch) -> DataStoreManager.MUSIXMATCH + context.getString(R.string.youtube_transcript) -> DataStoreManager.YOUTUBE + context.getString(R.string.lrclib) -> DataStoreManager.LRCLIB + else -> DataStoreManager.MUSIXMATCH + }, + ) + }, ), ) }, ) SettingItem( title = - if (musixmatchLoggedIn) { - stringResource(R.string.log_out_from_musixmatch) - } else { - stringResource(R.string.log_in_to_Musixmatch) - }, + if (musixmatchLoggedIn) { + stringResource(R.string.log_out_from_musixmatch) + } else { + stringResource(R.string.log_in_to_Musixmatch) + }, subtitle = - if (musixmatchLoggedIn) { - stringResource(R.string.logged_in) - } else { - stringResource(R.string.only_support_email_and_password_type) - }, + if (musixmatchLoggedIn) { + stringResource(R.string.logged_in) + } else { + stringResource(R.string.only_support_email_and_password_type) + }, onClick = { if (musixmatchLoggedIn) { viewModel.clearMusixmatchCookie() @@ -654,18 +656,18 @@ fun SettingScreen( SettingAlertState( title = context.getString(R.string.translation_language), textField = - SettingAlertState.TextFieldData( - label = context.getString(R.string.translation_language), - value = musixmatchTranslationLanguage ?: "", - verifyCodeBlock = { - (it.length == 2 && it.isTwoLetterCode()) to context.getString(R.string.invalid_language_code) - }, - ), + SettingAlertState.TextFieldData( + label = context.getString(R.string.translation_language), + value = musixmatchTranslationLanguage ?: "", + verifyCodeBlock = { + (it.length == 2 && it.isTwoLetterCode()) to context.getString(R.string.invalid_language_code) + }, + ), message = context.getString(R.string.translation_language_message), confirm = - context.getString(R.string.change) to { state -> - viewModel.setTranslationLanguage(state.textField?.value ?: "") - }, + context.getString(R.string.change) to { state -> + viewModel.setTranslationLanguage(state.textField?.value ?: "") + }, ), ) }, @@ -679,11 +681,11 @@ fun SettingScreen( SettingItem( title = stringResource(R.string.log_in_to_spotify), subtitle = - if (spotifyLoggedIn) { - stringResource(R.string.logged_in) - } else { - stringResource(R.string.intro_login_to_spotify) - }, + if (spotifyLoggedIn) { + stringResource(R.string.logged_in) + } else { + stringResource(R.string.intro_login_to_spotify) + }, onClick = { if (spotifyLoggedIn) { viewModel.setSpotifyLogIn(false) @@ -726,34 +728,34 @@ fun SettingScreen( SettingAlertState( title = context.getString(R.string.categories_sponsor_block), multipleSelect = - SettingAlertState.SelectData( - listSelect = - listName - .mapIndexed { index, item -> - ( - skipSegments?.contains( - SPONSOR_BLOCK.list.getOrNull(index), - ) == true - ) to item - }.also { - Log.w("SettingScreen", "SettingAlertState: $skipSegments") - Log.w("SettingScreen", "SettingAlertState: $it") - }, - ), + SettingAlertState.SelectData( + listSelect = + listName + .mapIndexed { index, item -> + ( + skipSegments?.contains( + SPONSOR_BLOCK.list.getOrNull(index), + ) == true + ) to item + }.also { + Log.w("SettingScreen", "SettingAlertState: $skipSegments") + Log.w("SettingScreen", "SettingAlertState: $it") + }, + ), confirm = - context.getString(R.string.save) to { state -> - viewModel.setSponsorBlockCategories( - state.multipleSelect - ?.getListSelected() - ?.map { selected -> - listName.indexOf(selected) - }?.mapNotNull { s -> - SPONSOR_BLOCK.list.getOrNull(s).let { - it?.toString() - } - }?.toCollection(ArrayList()) ?: arrayListOf(), - ) - }, + context.getString(R.string.save) to { state -> + viewModel.setSponsorBlockCategories( + state.multipleSelect + ?.getListSelected() + ?.map { selected -> + listName.indexOf(selected) + }?.mapNotNull { s -> + SPONSOR_BLOCK.list.getOrNull(s).let { + it?.toString() + } + }?.toCollection(ArrayList()) ?: arrayListOf(), + ) + }, dismiss = context.getString(R.string.cancel), ), ) @@ -792,9 +794,9 @@ fun SettingScreen( title = context.getString(R.string.clear_player_cache), message = null, confirm = - context.getString(R.string.clear) to { - viewModel.clearPlayerCache() - }, + context.getString(R.string.clear) to { + viewModel.clearPlayerCache() + }, dismiss = context.getString(R.string.cancel), ), ) @@ -809,9 +811,9 @@ fun SettingScreen( title = context.getString(R.string.clear_downloaded_cache), message = null, confirm = - context.getString(R.string.clear) to { - viewModel.clearDownloadedCache() - }, + context.getString(R.string.clear) to { + viewModel.clearDownloadedCache() + }, dismiss = context.getString(R.string.cancel), ), ) @@ -826,9 +828,9 @@ fun SettingScreen( title = context.getString(R.string.clear_thumbnail_cache), message = null, confirm = - context.getString(R.string.clear) to { - viewModel.clearThumbnailCache() - }, + context.getString(R.string.clear) to { + viewModel.clearThumbnailCache() + }, dismiss = context.getString(R.string.cancel), ), ) @@ -843,9 +845,9 @@ fun SettingScreen( title = context.getString(R.string.clear_canvas_cache), message = null, confirm = - context.getString(R.string.clear) to { - viewModel.clearCanvasCache() - }, + context.getString(R.string.clear) to { + viewModel.clearCanvasCache() + }, dismiss = context.getString(R.string.cancel), ), ) @@ -859,18 +861,18 @@ fun SettingScreen( SettingAlertState( title = context.getString(R.string.limit_player_cache), selectOne = - SettingAlertState.SelectData( - listSelect = - LIMIT_CACHE_SIZE.items.map { item -> - (item == LIMIT_CACHE_SIZE.getItemFromData(limitPlayerCache)) to item.toString() - }, - ), + SettingAlertState.SelectData( + listSelect = + LIMIT_CACHE_SIZE.items.map { item -> + (item == LIMIT_CACHE_SIZE.getItemFromData(limitPlayerCache)) to item.toString() + }, + ), confirm = - context.getString(R.string.change) to { state -> - viewModel.setPlayerCacheLimit( - LIMIT_CACHE_SIZE.getDataFromItem(state.selectOne?.getSelected()), - ) - }, + context.getString(R.string.change) to { state -> + viewModel.setPlayerCacheLimit( + LIMIT_CACHE_SIZE.getDataFromItem(state.selectOne?.getSelected()), + ) + }, dismiss = context.getString(R.string.cancel), ), ) @@ -885,108 +887,95 @@ fun SettingScreen( LazyRow( horizontalArrangement = Arrangement.spacedBy(0.dp), modifier = - Modifier - .fillMaxWidth() - .height(8.dp) - .clip(RoundedCornerShape(8.dp)) - .onGloballyPositioned { layoutCoordinates -> - with(localDensity) { - width = - layoutCoordinates.size.width - .toDp() - .value - .toInt() - } - }, + Modifier + .fillMaxWidth() + .height(8.dp) + .clip(RoundedCornerShape(8.dp)) + .onGloballyPositioned { layoutCoordinates -> + with(localDensity) { + width = + layoutCoordinates.size.width + .toDp() + .value + .toInt() + } + }, ) { item { Box( modifier = - Modifier - .width( - (fraction.otherApp * width).dp, - ) - .background( - md_theme_dark_primary, - ) - .fillMaxHeight(), + Modifier + .width( + (fraction.otherApp * width).dp, + ).background( + md_theme_dark_primary, + ).fillMaxHeight(), ) } item { Box( modifier = - Modifier - .width( - (fraction.downloadCache * width).dp, - ) - .background( - Color(0xD540FF17), - ) - .fillMaxHeight(), + Modifier + .width( + (fraction.downloadCache * width).dp, + ).background( + Color(0xD540FF17), + ).fillMaxHeight(), ) } item { Box( modifier = - Modifier - .width( - (fraction.playerCache * width).dp, - ) - .background( - Color(0xD5FFFF00), - ) - .fillMaxHeight(), + Modifier + .width( + (fraction.playerCache * width).dp, + ).background( + Color(0xD5FFFF00), + ).fillMaxHeight(), ) } item { Box( modifier = - Modifier - .width( - (fraction.canvasCache * width).dp, - ) - .background( - Color.Cyan, - ) - .fillMaxHeight(), + Modifier + .width( + (fraction.canvasCache * width).dp, + ).background( + Color.Cyan, + ).fillMaxHeight(), ) } item { Box( modifier = - Modifier - .width( - (fraction.thumbCache * width).dp, - ) - .background( - Color.Magenta, - ) - .fillMaxHeight(), + Modifier + .width( + (fraction.thumbCache * width).dp, + ).background( + Color.Magenta, + ).fillMaxHeight(), ) } item { Box( modifier = - Modifier - .width( - (fraction.appDatabase * width).dp, - ) - .background( - Color.White, - ), + Modifier + .width( + (fraction.appDatabase * width).dp, + ).background( + Color.White, + ), ) } item { Box( modifier = - Modifier - .width( - (fraction.freeSpace * width).dp, - ) - .background( - Color.DarkGray, - ) - .fillMaxHeight(), + Modifier + .width( + (fraction.freeSpace * width).dp, + ).background( + Color.DarkGray, + ).fillMaxHeight(), ) } } @@ -1246,19 +1235,21 @@ fun SettingScreen( LazyColumn(modifier = Modifier.padding(8.dp)) { item { Box( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) + modifier = + Modifier + .fillMaxWidth() + .height(48.dp), ) { IconButton( onClick = { showYouTubeAccountDialog = false }, colors = - IconButtonDefaults.iconButtonColors().copy( - contentColor = Color.White, - ), - modifier = Modifier - .align(Alignment.CenterStart) - .fillMaxHeight(), + IconButtonDefaults.iconButtonColors().copy( + contentColor = Color.White, + ), + modifier = + Modifier + .align(Alignment.CenterStart) + .fillMaxHeight(), ) { Icon(Icons.Outlined.Close, null, tint = Color.White) } @@ -1266,10 +1257,10 @@ fun SettingScreen( stringResource(R.string.youtube_account), style = typo.titleMedium, modifier = - Modifier - .align(Alignment.Center) - .wrapContentHeight(align = Alignment.CenterVertically) - .wrapContentWidth(), + Modifier + .align(Alignment.Center) + .wrapContentHeight(align = Alignment.CenterVertically) + .wrapContentWidth(), ) } } @@ -1281,37 +1272,38 @@ fun SettingScreen( stringResource(R.string.no_account), style = typo.bodyMedium, textAlign = TextAlign.Center, - modifier = Modifier - .padding(12.dp) - .fillMaxWidth(), + modifier = + Modifier + .padding(12.dp) + .fillMaxWidth(), ) } } else { items(data) { Row( modifier = - Modifier - .padding(vertical = 8.dp) - .clickable { - viewModel.setUsedAccount(it) - }, + Modifier + .padding(vertical = 8.dp) + .clickable { + viewModel.setUsedAccount(it) + }, verticalAlignment = Alignment.CenterVertically, ) { Spacer(Modifier.width(24.dp)) AsyncImage( model = - ImageRequest - .Builder(LocalContext.current) - .data(it.thumbnailUrl) - .crossfade(550) - .build(), + ImageRequest + .Builder(LocalContext.current) + .data(it.thumbnailUrl) + .crossfade(550) + .build(), placeholder = painterResource(R.drawable.baseline_people_alt_24), error = painterResource(R.drawable.baseline_people_alt_24), contentDescription = it.name, modifier = - Modifier - .size(48.dp) - .clip(CircleShape), + Modifier + .size(48.dp) + .clip(CircleShape), ) Spacer(Modifier.width(12.dp)) Column(Modifier.weight(1f)) { @@ -1337,7 +1329,7 @@ fun SettingScreen( CenterLoadingBox( Modifier .fillMaxWidth() - .height(54.dp) + .height(54.dp), ) } } @@ -1359,10 +1351,10 @@ fun SettingScreen( title = context.getString(R.string.warning), message = context.getString(R.string.log_out_warning), confirm = - context.getString(R.string.log_out) to { - viewModel.logOutAllYouTube() - showYouTubeAccountDialog = false - }, + context.getString(R.string.log_out) to { + viewModel.logOutAllYouTube() + showYouTubeAccountDialog = false + }, dismiss = context.getString(R.string.cancel), ), ) @@ -1407,9 +1399,9 @@ fun SettingScreen( viewModel.setAlertData( alertState.copy( textField = - alertState.textField.copy( - value = it, - ), + alertState.textField.copy( + value = it, + ), ), ) }, @@ -1430,11 +1422,11 @@ fun SettingScreen( } }, modifier = - Modifier - .fillMaxWidth() - .padding( - vertical = 6.dp, - ), + Modifier + .fillMaxWidth() + .padding( + vertical = 6.dp, + ), ) } } @@ -1449,16 +1441,16 @@ fun SettingScreen( viewModel.setAlertData( alertState.copy( selectOne = - alertState.selectOne.copy( - listSelect = - alertState.selectOne.listSelect.toMutableList().map { - if (it == item) { - true to it.second - } else { - false to it.second - } - }, - ), + alertState.selectOne.copy( + listSelect = + alertState.selectOne.listSelect.toMutableList().map { + if (it == item) { + true to it.second + } else { + false to it.second + } + }, + ), ), ) } @@ -1467,8 +1459,7 @@ fun SettingScreen( .padding(vertical = 4.dp) .clickable { onSelect.invoke() - } - .fillMaxWidth(), + }.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { RadioButton( @@ -1491,16 +1482,16 @@ fun SettingScreen( viewModel.setAlertData( alertState.copy( multipleSelect = - alertState.multipleSelect.copy( - listSelect = - alertState.multipleSelect.listSelect.toMutableList().map { - if (it == item) { - !it.first to it.second - } else { - it - } - }, - ), + alertState.multipleSelect.copy( + listSelect = + alertState.multipleSelect.listSelect.toMutableList().map { + if (it == item) { + !it.first to it.second + } else { + it + } + }, + ), ), ) } @@ -1509,8 +1500,7 @@ fun SettingScreen( .padding(vertical = 4.dp) .clickable { onCheck.invoke() - } - .fillMaxWidth(), + }.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Checkbox( @@ -1533,14 +1523,14 @@ fun SettingScreen( viewModel.setAlertData(null) }, enabled = - if (alertState.textField?.verifyCodeBlock != null) { - alertState.textField.verifyCodeBlock - .invoke( - alertState.textField.value, - ).first - } else { - true - }, + if (alertState.textField?.verifyCodeBlock != null) { + alertState.textField.verifyCodeBlock + .invoke( + alertState.textField.value, + ).first + } else { + true + }, ) { Text(text = alertState.confirm.first) } @@ -1561,9 +1551,9 @@ fun SettingScreen( title = { Text( text = - stringResource( - R.string.settings, - ), + stringResource( + R.string.settings, + ), style = typo.titleMedium, ) }, diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt index 88872f14..63025878 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -78,7 +79,6 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow @@ -91,6 +91,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.ComposeView @@ -106,8 +107,6 @@ import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.media3.common.util.UnstableApi import androidx.navigation.NavController -import androidx.compose.foundation.gestures.detectHorizontalDragGestures -import androidx.compose.ui.input.pointer.pointerInput import coil3.compose.AsyncImage import coil3.request.CachePolicy import coil3.request.ImageRequest @@ -376,52 +375,52 @@ fun NowPlayingScreen( } } -Column( - Modifier - .verticalScroll(mainScrollState) - .pointerInput(Unit) { - var isSwipeHandled = false - detectHorizontalDragGestures( - onDragEnd = { isSwipeHandled = false } - ) { change, dragAmount -> - change.consume() - if (!isSwipeHandled) { - when { - // Swipe left (negative dragAmount) - dragAmount < -90 -> { - if (controllerState.isNextAvailable) { - sharedViewModel.onUIEvent(UIEvent.Next) - isSwipeHandled = true + Column( + Modifier + .verticalScroll(mainScrollState) + .pointerInput(Unit) { + var isSwipeHandled = false + detectHorizontalDragGestures( + onDragEnd = { isSwipeHandled = false }, + ) { change, dragAmount -> + change.consume() + if (!isSwipeHandled) { + when { + // Swipe left (negative dragAmount) + dragAmount < -90 -> { + if (controllerState.isNextAvailable) { + sharedViewModel.onUIEvent(UIEvent.Next) + isSwipeHandled = true + } } - } - // Swipe right (positive dragAmount) - dragAmount > 90 -> { - if (controllerState.isPreviousAvailable) { - sharedViewModel.onUIEvent(UIEvent.Previous) - isSwipeHandled = true + // Swipe right (positive dragAmount) + dragAmount > 90 -> { + if (controllerState.isPreviousAvailable) { + sharedViewModel.onUIEvent(UIEvent.Previous) + isSwipeHandled = true + } } } } } - } - } - .then( - if (showHideMiddleLayout) { - Modifier.background( - Brush.linearGradient( - colors = listOf( - startColor.value, - endColor.value, + }.then( + if (showHideMiddleLayout) { + Modifier.background( + Brush.linearGradient( + colors = + listOf( + startColor.value, + endColor.value, + ), + start = gradientOffset.start, + end = gradientOffset.end, ), - start = gradientOffset.start, - end = gradientOffset.end, - ), - ) - } else { - Modifier.background(md_theme_dark_background) - }, - ) -) { + ) + } else { + Modifier.background(md_theme_dark_background) + }, + ), + ) { Box(modifier = Modifier.fillMaxWidth()) { // Canvas Layout Box( @@ -1392,6 +1391,7 @@ Column( text = when (screenDataState.lyricsData?.lyricsProvider) { LyricsProvider.MUSIXMATCH -> stringResource(id = R.string.lyrics_provider) + LyricsProvider.LRCLIB -> stringResource(id = R.string.lyrics_provider_lrc) LyricsProvider.YOUTUBE -> stringResource(id = R.string.lyrics_provider_youtube) LyricsProvider.SPOTIFY -> stringResource(id = R.string.spotify_lyrics_provider) LyricsProvider.OFFLINE -> stringResource(id = R.string.offline_mode) diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/NowPlayingBottomSheetViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/NowPlayingBottomSheetViewModel.kt index d0c43a26..4e7687c6 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/NowPlayingBottomSheetViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/NowPlayingBottomSheetViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import androidx.media3.common.util.UnstableApi import com.maxrave.simpmusic.R import com.maxrave.simpmusic.common.DownloadState +import com.maxrave.simpmusic.data.dataStore.DataStoreManager.Settings.LRCLIB import com.maxrave.simpmusic.data.dataStore.DataStoreManager.Settings.MUSIXMATCH import com.maxrave.simpmusic.data.dataStore.DataStoreManager.Settings.YOUTUBE import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity @@ -27,11 +28,9 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch - import org.koin.core.component.inject @UnstableApi - class NowPlayingBottomSheetViewModel( private val application: Application, ) : BaseViewModel(application) { @@ -81,6 +80,9 @@ class NowPlayingBottomSheetViewModel( YOUTUBE -> { _uiState.update { it.copy(mainLyricsProvider = YOUTUBE) } } + LRCLIB -> { + _uiState.update { it.copy(mainLyricsProvider = LRCLIB) } + } else -> { log("Unknown lyrics provider", Log.ERROR) } @@ -216,7 +218,7 @@ class NowPlayingBottomSheetViewModel( makeToast(getString(R.string.added_to_queue)) } is NowPlayingBottomSheetUIEvent.ChangeLyricsProvider -> { - if (listOf(MUSIXMATCH, YOUTUBE).contains(ev.lyricsProvider)) { + if (listOf(MUSIXMATCH, YOUTUBE, LRCLIB).contains(ev.lyricsProvider)) { dataStoreManager.setLyricsProvider(ev.lyricsProvider) } else { return@launch diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt index 84cdac00..9ecf3768 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt @@ -16,7 +16,6 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager -import com.maxrave.spotify.model.response.spotify.CanvasResponse import com.maxrave.kotlinytmusicscraper.models.simpmusic.GithubResponse import com.maxrave.simpmusic.R import com.maxrave.simpmusic.common.Config.ALBUM_CLICK @@ -63,6 +62,7 @@ import com.maxrave.simpmusic.service.test.download.DownloadUtils import com.maxrave.simpmusic.service.test.notification.NotifyWork import com.maxrave.simpmusic.utils.Resource import com.maxrave.simpmusic.viewModel.base.BaseViewModel +import com.maxrave.spotify.model.response.spotify.CanvasResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -1216,21 +1216,21 @@ class SharedViewModel( viewModelScope.launch { val videoId = song.videoId Log.w(tag, "Get Lyrics From Format for $videoId") + val artist = + if (song.artistName?.firstOrNull() != null && + song.artistName + .firstOrNull() + ?.contains("Various Artists") == false + ) { + song.artistName.firstOrNull() + } else { + simpleMediaServiceHandler.nowPlaying + .first() + ?.mediaMetadata + ?.artist + ?: "" + } if (dataStoreManager.lyricsProvider.first() == DataStoreManager.MUSIXMATCH) { - val artist = - if (song.artistName?.firstOrNull() != null && - song.artistName - .firstOrNull() - ?.contains("Various Artists") == false - ) { - song.artistName.firstOrNull() - } else { - simpleMediaServiceHandler.nowPlaying - .first() - ?.mediaMetadata - ?.artist - ?: "" - } mainRepository .getLyricsData( (artist ?: "").toString(), @@ -1306,6 +1306,37 @@ class SharedViewModel( } } } + } else if (dataStoreManager.lyricsProvider.first() == DataStoreManager.LRCLIB) { + mainRepository + .getLrclibLyricsData( + (artist ?: "").toString(), + song.title, + duration, + ).collectLatest { res -> + when (res) { + is Resource.Success -> { + Log.d(tag, "Get Lyrics Data Success") + updateLyrics( + videoId, + res.data, + false, + LyricsProvider.LRCLIB, + ) + insertLyrics( + res.data?.toLyricsEntity( + videoId, + ) ?: return@collectLatest, + ) + } + is Resource.Error -> { + getSavedLyrics( + song.toTrack().copy( + durationSeconds = duration, + ), + ) + } + } + } } else if (dataStoreManager.lyricsProvider.first() == DataStoreManager.YOUTUBE) { mainRepository.getYouTubeCaption(videoId).cancellable().collect { response -> _lyrics.value = response @@ -1635,6 +1666,7 @@ enum class LyricsProvider { MUSIXMATCH, YOUTUBE, SPOTIFY, + LRCLIB, OFFLINE, } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 941fc78d..8fc3cda1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -375,4 +375,6 @@ Please enter your Proxy host Please enter your Proxy port Five seconds + LRCLIB + Lyrics provided by LRCLIB \ No newline at end of file diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt index ed89f5d0..71f51e0a 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt @@ -1,14 +1,12 @@ package com.maxrave.kotlinytmusicscraper import android.util.Log -import com.google.gson.Gson import com.maxrave.kotlinytmusicscraper.models.AccountInfo import com.maxrave.kotlinytmusicscraper.models.AlbumItem import com.maxrave.kotlinytmusicscraper.models.Artist import com.maxrave.kotlinytmusicscraper.models.ArtistItem import com.maxrave.kotlinytmusicscraper.models.BrowseEndpoint import com.maxrave.kotlinytmusicscraper.models.GridRenderer -import com.maxrave.kotlinytmusicscraper.models.LrclibObject import com.maxrave.kotlinytmusicscraper.models.MediaType import com.maxrave.kotlinytmusicscraper.models.MusicCarouselShelfRenderer import com.maxrave.kotlinytmusicscraper.models.MusicShelfRenderer @@ -21,17 +19,10 @@ import com.maxrave.kotlinytmusicscraper.models.SongItem import com.maxrave.kotlinytmusicscraper.models.VideoItem import com.maxrave.kotlinytmusicscraper.models.WatchEndpoint import com.maxrave.kotlinytmusicscraper.models.YTItemType -import com.maxrave.kotlinytmusicscraper.models.YouTubeClient.Companion.ANDROID_MUSIC import com.maxrave.kotlinytmusicscraper.models.YouTubeClient.Companion.WEB import com.maxrave.kotlinytmusicscraper.models.YouTubeClient.Companion.WEB_REMIX import com.maxrave.kotlinytmusicscraper.models.YouTubeLocale import com.maxrave.kotlinytmusicscraper.models.getContinuation -import com.maxrave.kotlinytmusicscraper.models.musixmatch.MusixmatchCredential -import com.maxrave.kotlinytmusicscraper.models.musixmatch.MusixmatchLyricsReponse -import com.maxrave.kotlinytmusicscraper.models.musixmatch.MusixmatchLyricsResponseByQ -import com.maxrave.kotlinytmusicscraper.models.musixmatch.MusixmatchTranslationLyricsResponse -import com.maxrave.kotlinytmusicscraper.models.musixmatch.SearchMusixmatchResponse -import com.maxrave.kotlinytmusicscraper.models.musixmatch.UserTokenResponse import com.maxrave.kotlinytmusicscraper.models.oddElements import com.maxrave.kotlinytmusicscraper.models.response.AccountMenuResponse import com.maxrave.kotlinytmusicscraper.models.response.AddItemYouTubePlaylistResponse @@ -73,8 +64,6 @@ import com.maxrave.kotlinytmusicscraper.parser.getPlaylistContinuation import com.maxrave.kotlinytmusicscraper.parser.getReloadParams import com.maxrave.kotlinytmusicscraper.parser.getSuggestionSongItems import com.maxrave.kotlinytmusicscraper.parser.hasReloadParams -import com.maxrave.kotlinytmusicscraper.parser.parseMusixmatchLyrics -import com.maxrave.kotlinytmusicscraper.parser.parseUnsyncedLyrics import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlHandler import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlParser import io.ktor.client.call.body @@ -89,7 +78,6 @@ import kotlinx.serialization.json.jsonPrimitive import okhttp3.Interceptor import org.json.JSONArray import java.io.File -import kotlin.math.abs import kotlin.random.Random private fun List.toListFormat(): List { @@ -177,34 +165,24 @@ class YouTube { ytMusic.cookie = value } - var musixMatchCookie: String? - get() = ytMusic.musixMatchCookie - set(value) { - ytMusic.musixMatchCookie = value - } - - var musixmatchUserToken: String? - get() = ytMusic.musixmatchUserToken - set(value) { - ytMusic.musixmatchUserToken = value - } - /** * Json deserializer for PO token request */ - private val poTokenJsonDeserializer = Json { - ignoreUnknownKeys = true - encodeDefaults = true - coerceInputValues = true - useArrayPolymorphism = true - } + private val poTokenJsonDeserializer = + Json { + ignoreUnknownKeys = true + encodeDefaults = true + coerceInputValues = true + useArrayPolymorphism = true + } - private fun String.getPoToken(): String? { - return this.replace("[", "").replace("]", "") + private fun String.getPoToken(): String? = + this + .replace("[", "") + .replace("]", "") .split(",") .findLast { it.contains("\"") } ?.replace("\"", "") - } private var poTokenObject: Pair = Pair(null, 0) @@ -223,14 +201,8 @@ class YouTube { host: String, port: Int, ) { - val verifiedHost = - if (!host.contains("http")) { - "http://$host" - } else { - host - } runCatching { - if (isHttp) ProxyBuilder.http("$verifiedHost:$port") else ProxyBuilder.socks(verifiedHost, port) + if (isHttp) ProxyBuilder.http("$host:$port") else ProxyBuilder.socks(host, port) }.onSuccess { ytMusic.proxy = it }.onFailure { @@ -834,11 +806,6 @@ class YouTube { ytMusic.browse(WEB_REMIX, browseId, params, continuation, country, setLogin).body() } - fun fromArrayListNull(list: List?): String? { - val gson = Gson() - return gson.toJson(list) - } - /** * Get the related data of a song from YouTube Music * @param videoId the videoId of song @@ -849,162 +816,6 @@ class YouTube { ytMusic.nextCustom(WEB_REMIX, videoId).body() } - suspend fun getMusixmatchUserToken() = - runCatching { - ytMusic.getMusixmatchUserToken().body() - } - - suspend fun postMusixmatchCredentials( - email: String, - password: String, - userToken: String, - ) = runCatching { - val request = ytMusic.postMusixmatchPostCredentials(email, password, userToken) - val response = request.body() - if (response.message.body - .get(0) - .credential.error == null && - response.message.body - .get(0) - .credential.account != null - ) { - val setCookies = request.headers.getAll("Set-Cookie") -// Log.w("postMusixmatchCredentials", setCookies.toString()) - if (!setCookies.isNullOrEmpty()) { - fromArrayListNull(setCookies)?.let { - musixMatchCookie = it - } - } - } -// Log.w("postMusixmatchCredentials cookie", musixMatchCookie.toString()) -// Log.w("postMusixmatchCredentials", response.toString()) - return@runCatching response - } - - fun getMusixmatchCookie() = musixMatchCookie - - suspend fun searchMusixmatchTrackId( - query: String, - userToken: String, - ) = runCatching { -// val result = ytMusic.searchMusixmatchTrackId(query, userToken) -// Log.w("Lyrics", "Search Track $query: " + result.bodyAsText()) -// Log.w("Lyrics", "Search Track $query: " + result.body().message.body.macro_result_list) -// return@runCatching result.body(), - ytMusic.searchMusixmatchTrackId(query, userToken).body() - } - - suspend fun fixSearchMusixmatch( - q_artist: String, - q_track: String, - q_duration: String, - userToken: String, - ) = runCatching { - val rs = ytMusic.fixSearchMusixmatch(q_artist, q_track, q_duration, userToken).body() - Log.w("Search Result", rs.toString()) - return@runCatching rs - } - - suspend fun getMusixmatchLyrics( - trackId: String, - userToken: String, - ) = runCatching { - val response = ytMusic.getMusixmatchLyrics(trackId, userToken).body() - if (response.message.body.subtitle != null) { - return@runCatching parseMusixmatchLyrics(response.message.body.subtitle.subtitle_body) - } else { - val unsyncedResponse = ytMusic.getMusixmatchUnsyncedLyrics(trackId, userToken).body() - if (unsyncedResponse.message.body.lyrics != null && unsyncedResponse.message.body.lyrics.lyrics_body != "") { - return@runCatching parseUnsyncedLyrics(unsyncedResponse.message.body.lyrics.lyrics_body) - } else { - null - } - } - } - - suspend fun getMusixmatchLyricsByQ( - track: SearchMusixmatchResponse.Message.Body.Track.TrackX, - userToken: String, - ) = runCatching { - val response = ytMusic.getMusixmatchLyricsByQ(track, userToken).body() - - if (!response.message.body.subtitle_list - .isNullOrEmpty() && - response.message.body.subtitle_list - .firstOrNull() - ?.subtitle - ?.subtitle_body != null - ) { - return@runCatching parseMusixmatchLyrics( - response.message.body.subtitle_list - .firstOrNull() - ?.subtitle - ?.subtitle_body!!, - ) - } else { - val unsyncedResponse = ytMusic.getMusixmatchUnsyncedLyrics(track.track_id.toString(), userToken).body() - if (unsyncedResponse.message.body.lyrics != null && unsyncedResponse.message.body.lyrics.lyrics_body != "") { - return@runCatching parseUnsyncedLyrics(unsyncedResponse.message.body.lyrics.lyrics_body) - } else { - null - } - } - } - - suspend fun getMusixmatchTranslateLyrics( - trackId: String, - userToken: String, - language: String, - ) = runCatching { - ytMusic - .getMusixmatchTranslateLyrics(trackId, userToken, language) - .body() - } - - suspend fun getYouTubeCaption(videoId: String) = - runCatching { - val ytWeb = ytMusic.player(WEB, videoId, null, null).body() - ytMusic - .getYouTubeCaption( - ytWeb.captions?.playerCaptionsTracklistRenderer?.captionTracks?.firstOrNull()?.baseUrl?.replace( - "&fmt=srv3", - "", - ) ?: "", - ).body() - } - - suspend fun getLrclibLyrics( - q_track: String, - q_artist: String, - duration: Int?, - ) = runCatching { - val rs = - ytMusic - .searchLrclibLyrics( - q_track = q_track, - q_artist = q_artist, - ).body>() - val lrclibObject: LrclibObject? = - if (duration != null) { - rs.find { abs(it.duration.toInt() - duration) <= 10 } - } else { - rs.firstOrNull() - } - if (lrclibObject != null) { - val syncedLyrics = lrclibObject.syncedLyrics - val plainLyrics = lrclibObject.plainLyrics - if (syncedLyrics != null) { - parseMusixmatchLyrics(syncedLyrics) - } else if (plainLyrics != null) { - parseUnsyncedLyrics(plainLyrics) - } else { - null - } - } else { - null - } - } - /** * Get the suggest query from Google * @param query the search query @@ -1277,7 +1088,10 @@ class YouTube { // Get author thumbnails, subscribers, description, like count } - private suspend fun getVisitorData(videoId: String, playlistId: String?): Triple { + private suspend fun getVisitorData( + videoId: String, + playlistId: String?, + ): Triple { try { val pId = if (playlistId?.startsWith("VL") == true) playlistId.removeRange(0..1) else playlistId val ghostRequest = ytMusic.ghostRequest(videoId, pId) @@ -1315,10 +1129,12 @@ class YouTube { val ytInitialData = poTokenJsonDeserializer.decodeFromString(data) val ytInitialPlayerResponse = poTokenJsonDeserializer.decodeFromString(response) val playbackTracking = ytInitialPlayerResponse.playbackTracking - val loggedIn = ytInitialData.responseContext.serviceTrackingParams - ?.find { it.service == "GFEEDBACK" } - ?.params - ?.find { it.key == "logged_in" }?.value == "1" + val loggedIn = + ytInitialData.responseContext.serviceTrackingParams + ?.find { it.service == "GFEEDBACK" } + ?.params + ?.find { it.key == "logged_in" } + ?.value == "1" println("Logged In $loggedIn") val visitorData = ytInitialPlayerResponse.responseContext.serviceTrackingParams @@ -1357,20 +1173,24 @@ class YouTube { ] }.joinToString("") val now = System.currentTimeMillis() - val poToken = if (now < poTokenObject.second) { - println("Use saved PoToken") - poTokenObject.first - } - else - ytMusic.createPoTokenChallenge().bodyAsText().let { challenge -> - val listChallenge = poTokenJsonDeserializer.decodeFromString>(challenge) - listChallenge.filterIsInstance().firstOrNull() - }?.let { poTokenChallenge -> - ytMusic.generatePoToken(poTokenChallenge).bodyAsText().getPoToken().also { poToken -> - if (poToken != null) { - poTokenObject = Pair(poToken, now + 21600000) + val poToken = + if (now < poTokenObject.second) { + println("Use saved PoToken") + poTokenObject.first + } else { + ytMusic + .createPoTokenChallenge() + .bodyAsText() + .let { challenge -> + val listChallenge = poTokenJsonDeserializer.decodeFromString>(challenge) + listChallenge.filterIsInstance().firstOrNull() + }?.let { poTokenChallenge -> + ytMusic.generatePoToken(poTokenChallenge).bodyAsText().getPoToken().also { poToken -> + if (poToken != null) { + poTokenObject = Pair(poToken, now + 21600000) + } + } } - } } println("PoToken $poToken") val playerResponse = ytMusic.noLogInPlayer(videoId, tempCookie, visitorData, poToken ?: "").body() @@ -1432,7 +1252,7 @@ class YouTube { ), videoDetails = playerResponse.videoDetails?.copy(), playbackTracking = playbackTracking ?: playerResponse.playbackTracking, - ), + ), thumbnails, ) } catch (e: Exception) { @@ -1807,6 +1627,18 @@ class YouTube { ) } + suspend fun getYouTubeCaption(videoId: String) = + runCatching { + val ytWeb = ytMusic.player(WEB, videoId, null, null).body() + ytMusic + .getYouTubeCaption( + ytWeb.captions?.playerCaptionsTracklistRenderer?.captionTracks?.firstOrNull()?.baseUrl?.replace( + "&fmt=srv3", + "", + ) ?: "", + ).body() + } + suspend fun scrapeYouTube(videoId: String) = runCatching { ytMusic.scrapeYouTube(videoId).body() diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt index 07ebf546..74bed027 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt @@ -1,7 +1,5 @@ package com.maxrave.kotlinytmusicscraper -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import com.maxrave.kotlinytmusicscraper.encoder.brotli import com.maxrave.kotlinytmusicscraper.models.Context import com.maxrave.kotlinytmusicscraper.models.WatchEndpoint @@ -19,24 +17,16 @@ import com.maxrave.kotlinytmusicscraper.models.body.FormData import com.maxrave.kotlinytmusicscraper.models.body.GetQueueBody import com.maxrave.kotlinytmusicscraper.models.body.GetSearchSuggestionsBody import com.maxrave.kotlinytmusicscraper.models.body.LikeBody -import com.maxrave.kotlinytmusicscraper.models.body.MusixmatchCredentialsBody import com.maxrave.kotlinytmusicscraper.models.body.NextBody import com.maxrave.kotlinytmusicscraper.models.body.PlayerBody import com.maxrave.kotlinytmusicscraper.models.body.SearchBody -import com.maxrave.kotlinytmusicscraper.models.musixmatch.SearchMusixmatchResponse -import com.maxrave.kotlinytmusicscraper.utils.CustomRedirectConfig import com.maxrave.kotlinytmusicscraper.utils.parseCookieString import com.maxrave.kotlinytmusicscraper.utils.sha1 import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.HttpSend -import io.ktor.client.plugins.cache.HttpCache import io.ktor.client.plugins.compression.ContentEncoding import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.cookies.AcceptAllCookiesStorage -import io.ktor.client.plugins.cookies.HttpCookies import io.ktor.client.plugins.defaultRequest -import io.ktor.client.request.HttpRequest import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.accept import io.ktor.client.request.get @@ -46,52 +36,40 @@ import io.ktor.client.request.parameter import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.ContentType -import io.ktor.http.Headers import io.ktor.http.HttpHeaders import io.ktor.http.contentType -import io.ktor.http.parameters import io.ktor.http.userAgent -import io.ktor.serialization.kotlinx.KotlinxSerializationConverter import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.protobuf.protobuf import io.ktor.serialization.kotlinx.xml.xml import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json -import kotlinx.serialization.protobuf.ProtoBuf import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.serialization.XML -import okhttp3.Challenge import okhttp3.Interceptor import java.io.File -import java.lang.reflect.Type import java.net.Proxy import java.util.Locale class Ytmusic { private var httpClient = createClient() - private var musixmatchClient = createMusixmatchClient() var cacheControlInterceptor: Interceptor? = null set(value) { field = value httpClient.close() httpClient = createClient() - musixmatchClient.close() - musixmatchClient = createMusixmatchClient() } var forceCacheInterceptor: Interceptor? = null set(value) { field = value httpClient.close() httpClient = createClient() - musixmatchClient.close() - musixmatchClient = createMusixmatchClient() } var cachePath: File? = null set(value) { field = value httpClient = createClient() - musixmatchClient = createMusixmatchClient() } var locale = @@ -108,90 +86,11 @@ class Ytmusic { } private var cookieMap = emptyMap() - var musixMatchCookie: String? = null - set(value) { - field = value - } - - var musixmatchUserToken: String? = null - var proxy: Proxy? = null set(value) { field = value httpClient.close() - musixmatchClient.close() httpClient = createClient() - musixmatchClient = createMusixmatchClient() - } - - @OptIn(ExperimentalSerializationApi::class) - private fun createMusixmatchClient() = - HttpClient(OkHttp) { - expectSuccess = true - followRedirects = false - if (cachePath != null) { - engine { - config { - cache( - okhttp3.Cache(cachePath!!, 50L * 1024 * 1024), - ) - } - if (cacheControlInterceptor != null) { - addNetworkInterceptor(cacheControlInterceptor!!) - } - if (forceCacheInterceptor != null) { - addInterceptor(forceCacheInterceptor!!) - } - } - } - install(HttpCache) - install(HttpSend) { - maxSendCount = 100 - } - install(HttpCookies) { - storage = AcceptAllCookiesStorage() - } - install(CustomRedirectConfig) { - checkHttpMethod = false - allowHttpsDowngrade = true - defaultHostUrl = "https://apic-desktop.musixmatch.com" - } - install(ContentNegotiation) { - register( - ContentType.Text.Plain, - KotlinxSerializationConverter( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - explicitNulls = false - encodeDefaults = true - }, - ), - ) - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - explicitNulls = false - encodeDefaults = true - }, - ) - } - install(ContentEncoding) { - brotli(1.0F) - gzip(0.9F) - deflate(0.8F) - } - defaultRequest { - url("https://apic-desktop.musixmatch.com/ws/1.1/") - } - if (proxy != null) { - engine { - proxy = this@Ytmusic.proxy - } - } } @OptIn(ExperimentalSerializationApi::class) @@ -445,260 +344,6 @@ class Ytmusic { parameter("q", query) } - private fun fromString(value: String?): List? { - val listType: Type = object : TypeToken?>() {}.type - return Gson().fromJson(value, listType) - } - - suspend fun getMusixmatchUserToken() = - musixmatchClient.get("token.get?app_id=android-player-v1.0") { - contentType(ContentType.Application.Json) - headers { - header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") - header(HttpHeaders.Accept, "*/*") - header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") - header(HttpHeaders.Connection, "keep-alive") - if (musixMatchCookie != null) { - val listCookies = fromString(musixMatchCookie) - if (!listCookies.isNullOrEmpty()) { - val appendCookie = - listCookies.joinToString(separator = "; ") { eachCookie -> - eachCookie - } - header(HttpHeaders.Cookie, appendCookie) - } - } - } - } - - suspend fun postMusixmatchPostCredentials( - email: String, - password: String, - userToken: String, - ) = musixmatchClient.post("https://apic.musixmatch.com/ws/1.1/credential.post") { - contentType(ContentType.Application.Json) - headers { - header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") - header(HttpHeaders.Accept, "*/*") - header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") - header(HttpHeaders.Connection, "keep-alive") - } - parameter("app_id", "android-player-v1.0") - parameter("usertoken", userToken) - parameter("format", "json") - setBody( - MusixmatchCredentialsBody( - listOf( - MusixmatchCredentialsBody.Credential( - MusixmatchCredentialsBody.Credential.CredentialData( - email = email, - password = password, - ), - ), - ), - ), - ) - } - - suspend fun searchMusixmatchTrackId( - q: String, - userToken: String, - ) = musixmatchClient.get("macro.search?app_id=android-player-v1.0&page_size=5&page=1&s_track_rating=desc&quorum_factor=1.0") { - contentType(ContentType.Application.Json) - headers { - header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") - header(HttpHeaders.Accept, "*/*") - header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") - header(HttpHeaders.Connection, "keep-alive") - if (musixMatchCookie != null) { - val listCookies = fromString(musixMatchCookie) - if (!listCookies.isNullOrEmpty()) { - val appendCookie = - listCookies.joinToString(separator = "; ") { eachCookie -> - eachCookie - } - header(HttpHeaders.Cookie, appendCookie) - } - } - } - - parameter("q", q) - parameter("usertoken", userToken) - } - - suspend fun fixSearchMusixmatch( - q_artist: String, - q_track: String, - q_duration: String, - userToken: String, - ) = musixmatchClient.get( - "matcher.track.get?tags=scrobbling%2Cnotifications&subtitle_format=dfxp&page_size=5&questions_id_list=track_esync_action%2Ctrack_sync_action%2Ctrack_translation_action%2Clyrics_ai_mood_analysis_v3&optional_calls=track.richsync%2Ccrowd.track.actions&app_id=android-player-v1.0&country=us&part=lyrics_crowd%2Cuser%2Clyrics_vote%2Clyrics_poll%2Ctrack_lyrics_translation_status%2Clyrics_verified_by%2Clabels%2Ctrack_structure%2Ctrack_performer_tagging%2C&scrobbling_package=com.google.android.apps.youtube.music&language_iso_code=1&format=json", - ) { - contentType(ContentType.Application.Json) - parameter("usertoken", userToken) -// q_artist=culture+code,+james+roche+&+karra&q_track=make+me+move+(james+roche+remix) - parameter("q_artist", q_artist) - parameter("q_track", q_track) - parameter("q_duration", q_duration) - headers { - header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") - header(HttpHeaders.Accept, "*/*") - header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") - header(HttpHeaders.Connection, "keep-alive") - if (musixMatchCookie != null) { - val listCookies = fromString(musixMatchCookie) - if (!listCookies.isNullOrEmpty()) { - val appendCookie = - listCookies.joinToString(separator = "; ") { eachCookie -> - eachCookie - } - header(HttpHeaders.Cookie, appendCookie) - } - } - } - } - - suspend fun getMusixmatchLyrics( - trackId: String, - userToken: String, - ) = musixmatchClient.get("track.subtitle.get?app_id=android-player-v1.0&subtitle_format=id3") { - contentType(ContentType.Application.Json) - headers { - header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") - header(HttpHeaders.Accept, "*/*") - header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") - header(HttpHeaders.Connection, "keep-alive") - if (musixMatchCookie != null) { - val listCookies = fromString(musixMatchCookie) - if (!listCookies.isNullOrEmpty()) { - val appendCookie = - listCookies.joinToString(separator = "; ") { eachCookie -> - eachCookie - } - header(HttpHeaders.Cookie, appendCookie) - } - } - } - - parameter("usertoken", userToken) - parameter("track_id", trackId) - } - - suspend fun getMusixmatchLyricsByQ( - track: SearchMusixmatchResponse.Message.Body.Track.TrackX, - userToken: String, - ) = musixmatchClient.get("https://apic.musixmatch.com/ws/1.1/track.subtitles.get") { - contentType(ContentType.Application.Json) - headers { - header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") - header(HttpHeaders.Accept, "*/*") - header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") - header(HttpHeaders.Connection, "keep-alive") - if (musixMatchCookie != null) { - val listCookies = fromString(musixMatchCookie) - if (!listCookies.isNullOrEmpty()) { - val appendCookie = - listCookies.joinToString(separator = "; ") { eachCookie -> - eachCookie - } - header(HttpHeaders.Cookie, appendCookie) - } - } - } - - parameter("usertoken", userToken) - parameter("track_id", track.track_id) - parameter("f_subtitle_length_max_deviation", "1") - parameter("page_size", "1") - parameter("questions_id_list", "track_esync_action%2Ctrack_sync_action%2Ctrack_translation_action%2Clyrics_ai_mood_analysis_v3") - parameter("optional_calls", "track.richsync%2Ccrowd.track.actions") - parameter("q_artist", track.artist_name) - parameter("q_track", track.track_name) - parameter("app_id", "android-player-v1.0") - parameter( - "part", - "lyrics_crowd%2Cuser%2Clyrics_vote%2Clyrics_poll%2Ctrack_lyrics_translation_status%2Clyrics_verified_by%2Clabels%2Ctrack_structure%2Ctrack_performer_tagging%2C", - ) - parameter("language_iso_code", "1") - parameter("format", "json") - parameter("q_duration", track.track_length) - } - - suspend fun getMusixmatchUnsyncedLyrics( - trackId: String, - userToken: String, - ) = musixmatchClient.get("track.lyrics.get?app_id=android-player-v1.0&subtitle_format=id3") { - contentType(ContentType.Application.Json) - headers { - header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") - header(HttpHeaders.Accept, "*/*") - header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") - header(HttpHeaders.Connection, "keep-alive") - if (musixMatchCookie != null) { - val listCookies = fromString(musixMatchCookie) - if (!listCookies.isNullOrEmpty()) { - val appendCookie = - listCookies.joinToString(separator = "; ") { eachCookie -> - eachCookie - } - header(HttpHeaders.Cookie, appendCookie) - } - } - } - parameter("usertoken", userToken) - parameter("track_id", trackId) - } - - suspend fun searchLrclibLyrics( - q_track: String, - q_artist: String, - ) = httpClient.get("https://lrclib.net/api/search") { - contentType(ContentType.Application.Json) - headers { - header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") - header(HttpHeaders.Accept, "*/*") - header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") - header(HttpHeaders.Connection, "keep-alive") - } - parameter("track_name", q_track) - parameter("artist_name", q_artist) - } - - suspend fun getMusixmatchTranslateLyrics( - trackId: String, - userToken: String, - language: String, - ) = musixmatchClient.get("https://apic.musixmatch.com/ws/1.1/crowd.track.translations.get") { - contentType(ContentType.Application.Json) - headers { - header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") - header(HttpHeaders.Accept, "*/*") - header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") - header(HttpHeaders.Connection, "keep-alive") - if (musixMatchCookie != null) { - val listCookies = fromString(musixMatchCookie) - if (!listCookies.isNullOrEmpty()) { - val appendCookie = - listCookies.joinToString(separator = "; ") { eachCookie -> - eachCookie - } - header(HttpHeaders.Cookie, appendCookie) - } - } - } - parameters { - parameter("translation_fields_set", "minimal") - parameter("track_id", trackId) - parameter("selected_language", language) - parameter("comment_format", "text") - parameter("part", "user") - parameter("format", "json") - parameter("usertoken", userToken) - parameter("app_id", "android-player-v1.0") - parameter("tags", "playing") - } - } - suspend fun getYouTubeCaption(url: String) = httpClient.get(url) { contentType(ContentType.Text.Xml) diff --git a/lyricsProviders/.gitignore b/lyricsProviders/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/lyricsProviders/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/lyricsProviders/build.gradle.kts b/lyricsProviders/build.gradle.kts new file mode 100644 index 00000000..14f7889b --- /dev/null +++ b/lyricsProviders/build.gradle.kts @@ -0,0 +1,56 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + kotlin("plugin.serialization") + +} + +android { + namespace = "com.maxrave.lyricsproviders" + compileSdk = 35 + + defaultConfig { + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + implementation(libs.core.ktx) + implementation(libs.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.espresso.core) + + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.encoding) + implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.ktor.client.content.negotiation) + + implementation(libs.brotli.dec) + + implementation(libs.kotlin.reflect) +} \ No newline at end of file diff --git a/lyricsProviders/consumer-rules.pro b/lyricsProviders/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/lyricsProviders/proguard-rules.pro b/lyricsProviders/proguard-rules.pro new file mode 100644 index 00000000..3542fe16 --- /dev/null +++ b/lyricsProviders/proguard-rules.pro @@ -0,0 +1,38 @@ +-keep class kotlinx.coroutines.CoroutineExceptionHandler +-keep class kotlinx.coroutines.internal.MainDispatcherFactory +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} + +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} + +# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault + +# Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes +# See also https://github.com/Kotlin/kotlinx.serialization/issues/1900 +-dontnote kotlinx.serialization.** + +# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes. +# If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning. +# However, since in this case they will not be used, we can disable these warnings +-dontwarn kotlinx.serialization.internal.ClassValueReferences +-dontwarn org.slf4j.impl.StaticLoggerBinder \ No newline at end of file diff --git a/lyricsProviders/src/androidTest/java/com/maxrave/lyricsproviders/ExampleInstrumentedTest.kt b/lyricsProviders/src/androidTest/java/com/maxrave/lyricsproviders/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..d8725ff4 --- /dev/null +++ b/lyricsProviders/src/androidTest/java/com/maxrave/lyricsproviders/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.maxrave.lyricsproviders + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.maxrave.lyricsproviders.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/lyricsProviders/src/main/AndroidManifest.xml b/lyricsProviders/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/lyricsProviders/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/LyricsClient.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/LyricsClient.kt new file mode 100644 index 00000000..64d76394 --- /dev/null +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/LyricsClient.kt @@ -0,0 +1,212 @@ +package com.maxrave.lyricsproviders + +import android.content.Context +import android.util.Log +import com.maxrave.lyricsproviders.models.response.LrclibObject +import com.maxrave.lyricsproviders.models.response.MusixmatchCredential +import com.maxrave.lyricsproviders.models.response.MusixmatchLyricsReponse +import com.maxrave.lyricsproviders.models.response.MusixmatchLyricsResponseByQ +import com.maxrave.lyricsproviders.models.response.MusixmatchTranslationLyricsResponse +import com.maxrave.lyricsproviders.models.response.SearchMusixmatchResponse +import com.maxrave.lyricsproviders.models.response.UserTokenResponse +import com.maxrave.lyricsproviders.parser.parseMusixmatchLyrics +import com.maxrave.lyricsproviders.parser.parseUnsyncedLyrics +import com.maxrave.lyricsproviders.utils.fromArrayListNull +import io.ktor.client.call.body +import io.ktor.client.engine.ProxyBuilder +import io.ktor.client.engine.http +import kotlinx.serialization.json.Json +import kotlin.math.abs + +class LyricsClient( + private val context: Context, +) { + private val commonJson = commonJson() + private val lyricsProvider = LyricsProviders(context, commonJson) + + private fun commonJson() = + Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + explicitNulls = false + encodeDefaults = true + } + + var musixmatchCookie: String? + get() = lyricsProvider.musixmatchCookie + set(value) { + lyricsProvider.musixmatchCookie = value + } + + var musixmatchUserToken: String? + get() = lyricsProvider.musixmatchUserToken + set(value) { + lyricsProvider.musixmatchUserToken = value + } + + fun removeProxy() { + lyricsProvider.proxy = null + } + + /** + * Set the proxy for client + */ + fun setProxy( + isHttp: Boolean, + host: String, + port: Int, + ) { + runCatching { + if (isHttp) ProxyBuilder.http("$host:$port") else ProxyBuilder.socks(host, port) + }.onSuccess { + lyricsProvider.proxy = it + }.onFailure { + it.printStackTrace() + } + } + + suspend fun getMusixmatchUserToken() = + runCatching { + lyricsProvider.getMusixmatchUserToken().body() + } + + suspend fun postMusixmatchCredentials( + email: String, + password: String, + userToken: String, + ) = runCatching { + val request = lyricsProvider.postMusixmatchPostCredentials(email, password, userToken) + val response = request.body() + if (response.message.body + .get(0) + .credential.error == null && + response.message.body + .get(0) + .credential.account != null + ) { + val setCookies = request.headers.getAll("Set-Cookie") +// Log.w("postMusixmatchCredentials", setCookies.toString()) + if (!setCookies.isNullOrEmpty()) { + fromArrayListNull(setCookies, commonJson)?.let { + musixmatchCookie = it + } + } + } +// Log.w("postMusixmatchCredentials cookie", musixmatchCookie.toString()) +// Log.w("postMusixmatchCredentials", response.toString()) + return@runCatching response + } + + fun getmusixmatchCookie() = musixmatchCookie + + suspend fun searchMusixmatchTrackId( + query: String, + userToken: String, + ) = runCatching { +// val result = lyricsProvider.searchMusixmatchTrackId(query, userToken) +// Log.w("Lyrics", "Search Track $query: " + result.bodyAsText()) +// Log.w("Lyrics", "Search Track $query: " + result.body().message.body.macro_result_list) +// return@runCatching result.body(), + lyricsProvider.searchMusixmatchTrackId(query, userToken).body() + } + + suspend fun fixSearchMusixmatch( + q_artist: String, + q_track: String, + q_duration: String, + userToken: String, + ) = runCatching { + val rs = lyricsProvider.fixSearchMusixmatch(q_artist, q_track, q_duration, userToken).body() + Log.w("Search Result", rs.toString()) + return@runCatching rs + } + + suspend fun getMusixmatchLyrics( + trackId: String, + userToken: String, + ) = runCatching { + val response = lyricsProvider.getMusixmatchLyrics(trackId, userToken).body() + if (response.message.body.subtitle != null) { + return@runCatching parseMusixmatchLyrics(response.message.body.subtitle.subtitle_body) + } else { + val unsyncedResponse = lyricsProvider.getMusixmatchUnsyncedLyrics(trackId, userToken).body() + if (unsyncedResponse.message.body.lyrics != null && unsyncedResponse.message.body.lyrics.lyrics_body != "") { + return@runCatching parseUnsyncedLyrics(unsyncedResponse.message.body.lyrics.lyrics_body) + } else { + null + } + } + } + + suspend fun getMusixmatchLyricsByQ( + track: SearchMusixmatchResponse.Message.Body.Track.TrackX, + userToken: String, + ) = runCatching { + val response = lyricsProvider.getMusixmatchLyricsByQ(track, userToken).body() + + if (!response.message.body.subtitle_list + .isNullOrEmpty() && + response.message.body.subtitle_list + .firstOrNull() + ?.subtitle + ?.subtitle_body != null + ) { + return@runCatching parseMusixmatchLyrics( + response.message.body.subtitle_list + .firstOrNull() + ?.subtitle + ?.subtitle_body!!, + ) + } else { + val unsyncedResponse = lyricsProvider.getMusixmatchUnsyncedLyrics(track.track_id.toString(), userToken).body() + if (unsyncedResponse.message.body.lyrics != null && unsyncedResponse.message.body.lyrics.lyrics_body != "") { + return@runCatching parseUnsyncedLyrics(unsyncedResponse.message.body.lyrics.lyrics_body) + } else { + null + } + } + } + + suspend fun getMusixmatchTranslateLyrics( + trackId: String, + userToken: String, + language: String, + ) = runCatching { + lyricsProvider + .getMusixmatchTranslateLyrics(trackId, userToken, language) + .body() + } + + suspend fun getLrclibLyrics( + q_track: String, + q_artist: String, + duration: Int?, + ) = runCatching { + val rs = + lyricsProvider + .searchLrclibLyrics( + q_track = q_track, + q_artist = q_artist, + ).body>() + val lrclibObject: LrclibObject? = + if (duration != null) { + rs.find { abs(it.duration.toInt() - duration) <= 10 } + } else { + rs.firstOrNull() + } + if (lrclibObject != null) { + val syncedLyrics = lrclibObject.syncedLyrics + val plainLyrics = lrclibObject.plainLyrics + if (syncedLyrics != null) { + parseMusixmatchLyrics(syncedLyrics) + } else if (plainLyrics != null) { + parseUnsyncedLyrics(plainLyrics) + } else { + null + } + } else { + null + } + } +} \ No newline at end of file diff --git a/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/LyricsProviders.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/LyricsProviders.kt new file mode 100644 index 00000000..57eac07e --- /dev/null +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/LyricsProviders.kt @@ -0,0 +1,352 @@ +package com.maxrave.lyricsproviders + +import android.content.Context +import com.maxrave.lyricsproviders.encoder.brotli +import com.maxrave.lyricsproviders.models.body.MusixmatchCredentialsBody +import com.maxrave.lyricsproviders.models.response.SearchMusixmatchResponse +import com.maxrave.lyricsproviders.utils.CustomRedirectConfig +import com.maxrave.lyricsproviders.utils.fromString +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.cache.HttpCache +import io.ktor.client.plugins.compression.ContentEncoding +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.cookies.AcceptAllCookiesStorage +import io.ktor.client.plugins.cookies.HttpCookies +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.headers +import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.contentType +import io.ktor.http.parameters +import io.ktor.serialization.kotlinx.KotlinxSerializationConverter +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import okhttp3.Cache +import java.net.Proxy + +class LyricsProviders( + private val context: Context, + private val commonJson: Json, +) { + private var lyricsClient = createClient() + + var musixmatchCookie: String? = null + set(value) { + field = value + } + + var musixmatchUserToken: String? = null + + var proxy: Proxy? = null + set(value) { + field = value + lyricsClient.close() + lyricsClient = createClient() + } + + private fun cachePath() = context.cacheDir.resolve("http_cache") + + private fun createClient() = + HttpClient(OkHttp) { + expectSuccess = true + followRedirects = false + engine { + config { + cache( + Cache(cachePath(), 50L * 1024 * 1024), + ) + } + } + install(HttpCache) + install(HttpSend) { + maxSendCount = 100 + } + install(HttpCookies) { + storage = AcceptAllCookiesStorage() + } + install(CustomRedirectConfig) { + checkHttpMethod = false + allowHttpsDowngrade = true + defaultHostUrl = "https://apic-desktop.musixmatch.com" + } + install(ContentNegotiation) { + register( + ContentType.Text.Plain, + KotlinxSerializationConverter( + commonJson, + ), + ) + json( + commonJson, + ) + } + install(ContentEncoding) { + brotli(1.0F) + gzip(0.9F) + deflate(0.8F) + } + defaultRequest { + url("https://apic-desktop.musixmatch.com/ws/1.1/") + } + if (proxy != null) { + engine { + proxy = this@LyricsProviders.proxy + } + } + } + + suspend fun getMusixmatchUserToken() = + lyricsClient.get("token.get?app_id=android-player-v1.0") { + contentType(ContentType.Application.Json) + headers { + header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") + header(HttpHeaders.Accept, "*/*") + header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") + header(HttpHeaders.Connection, "keep-alive") + if (musixmatchCookie != null) { + val listCookies = fromString(musixmatchCookie, commonJson) + if (!listCookies.isNullOrEmpty()) { + val appendCookie = + listCookies.joinToString(separator = "; ") { eachCookie -> + eachCookie + } + header(HttpHeaders.Cookie, appendCookie) + } + } + } + } + + suspend fun postMusixmatchPostCredentials( + email: String, + password: String, + userToken: String, + ) = lyricsClient.post("https://apic.musixmatch.com/ws/1.1/credential.post") { + contentType(ContentType.Application.Json) + headers { + header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") + header(HttpHeaders.Accept, "*/*") + header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") + header(HttpHeaders.Connection, "keep-alive") + } + parameter("app_id", "android-player-v1.0") + parameter("usertoken", userToken) + parameter("format", "json") + setBody( + MusixmatchCredentialsBody( + listOf( + MusixmatchCredentialsBody.Credential( + MusixmatchCredentialsBody.Credential.CredentialData( + email = email, + password = password, + ), + ), + ), + ), + ) + } + + suspend fun searchMusixmatchTrackId( + q: String, + userToken: String, + ) = lyricsClient.get("macro.search?app_id=android-player-v1.0&page_size=5&page=1&s_track_rating=desc&quorum_factor=1.0") { + contentType(ContentType.Application.Json) + headers { + header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") + header(HttpHeaders.Accept, "*/*") + header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") + header(HttpHeaders.Connection, "keep-alive") + if (musixmatchCookie != null) { + val listCookies = fromString(musixmatchCookie, commonJson) + if (!listCookies.isNullOrEmpty()) { + val appendCookie = + listCookies.joinToString(separator = "; ") { eachCookie -> + eachCookie + } + header(HttpHeaders.Cookie, appendCookie) + } + } + } + + parameter("q", q) + parameter("usertoken", userToken) + } + + suspend fun fixSearchMusixmatch( + q_artist: String, + q_track: String, + q_duration: String, + userToken: String, + ) = lyricsClient.get( + "matcher.track.get?tags=scrobbling%2Cnotifications&subtitle_format=dfxp&page_size=5&questions_id_list=track_esync_action%2Ctrack_sync_action%2Ctrack_translation_action%2Clyrics_ai_mood_analysis_v3&optional_calls=track.richsync%2Ccrowd.track.actions&app_id=android-player-v1.0&country=us&part=lyrics_crowd%2Cuser%2Clyrics_vote%2Clyrics_poll%2Ctrack_lyrics_translation_status%2Clyrics_verified_by%2Clabels%2Ctrack_structure%2Ctrack_performer_tagging%2C&scrobbling_package=com.google.android.apps.youtube.music&language_iso_code=1&format=json", + ) { + contentType(ContentType.Application.Json) + parameter("usertoken", userToken) +// q_artist=culture+code,+james+roche+&+karra&q_track=make+me+move+(james+roche+remix) + parameter("q_artist", q_artist) + parameter("q_track", q_track) + parameter("q_duration", q_duration) + headers { + header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") + header(HttpHeaders.Accept, "*/*") + header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") + header(HttpHeaders.Connection, "keep-alive") + if (musixmatchCookie != null) { + val listCookies = fromString(musixmatchCookie, commonJson) + if (!listCookies.isNullOrEmpty()) { + val appendCookie = + listCookies.joinToString(separator = "; ") { eachCookie -> + eachCookie + } + header(HttpHeaders.Cookie, appendCookie) + } + } + } + } + + suspend fun getMusixmatchLyrics( + trackId: String, + userToken: String, + ) = lyricsClient.get("track.subtitle.get?app_id=android-player-v1.0&subtitle_format=id3") { + contentType(ContentType.Application.Json) + headers { + header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") + header(HttpHeaders.Accept, "*/*") + header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") + header(HttpHeaders.Connection, "keep-alive") + if (musixmatchCookie != null) { + val listCookies = fromString(musixmatchCookie, commonJson) + if (!listCookies.isNullOrEmpty()) { + val appendCookie = + listCookies.joinToString(separator = "; ") { eachCookie -> + eachCookie + } + header(HttpHeaders.Cookie, appendCookie) + } + } + } + + parameter("usertoken", userToken) + parameter("track_id", trackId) + } + + suspend fun getMusixmatchLyricsByQ( + track: SearchMusixmatchResponse.Message.Body.Track.TrackX, + userToken: String, + ) = lyricsClient.get("https://apic.musixmatch.com/ws/1.1/track.subtitles.get") { + contentType(ContentType.Application.Json) + headers { + header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") + header(HttpHeaders.Accept, "*/*") + header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") + header(HttpHeaders.Connection, "keep-alive") + if (musixmatchCookie != null) { + val listCookies = fromString(musixmatchCookie, commonJson) + if (!listCookies.isNullOrEmpty()) { + val appendCookie = + listCookies.joinToString(separator = "; ") { eachCookie -> + eachCookie + } + header(HttpHeaders.Cookie, appendCookie) + } + } + } + + parameter("usertoken", userToken) + parameter("track_id", track.track_id) + parameter("f_subtitle_length_max_deviation", "1") + parameter("page_size", "1") + parameter("questions_id_list", "track_esync_action%2Ctrack_sync_action%2Ctrack_translation_action%2Clyrics_ai_mood_analysis_v3") + parameter("optional_calls", "track.richsync%2Ccrowd.track.actions") + parameter("q_artist", track.artist_name) + parameter("q_track", track.track_name) + parameter("app_id", "android-player-v1.0") + parameter( + "part", + "lyrics_crowd%2Cuser%2Clyrics_vote%2Clyrics_poll%2Ctrack_lyrics_translation_status%2Clyrics_verified_by%2Clabels%2Ctrack_structure%2Ctrack_performer_tagging%2C", + ) + parameter("language_iso_code", "1") + parameter("format", "json") + parameter("q_duration", track.track_length) + } + + suspend fun getMusixmatchUnsyncedLyrics( + trackId: String, + userToken: String, + ) = lyricsClient.get("track.lyrics.get?app_id=android-player-v1.0&subtitle_format=id3") { + contentType(ContentType.Application.Json) + headers { + header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") + header(HttpHeaders.Accept, "*/*") + header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") + header(HttpHeaders.Connection, "keep-alive") + if (musixmatchCookie != null) { + val listCookies = fromString(musixmatchCookie, commonJson) + if (!listCookies.isNullOrEmpty()) { + val appendCookie = + listCookies.joinToString(separator = "; ") { eachCookie -> + eachCookie + } + header(HttpHeaders.Cookie, appendCookie) + } + } + } + parameter("usertoken", userToken) + parameter("track_id", trackId) + } + + suspend fun searchLrclibLyrics( + q_track: String, + q_artist: String, + ) = lyricsClient.get("https://lrclib.net/api/search") { + contentType(ContentType.Application.Json) + headers { + header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") + header(HttpHeaders.Accept, "*/*") + header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") + header(HttpHeaders.Connection, "keep-alive") + } + parameter("q", "$q_artist $q_track") + } + + suspend fun getMusixmatchTranslateLyrics( + trackId: String, + userToken: String, + language: String, + ) = lyricsClient.get("https://apic.musixmatch.com/ws/1.1/crowd.track.translations.get") { + contentType(ContentType.Application.Json) + headers { + header(HttpHeaders.UserAgent, "PostmanRuntime/7.33.0") + header(HttpHeaders.Accept, "*/*") + header(HttpHeaders.AcceptEncoding, "gzip, deflate, br") + header(HttpHeaders.Connection, "keep-alive") + if (musixmatchCookie != null) { + val listCookies = fromString(musixmatchCookie, commonJson) + if (!listCookies.isNullOrEmpty()) { + val appendCookie = + listCookies.joinToString(separator = "; ") { eachCookie -> + eachCookie + } + header(HttpHeaders.Cookie, appendCookie) + } + } + } + parameters { + parameter("translation_fields_set", "minimal") + parameter("track_id", trackId) + parameter("selected_language", language) + parameter("comment_format", "text") + parameter("part", "user") + parameter("format", "json") + parameter("usertoken", userToken) + parameter("app_id", "android-player-v1.0") + parameter("tags", "playing") + } + } +} \ No newline at end of file diff --git a/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/encoder/BrotliEncoder.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/encoder/BrotliEncoder.kt new file mode 100644 index 00000000..6ebd8c6c --- /dev/null +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/encoder/BrotliEncoder.kt @@ -0,0 +1,29 @@ +package com.maxrave.lyricsproviders.encoder + +import io.ktor.client.plugins.compression.ContentEncodingConfig +import io.ktor.util.ContentEncoder +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.ByteWriteChannel +import io.ktor.utils.io.jvm.javaio.toByteReadChannel +import io.ktor.utils.io.jvm.javaio.toInputStream +import org.brotli.dec.BrotliInputStream +import kotlin.coroutines.CoroutineContext + +object BrotliEncoder : ContentEncoder { + override val name: String = "br" + override fun decode(source: ByteReadChannel, coroutineContext: CoroutineContext): ByteReadChannel { + return BrotliInputStream(source.toInputStream()).toByteReadChannel(coroutineContext) + } + + override fun encode(source: ByteReadChannel, coroutineContext: CoroutineContext): ByteReadChannel { + throw UnsupportedOperationException("Encode not implemented by the library yet.") + } + + override fun encode(source: ByteWriteChannel, coroutineContext: CoroutineContext): ByteWriteChannel { + throw UnsupportedOperationException("Encode not implemented by the library yet.") + } +} + +fun ContentEncodingConfig.brotli(quality: Float? = null) { + customEncoder(BrotliEncoder, quality) +} \ No newline at end of file diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/body/MusixmatchCredentialsBody.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/body/MusixmatchCredentialsBody.kt similarity index 89% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/body/MusixmatchCredentialsBody.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/body/MusixmatchCredentialsBody.kt index 0f5a01a0..31ec0f41 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/body/MusixmatchCredentialsBody.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/body/MusixmatchCredentialsBody.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models.body +package com.maxrave.lyricsproviders.models.body import kotlinx.serialization.Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/lyrics/Line.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/lyrics/Line.kt similarity index 86% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/lyrics/Line.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/lyrics/Line.kt index d79891f0..143e6513 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/lyrics/Line.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/lyrics/Line.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models.lyrics +package com.maxrave.lyricsproviders.models.lyrics import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/lyrics/Lyrics.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/lyrics/Lyrics.kt similarity index 86% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/lyrics/Lyrics.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/lyrics/Lyrics.kt index 7cd29b44..f0bd8088 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/lyrics/Lyrics.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/lyrics/Lyrics.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models.lyrics +package com.maxrave.lyricsproviders.models.lyrics import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/LrclibObject.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/LrclibObject.kt similarity index 86% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/LrclibObject.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/LrclibObject.kt index 0091cebe..c6efb873 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/LrclibObject.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/LrclibObject.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models +package com.maxrave.lyricsproviders.models.response import kotlinx.serialization.Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchCredential.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchCredential.kt similarity index 95% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchCredential.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchCredential.kt index ccd450de..1744094d 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchCredential.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchCredential.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models.musixmatch +package com.maxrave.lyricsproviders.models.response import kotlinx.serialization.Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchLyricsReponse.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchLyricsReponse.kt similarity index 93% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchLyricsReponse.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchLyricsReponse.kt index e57a7b30..0cf41709 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchLyricsReponse.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchLyricsReponse.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models.musixmatch +package com.maxrave.lyricsproviders.models.response import kotlinx.serialization.Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchLyricsResponseByQ.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchLyricsResponseByQ.kt similarity index 93% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchLyricsResponseByQ.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchLyricsResponseByQ.kt index 93a15d92..cae23d7d 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchLyricsResponseByQ.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchLyricsResponseByQ.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models.musixmatch +package com.maxrave.lyricsproviders.models.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchTranslationLyricsResponse.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchTranslationLyricsResponse.kt similarity index 93% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchTranslationLyricsResponse.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchTranslationLyricsResponse.kt index 38c6fc7d..60eec690 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/MusixmatchTranslationLyricsResponse.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/MusixmatchTranslationLyricsResponse.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models.musixmatch +package com.maxrave.lyricsproviders.models.response import kotlinx.serialization.Serializable @Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/SearchMusixmatchResponse.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/SearchMusixmatchResponse.kt similarity index 94% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/SearchMusixmatchResponse.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/SearchMusixmatchResponse.kt index bdc77793..fd053f6e 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/SearchMusixmatchResponse.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/SearchMusixmatchResponse.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models.musixmatch +package com.maxrave.lyricsproviders.models.response import kotlinx.serialization.Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/UserTokenResponse.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/UserTokenResponse.kt similarity index 87% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/UserTokenResponse.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/UserTokenResponse.kt index 8f00406c..f221df30 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/musixmatch/UserTokenResponse.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/models/response/UserTokenResponse.kt @@ -1,4 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.models.musixmatch +package com.maxrave.lyricsproviders.models.response import kotlinx.serialization.Serializable diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/parser/MusixmatchLyricsParser.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/parser/MusixmatchLyricsParser.kt similarity index 90% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/parser/MusixmatchLyricsParser.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/parser/MusixmatchLyricsParser.kt index 672199b8..95802be9 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/parser/MusixmatchLyricsParser.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/parser/MusixmatchLyricsParser.kt @@ -1,7 +1,7 @@ -package com.maxrave.kotlinytmusicscraper.parser +package com.maxrave.lyricsproviders.parser -import com.maxrave.kotlinytmusicscraper.models.lyrics.Line -import com.maxrave.kotlinytmusicscraper.models.lyrics.Lyrics +import com.maxrave.lyricsproviders.models.lyrics.Line +import com.maxrave.lyricsproviders.models.lyrics.Lyrics fun parseMusixmatchLyrics(data: String): Lyrics { val regex = Regex("\\[(\\d{2}):(\\d{2})\\.(\\d{2})\\](.+)") diff --git a/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/utils/Converter.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/utils/Converter.kt new file mode 100644 index 00000000..ee986273 --- /dev/null +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/utils/Converter.kt @@ -0,0 +1,28 @@ +package com.maxrave.lyricsproviders.utils + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +fun fromString( + value: String?, + json: Json, +): List? { + return try { + json.decodeFromString>(value ?: return null) + } catch (e: Exception) { + e.printStackTrace() + null + } +} + +fun fromArrayListNull( + list: List?, + json: Json, +): String? { + return try { + json.encodeToString(list?.filterNotNull() ?: return null) + } catch (e: Exception) { + e.printStackTrace() + null + } +} \ No newline at end of file diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/utils/CustomRedirectConfig.kt b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/utils/CustomRedirectConfig.kt similarity index 94% rename from kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/utils/CustomRedirectConfig.kt rename to lyricsProviders/src/main/java/com/maxrave/lyricsproviders/utils/CustomRedirectConfig.kt index d3de9a63..2d77a022 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/utils/CustomRedirectConfig.kt +++ b/lyricsProviders/src/main/java/com/maxrave/lyricsproviders/utils/CustomRedirectConfig.kt @@ -1,8 +1,4 @@ -package com.maxrave.kotlinytmusicscraper.utils - -/* -* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. -*/ +package com.maxrave.lyricsproviders.utils import io.ktor.client.HttpClient import io.ktor.client.call.HttpClientCall @@ -14,20 +10,31 @@ import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.statement.HttpResponse import io.ktor.events.EventDefinition import io.ktor.http.HttpHeaders -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode import io.ktor.http.authority import io.ktor.http.isSecure import io.ktor.http.takeFrom import io.ktor.util.AttributeKey -import io.ktor.util.logging.KtorSimpleLogger import io.ktor.utils.io.InternalAPI import io.ktor.utils.io.KtorDsl +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.util.logging.KtorSimpleLogger private val ALLOWED_FOR_REDIRECT: Set = setOf(HttpMethod.Get, HttpMethod.Head) private val LOGGER = KtorSimpleLogger("io.ktor.client.plugins.HttpRedirect") +private fun HttpStatusCode.isRedirect(): Boolean = + when (value) { + HttpStatusCode.MovedPermanently.value, + HttpStatusCode.Found.value, + HttpStatusCode.TemporaryRedirect.value, + HttpStatusCode.PermanentRedirect.value, + HttpStatusCode.SeeOther.value, + -> true + + else -> false + } /** * An [HttpClient] plugin that handles HTTP redirects * Use only for Musixmatch API @@ -128,13 +135,13 @@ class CustomRedirectConfig private constructor( * Disallow redirect with a security downgrade. */ if (!allowHttpsDowngrade && originProtocol.isSecure() && !url.protocol.isSecure()) { - LOGGER.trace("Can not redirect ${context.url} because of security downgrade") + LOGGER.trace("Can not redirect {} because of security downgrade", context.url) return call } if (originAuthority != url.authority) { headers.remove(HttpHeaders.Authorization) - LOGGER.trace("Removing Authorization header from redirect for ${context.url}") + LOGGER.trace("Removing Authorization header from redirect for {}", context.url) } } @@ -143,16 +150,4 @@ class CustomRedirectConfig private constructor( } } } -} - -private fun HttpStatusCode.isRedirect(): Boolean = - when (value) { - HttpStatusCode.MovedPermanently.value, - HttpStatusCode.Found.value, - HttpStatusCode.TemporaryRedirect.value, - HttpStatusCode.PermanentRedirect.value, - HttpStatusCode.SeeOther.value, - -> true - - else -> false - } \ No newline at end of file +} \ No newline at end of file diff --git a/lyricsProviders/src/test/java/com/maxrave/lyricsproviders/ExampleUnitTest.kt b/lyricsProviders/src/test/java/com/maxrave/lyricsproviders/ExampleUnitTest.kt new file mode 100644 index 00000000..35617eb3 --- /dev/null +++ b/lyricsProviders/src/test/java/com/maxrave/lyricsproviders/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.maxrave.lyricsproviders + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c50df2f7..375b9e95 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,3 +23,4 @@ rootProject.name = "SimpMusic" include("app") include(":kotlinYtmusicScraper") include(":spotify") +include(":lyricsProviders") diff --git a/spotify/build.gradle.kts b/spotify/build.gradle.kts index 618bf6fc..a031e8a6 100644 --- a/spotify/build.gradle.kts +++ b/spotify/build.gradle.kts @@ -4,7 +4,6 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) kotlin("plugin.serialization") - alias(libs.plugins.aboutlibraries) } android {