diff --git a/app/build.gradle b/app/build.gradle index cdc3902..5a61674 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,9 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:4.9.0" implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.4.1' - implementation 'org.jsoup:jsoup:1.10.3' + implementation 'org.jsoup:jsoup:1.13.1' + + implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.ibm.watson:ibm-watson:8.6.3' } \ No newline at end of file diff --git a/app/src/main/java/com/archrahkshi/spotifine/App.kt b/app/src/main/java/com/archrahkshi/spotifine/App.kt index 1d22c13..87bdba4 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/App.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/App.kt @@ -2,12 +2,15 @@ package com.archrahkshi.spotifine import android.app.Application import android.os.StrictMode +import timber.log.Timber +import timber.log.Timber.DebugTree class App : Application() { // Some weird warning which is certainly false override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { + Timber.plant(DebugTree()) StrictMode.setThreadPolicy( StrictMode.ThreadPolicy.Builder() // .detectDiskReads() - Not detecting disk reads cuz Xiaomi ¯\_(ツ)_/¯ diff --git a/app/src/main/java/com/archrahkshi/spotifine/data/LibraryListsAdapter.kt b/app/src/main/java/com/archrahkshi/spotifine/data/LibraryListsAdapter.kt index 17083b7..a83cd10 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/data/LibraryListsAdapter.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/data/LibraryListsAdapter.kt @@ -12,12 +12,12 @@ import kotlinx.android.synthetic.main.item_library_list.view.layoutLibraryList import kotlinx.android.synthetic.main.item_library_list.view.textViewListInfo import kotlinx.android.synthetic.main.item_library_list.view.textViewListName -class LibraryListsAdapter( +class LibraryListsAdapter( private val libraryLists: List, private val clickListener: (ListType) -> Unit -) : RecyclerView.Adapter>() { +) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( LayoutInflater .from(parent.context) .inflate(R.layout.item_library_list, parent, false) @@ -25,11 +25,11 @@ class LibraryListsAdapter( override fun getItemCount() = libraryLists.size - override fun onBindViewHolder(holder: ViewHolder, position: Int) { + override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(libraryLists[position], clickListener) } - class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val imageViewListPic = view.imageViewListPic private val layoutItemList = view.layoutLibraryList private val textViewListInfo = view.textViewListInfo @@ -42,7 +42,6 @@ class LibraryListsAdapter( is Playlist -> listType.name is Artist -> listType.name is Album -> listType.name - else -> null } textViewListInfo.text = when (listType) { is Playlist -> { @@ -51,14 +50,12 @@ class LibraryListsAdapter( } is Artist -> "" is Album -> listType.artists - else -> null } Glide.with(viewTest).load( when (listType) { is Playlist -> listType.image is Artist -> listType.image is Album -> listType.image - else -> null } ).into(imageViewListPic) layoutItemList.setOnClickListener { clickListener(listType) } diff --git a/app/src/main/java/com/archrahkshi/spotifine/data/Entities.kt b/app/src/main/java/com/archrahkshi/spotifine/data/entities.kt similarity index 91% rename from app/src/main/java/com/archrahkshi/spotifine/data/Entities.kt rename to app/src/main/java/com/archrahkshi/spotifine/data/entities.kt index 00d4137..05a1073 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/data/Entities.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/data/entities.kt @@ -3,6 +3,8 @@ package com.archrahkshi.spotifine.data import androidx.room.Entity import androidx.room.PrimaryKey +sealed class ListType + @Entity data class Playlist( @PrimaryKey(autoGenerate = true) @@ -11,7 +13,7 @@ data class Playlist( val name: String, val size: Int, val url: String, -) +) : ListType() @Entity data class Artist( @@ -20,7 +22,7 @@ data class Artist( val image: String, val name: String, val url: String, -) +) : ListType() @Entity data class Album( @@ -31,7 +33,7 @@ data class Album( val name: String, val size: Int, val url: String, -) +) : ListType() @Entity data class Track( diff --git a/app/src/main/java/com/archrahkshi/spotifine/ui/LibraryListsFragment.kt b/app/src/main/java/com/archrahkshi/spotifine/ui/LibraryListsFragment.kt index f1bc544..318cdac 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/ui/LibraryListsFragment.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/ui/LibraryListsFragment.kt @@ -1,7 +1,6 @@ package com.archrahkshi.spotifine.ui import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -24,16 +23,15 @@ import com.archrahkshi.spotifine.util.PLAYLISTS import com.archrahkshi.spotifine.util.SIZE import com.archrahkshi.spotifine.util.SPOTIFY_PREFIX import com.archrahkshi.spotifine.util.URL -import com.google.gson.JsonObject -import com.google.gson.JsonParser +import com.archrahkshi.spotifine.util.createAlbum +import com.archrahkshi.spotifine.util.createArtist +import com.archrahkshi.spotifine.util.createPlaylist +import com.archrahkshi.spotifine.util.getJsonFromApi import kotlinx.android.synthetic.main.fragment_library_lists.recyclerViewLists import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import java.io.IOException import kotlin.coroutines.CoroutineContext class LibraryListsFragment( @@ -52,60 +50,56 @@ class LibraryListsFragment( val accessToken = arguments?.getString(ACCESS_TOKEN) launch { - val libraryLists = createLibraryLists(accessToken)!! - recyclerViewLists.adapter = LibraryListsAdapter(libraryLists) { + recyclerViewLists.adapter = LibraryListsAdapter(createLibraryLists(accessToken)) { fragmentManager?.beginTransaction()?.replace( R.id.frameLayoutLibrary, when (it) { is Playlist -> TracksFragment().apply { arguments = Bundle().apply { - putString(URL, it.url) + putString(ACCESS_TOKEN, accessToken) putString(IMAGE, it.image) putString(NAME, it.name) putInt(SIZE, it.size) - putString(ACCESS_TOKEN, accessToken) + putString(URL, it.url) } } is Artist -> LibraryListsFragment().apply { arguments = Bundle().apply { + putString(ACCESS_TOKEN, accessToken) + putString(IMAGE, it.image) putString(LIST_TYPE, ALBUMS) putString(URL, it.url) - putString(IMAGE, it.image) - putString(ACCESS_TOKEN, accessToken) } } is Album -> TracksFragment().apply { arguments = Bundle().apply { - putString(URL, it.url) + putString(ACCESS_TOKEN, accessToken) + putString(ARTISTS, it.artists) putString(IMAGE, it.image) putString(NAME, it.name) - putString(ARTISTS, it.artists) putInt(SIZE, it.size) - putString(ACCESS_TOKEN, accessToken) + putString(URL, it.url) } } - else -> null - }!! + } )?.addToBackStack(null)?.commit() } } } - private suspend fun createLibraryLists( - accessToken: String? - ) = withContext(Dispatchers.IO) { + private suspend fun createLibraryLists(accessToken: String?) = withContext(Dispatchers.IO) { when (arguments?.getString(LIST_TYPE)) { PLAYLISTS -> getJsonFromApi( - "me/playlists", + "${SPOTIFY_PREFIX}me/playlists", accessToken )["items"].asJsonArray.map { createPlaylist(it.asJsonObject) } ARTISTS -> getJsonFromApi( - "me/following?type=artist", + "${SPOTIFY_PREFIX}me/following?type=artist", accessToken )["artists"].asJsonObject["items"].asJsonArray.map { createArtist(it.asJsonObject) } ALBUMS -> { val json = getJsonFromApi( - "${arguments?.getString(URL) ?: "me"}/albums", + "$SPOTIFY_PREFIX${arguments?.getString(URL) ?: "me"}/albums", accessToken ) val items = json["items"].asJsonArray @@ -122,51 +116,7 @@ class LibraryListsFragment( else -> listOf() } } - else -> null + else -> listOf() } } - - private fun createPlaylist(item: JsonObject): Playlist { - val tracks = item["tracks"].asJsonObject - return Playlist( - image = item["images"].asJsonArray.first().asJsonObject["url"].asString, - name = item["name"].asString, - size = tracks["total"].asInt, - url = tracks["href"].asString - ) - } - - private fun createArtist(item: JsonObject) = Artist( - image = item["images"].asJsonArray[1].asJsonObject["url"].asString, - name = item["name"].asString, - url = "artists/${item["id"].asString}" - ) - - private fun createAlbum(item: JsonObject, type: String) = Album( - artists = item["artists"].asJsonArray - .joinToString { it.asJsonObject["name"].asString }, - image = item["images"].asJsonArray[1].asJsonObject["url"].asString, - name = item["name"].asString, - size = item["total_tracks"].asInt, - url = if (type == FROM_ARTIST) - "${item["href"].asString}/tracks" - else - item["tracks"].asJsonObject["href"].asString - ) - - private fun getJsonFromApi(requestPostfix: String, accessToken: String?) = JsonParser().parse( - try { - OkHttpClient().newCall( - Request.Builder() - .url("$SPOTIFY_PREFIX$requestPostfix") - .header("Authorization", "Bearer $accessToken") - .header("Accept", "application/json") - .header("Content-Type", "application/json") - .build() - ).execute().body?.string() - } catch (e: IOException) { - Log.wtf("getJsonFromApi", e) - null - } - ).asJsonObject } diff --git a/app/src/main/java/com/archrahkshi/spotifine/ui/LyricsFragment.kt b/app/src/main/java/com/archrahkshi/spotifine/ui/LyricsFragment.kt index 08715ae..52c9020 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/ui/LyricsFragment.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/ui/LyricsFragment.kt @@ -1,7 +1,6 @@ package com.archrahkshi.spotifine.ui import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,33 +8,23 @@ import androidx.fragment.app.Fragment import com.archrahkshi.spotifine.R import com.archrahkshi.spotifine.data.LyricsAdapter import com.archrahkshi.spotifine.util.ARTISTS -import com.archrahkshi.spotifine.util.GENIUS_ACCESS_TOKEN -import com.archrahkshi.spotifine.util.GENIUS_API_BASE_URL -import com.archrahkshi.spotifine.util.GENIUS_BASE_URL +import com.archrahkshi.spotifine.util.IS_LYRICS_TRANSLATED import com.archrahkshi.spotifine.util.NAME -import com.archrahkshi.spotifine.util.TRANSLATOR_API_KEY -import com.archrahkshi.spotifine.util.TRANSLATOR_URL -import com.archrahkshi.spotifine.util.TRANSLATOR_VERSION -import com.google.gson.JsonParser -import com.ibm.cloud.sdk.core.security.IamAuthenticator -import com.ibm.watson.language_translator.v3.LanguageTranslator -import com.ibm.watson.language_translator.v3.model.TranslateOptions +import com.archrahkshi.spotifine.util.ORIGINAL_LYRICS +import com.archrahkshi.spotifine.util.getOriginalLyrics +import com.archrahkshi.spotifine.util.identifyLanguage +import com.archrahkshi.spotifine.util.translateFromTo import com.ibm.watson.language_translator.v3.util.Language.RUSSIAN import kotlinx.android.synthetic.main.fragment_lyrics.buttonTranslate import kotlinx.android.synthetic.main.fragment_lyrics.recyclerViewLyrics +import kotlinx.android.synthetic.main.fragment_lyrics.viewLyricsFloor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import org.jsoup.Jsoup -import java.io.IOException import java.util.Locale import kotlin.coroutines.CoroutineContext class LyricsFragment( - private val isLyricsTranslated: Boolean, override val coroutineContext: CoroutineContext = Dispatchers.Main.immediate ) : Fragment(), CoroutineScope { @@ -48,119 +37,74 @@ class LyricsFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val appLanguage = RUSSIAN + val appLocale = Locale(appLanguage) + + val isLyricsTranslated = arguments?.getBoolean(IS_LYRICS_TRANSLATED) ?: false + val name = arguments?.getString(NAME) ?: "" + val artists = arguments?.getString(ARTISTS) ?: "" + launch { - val name = arguments?.getString(NAME) ?: "" - val artists = arguments?.getString(ARTISTS) ?: "" - val originalLyrics = getOriginalLyrics(name, artists) + val originalLyrics = arguments?.getString(ORIGINAL_LYRICS) + ?: getOriginalLyrics(name, artists) + if (originalLyrics == null) { - recyclerViewLyrics.adapter = LyricsAdapter( - listOf("It seems like this song is instrumental... or is it not?") - ) + recyclerViewLyrics.adapter = LyricsAdapter(listOf(getString(R.string.no_lyrics))) buttonTranslate.visibility = View.GONE + viewLyricsFloor.visibility = View.GONE } else { - if (isLyricsTranslated) { - val (detectedLanguage, translatedLyrics) = getTranslatedLyrics(originalLyrics) - val appLocale = Locale(RUSSIAN) - buttonTranslate.text = getString( - R.string.detected_language, - Locale(detectedLanguage).getDisplayLanguage(appLocale), - appLocale.getDisplayLanguage(appLocale) - ) + val identifiedLanguage = originalLyrics.identifyLanguage() + + if (identifiedLanguage == appLanguage) { + buttonTranslate.visibility = View.GONE + viewLyricsFloor.visibility = View.GONE recyclerViewLyrics.adapter = LyricsAdapter( - translatedLyrics.split('\n') + originalLyrics.split('\n') ) } else { - buttonTranslate.text = getString(R.string.translate) - recyclerViewLyrics.adapter = LyricsAdapter(originalLyrics.split('\n')) - } + if (!isLyricsTranslated) { + buttonTranslate.text = getString(R.string.translate) + recyclerViewLyrics.adapter = LyricsAdapter( + originalLyrics.split('\n') + ) + } else { + val translatedLyrics = + originalLyrics.translateFromTo(identifiedLanguage, appLanguage) + ?: getString(R.string.unidentifiable_language) + try { + buttonTranslate.text = getString( + R.string.detected_language, + Locale(identifiedLanguage).getDisplayLanguage(appLocale), + appLocale.getDisplayLanguage(appLocale) + ) + } catch (e: NullPointerException) { // Couldn't identify language + buttonTranslate.text = getString( + R.string.detected_language, + getString(R.string.elvish), + appLocale.getDisplayLanguage(appLocale) + ) + } + recyclerViewLyrics.adapter = LyricsAdapter( + translatedLyrics.split('\n') + ) + } - buttonTranslate.setOnClickListener { - fragmentManager?.beginTransaction()?.replace( - R.id.frameLayoutPlayer, - LyricsFragment(!isLyricsTranslated).apply { - arguments = Bundle().apply { - putString(NAME, name) - putString(ARTISTS, artists) + buttonTranslate.setOnClickListener { + fragmentManager?.beginTransaction()?.replace( + R.id.frameLayoutPlayer, + LyricsFragment().apply { + arguments = Bundle().apply { + putString(ARTISTS, artists) + putBoolean(IS_LYRICS_TRANSLATED, !isLyricsTranslated) + putString(NAME, name) + if (isLyricsTranslated) + putString(ORIGINAL_LYRICS, originalLyrics) + } } - } - )?.commit() + )?.commit() + } } } } } - - private suspend fun getOriginalLyrics( - title: String, - artists: String - ) = withContext(Dispatchers.IO) { - val songInfo = JsonParser().parse( - buildGeniusRequest( - "$GENIUS_API_BASE_URL/search?q=$artists $title".replace(" ", "%20") - ) - ).asJsonObject["response"].asJsonObject["hits"].asJsonArray.find { - it.asJsonObject["type"].asString == "song" - } - if (songInfo != null) ( - getLyricsFromPath(songInfo.asJsonObject["result"].asJsonObject["path"].asString) - ?: "Something went wrong, sorry :(" - ).deleteTrash() - else { - Log.wtf("Genius", "no song info") - null - } - } - - private fun getLyricsFromPath(path: String) = try { - Jsoup.parse(buildGeniusRequest("$GENIUS_BASE_URL$path")).run { - select("div.lyrics") - .first() - ?.select("p") - ?.first() - ?.html() - ?.replace("
", "\n") - } - } catch (e: IOException) { - Log.wtf("Jsoup", e) - null - } - - private fun String.deleteTrash(): String { - var str = this - while (str.indexOf("<") != -1) { - str = str.replace( - str.substring(str.indexOf("<"), str.indexOf(">") + 1), - "" - ) - } - return str - } - - private fun buildGeniusRequest(url: String) = OkHttpClient().newCall( - Request.Builder() - .url(url) - .header("Authorization", "Bearer $GENIUS_ACCESS_TOKEN") - .build() - ).execute().body?.string() - - private suspend fun getTranslatedLyrics(lyricsOriginal: String): Pair = - withContext(Dispatchers.IO) { - val result = LanguageTranslator( - TRANSLATOR_VERSION, - IamAuthenticator(TRANSLATOR_API_KEY) - ).apply { - serviceUrl = TRANSLATOR_URL - }.translate( - TranslateOptions.Builder() - // Line separators must be doubled for the lyrics to be translated line by line, - // not as a uniform text - .addText(lyricsOriginal.replace("\n", "\n\n")) - .target(RUSSIAN) - .build() - ).execute().result - Pair( - result.detectedLanguage, - // Returning to the original line separators - result.translations.first().translation.replace("\n\n", "\n") - ) - } } diff --git a/app/src/main/java/com/archrahkshi/spotifine/ui/MainActivity.kt b/app/src/main/java/com/archrahkshi/spotifine/ui/MainActivity.kt index 61987ac..5bf55bb 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/ui/MainActivity.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/ui/MainActivity.kt @@ -2,7 +2,6 @@ package com.archrahkshi.spotifine.ui import android.content.Intent import android.os.Bundle -import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.archrahkshi.spotifine.R import com.archrahkshi.spotifine.util.ACCESS_TOKEN @@ -12,6 +11,7 @@ import com.archrahkshi.spotifine.util.SPOTIFY_REQUEST_CODE import com.spotify.sdk.android.auth.AuthorizationClient import com.spotify.sdk.android.auth.AuthorizationRequest import com.spotify.sdk.android.auth.AuthorizationResponse +import timber.log.Timber class MainActivity : AppCompatActivity() { @@ -41,21 +41,20 @@ class MainActivity : AppCompatActivity() { AuthorizationClient.getResponse(resultCode, intent) when (response?.type) { AuthorizationResponse.Type.TOKEN -> { - Log.i("Token", "OK") startActivity( Intent(this, LibraryActivity::class.java).apply { putExtra( ACCESS_TOKEN, response.accessToken.also { - Log.i("Access token", it) + Timber.i(it) } ) } ) finish() } - AuthorizationResponse.Type.ERROR -> Log.wtf("Token", response.error) - else -> Log.wtf("Token", "bullshit") + AuthorizationResponse.Type.ERROR -> Timber.wtf(response.error) + else -> Timber.wtf("bullshit") } } } diff --git a/app/src/main/java/com/archrahkshi/spotifine/ui/PlayerActivity.kt b/app/src/main/java/com/archrahkshi/spotifine/ui/PlayerActivity.kt index 93cd01f..9e696d5 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/ui/PlayerActivity.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/ui/PlayerActivity.kt @@ -1,7 +1,6 @@ package com.archrahkshi.spotifine.ui import android.os.Bundle -import android.util.Log import android.widget.SeekBar import android.widget.SeekBar.OnSeekBarChangeListener import androidx.appcompat.app.AppCompatActivity @@ -9,6 +8,7 @@ import com.archrahkshi.spotifine.R import com.archrahkshi.spotifine.util.ARTISTS import com.archrahkshi.spotifine.util.DURATION import com.archrahkshi.spotifine.util.ID +import com.archrahkshi.spotifine.util.IS_LYRICS_TRANSLATED import com.archrahkshi.spotifine.util.NAME import com.archrahkshi.spotifine.util.SPOTIFY_CLIENT_ID import com.archrahkshi.spotifine.util.SPOTIFY_REDIRECT_URI @@ -16,6 +16,7 @@ import com.spotify.android.appremote.api.ConnectionParams import com.spotify.android.appremote.api.Connector import com.spotify.android.appremote.api.SpotifyAppRemote import kotlinx.android.synthetic.main.activity_player.* +import timber.log.Timber class PlayerActivity : AppCompatActivity() { private var spotifyAppRemote: SpotifyAppRemote? = null @@ -27,10 +28,11 @@ class PlayerActivity : AppCompatActivity() { if (savedInstanceState == null) supportFragmentManager.beginTransaction().replace( R.id.frameLayoutPlayer, - LyricsFragment(false).apply { + LyricsFragment().apply { arguments = Bundle().apply { - putString(NAME, intent.getStringExtra(NAME)) putString(ARTISTS, intent.getStringExtra(ARTISTS)) + putString(NAME, intent.getStringExtra(NAME)) + putBoolean(IS_LYRICS_TRANSLATED, false) } } ).commit() @@ -38,22 +40,22 @@ class PlayerActivity : AppCompatActivity() { override fun onStart() { super.onStart() + val id = intent.getStringExtra(ID) val duration = intent.getLongExtra(DURATION, 0) - Log.wtf("id", id?.toString()) - Log.wtf("duration", duration.toString()) - val connectionParams = ConnectionParams.Builder(SPOTIFY_CLIENT_ID) - .setRedirectUri(SPOTIFY_REDIRECT_URI) - .showAuthView(true) - .build() + Timber.wtf(id?.toString()) + Timber.wtf(duration.toString()) + SpotifyAppRemote.connect( this, - connectionParams, + ConnectionParams.Builder(SPOTIFY_CLIENT_ID) + .setRedirectUri(SPOTIFY_REDIRECT_URI) + .showAuthView(true) + .build(), object : Connector.ConnectionListener { override fun onConnected(spotifyAppRemote: SpotifyAppRemote) { this@PlayerActivity.spotifyAppRemote = spotifyAppRemote - val appRemote = this@PlayerActivity.spotifyAppRemote!! - Log.d("PlayerActivity", "Connected! Yay!") + Timber.d("Connected! Yay!") val seekBar = findViewById(R.id.seekBar) seekBar.max = duration.toInt() @@ -65,19 +67,19 @@ class PlayerActivity : AppCompatActivity() { if (flag == 0) { seekBar.progress = 0 } else { - appRemote.playerApi.seekTo(seekBar.progress.toLong()) + spotifyAppRemote.playerApi.seekTo(seekBar.progress.toLong()) } } override fun onStartTrackingTouch(seekBar: SeekBar) { if (flag == 1) { - appRemote.playerApi.pause() + spotifyAppRemote.playerApi.pause() } } override fun onStopTrackingTouch(seekBar: SeekBar) { if (flag == 1) { - appRemote.playerApi.resume() + spotifyAppRemote.playerApi.resume() } } } @@ -85,17 +87,17 @@ class PlayerActivity : AppCompatActivity() { buttonPlay.setOnClickListener { when (flag) { 0 -> { - appRemote.playerApi.play("spotify:track:$id") + spotifyAppRemote.playerApi.play("spotify:track:$id") flag = 1 buttonPlay.text = getString(R.string.pause) } 1 -> { - appRemote.playerApi.pause() + spotifyAppRemote.playerApi.pause() flag = 2 buttonPlay.text = getString(R.string.play) } 2 -> { - appRemote.playerApi.resume() + spotifyAppRemote.playerApi.resume() flag = 1 buttonPlay.text = getString(R.string.pause) } @@ -104,7 +106,7 @@ class PlayerActivity : AppCompatActivity() { } override fun onFailure(throwable: Throwable) { - Log.e("MyActivity", throwable.message, throwable) + Timber.e(throwable) // Something went wrong when attempting to connect! Handle errors here } diff --git a/app/src/main/java/com/archrahkshi/spotifine/ui/TracksFragment.kt b/app/src/main/java/com/archrahkshi/spotifine/ui/TracksFragment.kt index 21f7fe5..87e9984 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/ui/TracksFragment.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/ui/TracksFragment.kt @@ -2,16 +2,13 @@ package com.archrahkshi.spotifine.ui import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import com.archrahkshi.spotifine.R -import com.archrahkshi.spotifine.data.Track import com.archrahkshi.spotifine.data.TracksAdapter import com.archrahkshi.spotifine.util.ACCESS_TOKEN -import com.archrahkshi.spotifine.util.ALBUM_FROM_PLAYLIST_DISTINCTION import com.archrahkshi.spotifine.util.ARTISTS import com.archrahkshi.spotifine.util.DURATION import com.archrahkshi.spotifine.util.ID @@ -19,10 +16,9 @@ import com.archrahkshi.spotifine.util.IMAGE import com.archrahkshi.spotifine.util.NAME import com.archrahkshi.spotifine.util.SIZE import com.archrahkshi.spotifine.util.URL +import com.archrahkshi.spotifine.util.createTrackLists import com.archrahkshi.spotifine.util.setWordTracks import com.bumptech.glide.Glide -import com.google.gson.JsonObject -import com.google.gson.JsonParser import kotlinx.android.synthetic.main.fragment_tracks.imageViewHeader import kotlinx.android.synthetic.main.fragment_tracks.recyclerViewTracks import kotlinx.android.synthetic.main.fragment_tracks.textViewHeaderLine1 @@ -31,10 +27,7 @@ import kotlinx.android.synthetic.main.fragment_tracks.textViewHeaderLine3 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import java.io.IOException +import timber.log.Timber import kotlin.coroutines.CoroutineContext import kotlin.time.ExperimentalTime @@ -51,30 +44,30 @@ class TracksFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val args = this.arguments + val args = this.arguments!! - textViewHeaderLine1.text = args?.getString(NAME) + textViewHeaderLine1.text = args.getString(NAME) - val artists = args?.getString(ARTISTS) + val artists = args.getString(ARTISTS) if (artists != null) textViewHeaderLine2.text = artists else textViewHeaderLine2.visibility = View.GONE - val size = args?.getInt(SIZE) + val size = args.getInt(SIZE) textViewHeaderLine3.text = getString( R.string.header_line3, size, setWordTracks(context, size) ) - Glide.with(this).load(args?.getString(IMAGE)).into(imageViewHeader) + Glide.with(this).load(args.getString(IMAGE)).into(imageViewHeader) launch { recyclerViewTracks.adapter = TracksAdapter( - createTrackLists(args?.getString(URL), args?.getString(ACCESS_TOKEN)) + createTrackLists(args.getString(URL)!!, args.getString(ACCESS_TOKEN)) ) { - Log.i("Track clicked", it.toString()) + Timber.tag("Track clicked").i(it.toString()) startActivity( Intent(activity, PlayerActivity::class.java).apply { putExtra(ID, it.id) @@ -86,48 +79,4 @@ class TracksFragment( } } } - - private fun getJsonFromApi(url: String?, accessToken: String?) = JsonParser().parse( - try { - OkHttpClient().newCall( - Request.Builder() - .url(url!!) - .header("Authorization", "Bearer $accessToken") - .header("Accept", "application/json") - .header("Content-Type", "application/json") - .build() - ).execute().body?.string() - } catch (e: IOException) { - Log.wtf("getJsonFromApi", e) - null - } - ).asJsonObject - - private suspend fun createTrackLists( - url: String?, - accessToken: String? - ) = withContext(Dispatchers.IO) { - val json = getJsonFromApi(url, accessToken) - val items = json["items"].asJsonArray - when ( - json["href"] - .asString - .removePrefix("https://api.spotify.com/v1/") - .take(ALBUM_FROM_PLAYLIST_DISTINCTION) - ) { - "album" -> items.map { createTrack(it.asJsonObject) } - "playl" -> items.map { createTrack(it.asJsonObject["track"].asJsonObject) } - else -> listOf() - } - } - - private suspend fun createTrack(item: JsonObject) = withContext(Dispatchers.IO) { - Track( - name = item["name"].asString, - artists = item["artists"].asJsonArray - .joinToString { it.asJsonObject["name"].asString }, - duration = item["duration_ms"].asLong, - id = item["id"].asString - ) - } } diff --git a/app/src/main/java/com/archrahkshi/spotifine/util/Functions.kt b/app/src/main/java/com/archrahkshi/spotifine/util/common.kt similarity index 55% rename from app/src/main/java/com/archrahkshi/spotifine/util/Functions.kt rename to app/src/main/java/com/archrahkshi/spotifine/util/common.kt index 2deb501..f47fd20 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/util/Functions.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/util/common.kt @@ -2,6 +2,12 @@ package com.archrahkshi.spotifine.util import android.content.Context import com.archrahkshi.spotifine.R +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import okhttp3.OkHttpClient +import okhttp3.Request +import timber.log.Timber +import java.io.IOException import kotlin.time.ExperimentalTime import kotlin.time.milliseconds @@ -25,3 +31,21 @@ fun formatDuration(milliseconds: Long) = milliseconds.milliseconds.toComponents if (duration.isNotEmpty() && ss <= ONE_DIGIT) duration += '0' "$duration$ss" } + +fun getJsonFromApi(url: String, accessToken: String?): JsonObject = JsonParser().parse( + try { + url.buildRequest(accessToken) + } catch (e: IOException) { + Timber.wtf(e) + null + } +).asJsonObject + +fun String.buildRequest(accessToken: String?) = OkHttpClient().newCall( + Request.Builder() + .url(this) + .header("Authorization", "Bearer $accessToken") + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .build() +).execute().body?.string() diff --git a/app/src/main/java/com/archrahkshi/spotifine/util/Constants.kt b/app/src/main/java/com/archrahkshi/spotifine/util/constants.kt similarity index 89% rename from app/src/main/java/com/archrahkshi/spotifine/util/Constants.kt rename to app/src/main/java/com/archrahkshi/spotifine/util/constants.kt index 8d54ca8..90e6eac 100644 --- a/app/src/main/java/com/archrahkshi/spotifine/util/Constants.kt +++ b/app/src/main/java/com/archrahkshi/spotifine/util/constants.kt @@ -7,16 +7,17 @@ const val ARTIST_FROM_USER_DISTINCTION = 2 const val ARTISTS = "artists" const val DURATION = "duration" const val FROM_ARTIST = "from_artist" -const val FROM_USER = "from_me" +const val FROM_USER = "from_user" const val GENIUS_ACCESS_TOKEN = "apFfOiyOXUivBQvSDkm4J4wlTQuOOx-9uV1Euc-wcKviTZFBXCpDyflAg68ay6AF" const val GENIUS_API_BASE_URL = "https://api.genius.com" const val GENIUS_BASE_URL = "https://genius.com" -const val GENIUS_CLIENT_ID = "XZaueDB4wXtQkMeWKm1Qwrs3HHPdKEza-b1_Ubwu0ntfKIaXvP_JTqEmgzZLQ94m" const val ID = "id" +const val IS_LYRICS_TRANSLATED = "is_lyrics_translated" const val IMAGE = "image" const val LIST_TYPE = "list_type" const val NAME = "name" const val ONE_DIGIT = 9 +const val ORIGINAL_LYRICS = "original_lyrics" const val PLAYLISTS = "playlists" const val SIZE = "size" const val SPOTIFY_CLIENT_ID = "fbe0ec189f0247f99909e75530bac38e" diff --git a/app/src/main/java/com/archrahkshi/spotifine/util/genius.kt b/app/src/main/java/com/archrahkshi/spotifine/util/genius.kt new file mode 100644 index 0000000..6d199fd --- /dev/null +++ b/app/src/main/java/com/archrahkshi/spotifine/util/genius.kt @@ -0,0 +1,63 @@ +package com.archrahkshi.spotifine.util + +import com.google.gson.JsonParser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jsoup.Jsoup +import timber.log.Timber +import java.io.IOException + +suspend fun getOriginalLyrics( + title: String, + artists: String +) = withContext(Dispatchers.IO) { + val songInfo = JsonParser().parse( + "$GENIUS_API_BASE_URL/search?q=$artists $title".replace(" ", "%20") + .buildRequest(GENIUS_ACCESS_TOKEN) + ).asJsonObject["response"].asJsonObject["hits"].asJsonArray.find { + it.asJsonObject["type"].asString == "song" + } + if (songInfo != null) { + val lyrics = songInfo.asJsonObject["result"].asJsonObject["path"].asString.getLyrics() + if (lyrics == "[Instrumental]") + null + else + lyrics + } else { + Timber.wtf("no song info") + null + } +} + +fun String.getLyrics() = try { // Forbidden dark magic + val parsed = Jsoup.parse("$GENIUS_BASE_URL$this".buildRequest(GENIUS_ACCESS_TOKEN)) + parsed?.run { + val lyricsClass = selectFirst("div.lyrics")?.selectFirst("p") + if (lyricsClass == null) { + Timber.wtf("ROOT") + selectFirst("div[class*=Lyrics__Root]") + ?.html() + ?.replace("
", "") + ?.split('\n') + ?.joinToString("\n") { it.trim() } + ?.cleanTags() + } else lyricsClass.html() + .split("
") + .joinToString("\n") { it.trim() } + .cleanTags() + } +} catch (e: IOException) { + Timber.wtf(e) + null +} + +fun String.cleanTags(): String { + var str = this + while (str.indexOf("<") != -1) { + str = str.replace( + str.substring(str.indexOf("<"), str.indexOf(">") + 1), + "" + ) + } + return str +} diff --git a/app/src/main/java/com/archrahkshi/spotifine/util/listBuilders.kt b/app/src/main/java/com/archrahkshi/spotifine/util/listBuilders.kt new file mode 100644 index 0000000..622dbf9 --- /dev/null +++ b/app/src/main/java/com/archrahkshi/spotifine/util/listBuilders.kt @@ -0,0 +1,22 @@ +package com.archrahkshi.spotifine.util + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun createTrackLists( + url: String, + accessToken: String? +) = withContext(Dispatchers.IO) { + val json = getJsonFromApi(url, accessToken) + val items = json["items"].asJsonArray + when ( + json["href"] + .asString + .removePrefix("https://api.spotify.com/v1/") + .take(ALBUM_FROM_PLAYLIST_DISTINCTION) + ) { + "album" -> items.map { createTrack(it.asJsonObject) } + "playl" -> items.map { createTrack(it.asJsonObject["track"].asJsonObject) } + else -> listOf() + } +} diff --git a/app/src/main/java/com/archrahkshi/spotifine/util/objectBuilders.kt b/app/src/main/java/com/archrahkshi/spotifine/util/objectBuilders.kt new file mode 100644 index 0000000..c1a291e --- /dev/null +++ b/app/src/main/java/com/archrahkshi/spotifine/util/objectBuilders.kt @@ -0,0 +1,42 @@ +package com.archrahkshi.spotifine.util + +import com.archrahkshi.spotifine.data.Album +import com.archrahkshi.spotifine.data.Artist +import com.archrahkshi.spotifine.data.Playlist +import com.archrahkshi.spotifine.data.Track +import com.google.gson.JsonObject + +fun createPlaylist(item: JsonObject): Playlist { + val tracks = item["tracks"].asJsonObject + return Playlist( + image = item["images"].asJsonArray.first().asJsonObject["url"].asString, + name = item["name"].asString, + size = tracks["total"].asInt, + url = tracks["href"].asString + ) +} + +fun createArtist(item: JsonObject) = Artist( + image = item["images"].asJsonArray[1].asJsonObject["url"].asString, + name = item["name"].asString, + url = "artists/${item["id"].asString}" +) + +fun createAlbum(item: JsonObject, type: String) = Album( + artists = item["artists"].asJsonArray.joinToString { it.asJsonObject["name"].asString }, + image = item["images"].asJsonArray[1].asJsonObject["url"].asString, + name = item["name"].asString, + size = item["total_tracks"].asInt, + url = if (type == FROM_ARTIST) + "${item["href"].asString}/tracks" + else + item["tracks"].asJsonObject["href"].asString +) + +fun createTrack(item: JsonObject) = Track( + name = item["name"].asString, + artists = item["artists"].asJsonArray + .joinToString { it.asJsonObject["name"].asString }, + duration = item["duration_ms"].asLong, + id = item["id"].asString +) diff --git a/app/src/main/java/com/archrahkshi/spotifine/util/watson.kt b/app/src/main/java/com/archrahkshi/spotifine/util/watson.kt new file mode 100644 index 0000000..030b27b --- /dev/null +++ b/app/src/main/java/com/archrahkshi/spotifine/util/watson.kt @@ -0,0 +1,39 @@ +package com.archrahkshi.spotifine.util + +import com.ibm.cloud.sdk.core.security.IamAuthenticator +import com.ibm.cloud.sdk.core.service.exception.ServiceResponseException +import com.ibm.watson.language_translator.v3.LanguageTranslator +import com.ibm.watson.language_translator.v3.model.IdentifyOptions +import com.ibm.watson.language_translator.v3.model.TranslateOptions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +private val languageTranslator = LanguageTranslator( + TRANSLATOR_VERSION, + IamAuthenticator(TRANSLATOR_API_KEY) +).apply { serviceUrl = TRANSLATOR_URL } + +suspend fun String.identifyLanguage(): String = withContext(Dispatchers.IO) { + languageTranslator + .identify(IdentifyOptions.Builder().text(this@identifyLanguage).build()) + .execute() + .result + .languages.first().language +} + +suspend fun String.translateFromTo(source: String, target: String) = withContext(Dispatchers.IO) { + try { + languageTranslator.translate( + TranslateOptions.Builder() + // Line separators must be doubled for the lyrics to be translated line by line, + // not as a uniform text + .addText(this@translateFromTo.replace("\n", "\n\n")) + .source(source) + .target(target) + .build() + ).execute().result.translations.first().translation + .replace("\n\n", "\n") // Returning to the original line separators + } catch (e: ServiceResponseException) { + null + } +} diff --git a/app/src/main/res/layout/fragment_lyrics.xml b/app/src/main/res/layout/fragment_lyrics.xml index ab87dfe..414fb07 100644 --- a/app/src/main/res/layout/fragment_lyrics.xml +++ b/app/src/main/res/layout/fragment_lyrics.xml @@ -1,24 +1,38 @@ - - + android:fillViewport="true"> + + + + + + + +