Handle radios when logged in anonymously.
On top this fix, this commit adds support for "My content" and "Favorites" instance radios (fixes #51), as well as clearly separates instance radios from user radios. Radios were a bit unusable when not logged in with an actual authorized user account, this commit fixes the following elements: * Anonymous users get a transient session cookie when starting a radio session that was not stored and forwarded on playback, meaning no radios would play; * Anonymous users do not have their own own content. Thus, only the "Random" radio makes sense in that context. This commit only display the instance radios that are relevant to your authentication status. "My content" radios needs the user ID to function properly, this commit also adds retrieving it from the /api/v1/users/users/me/ endpoint, which now may be used in the future for other purposes.
This commit is contained in:
parent
18e981fba5
commit
490de25b05
|
@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.LoginDialog
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Userinfo
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
|
@ -103,9 +104,13 @@ class LoginActivity : AppCompatActivity() {
|
|||
setString("access_token", result.get().token)
|
||||
}
|
||||
|
||||
dialog.dismiss()
|
||||
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
||||
finish()
|
||||
Userinfo.get()?.let {
|
||||
dialog.dismiss()
|
||||
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
throw Exception(getString(R.string.login_error_userinfo))
|
||||
}
|
||||
|
||||
is Result.Failure -> {
|
||||
|
|
|
@ -88,6 +88,10 @@ class MainActivity : AppCompatActivity() {
|
|||
startService(Intent(this, PlayerService::class.java))
|
||||
DownloadService.start(this, PinService::class.java)
|
||||
|
||||
GlobalScope.launch(IO) {
|
||||
Userinfo.get()
|
||||
}
|
||||
|
||||
now_playing_toggle.setOnClickListener {
|
||||
CommandBus.send(Command.ToggleState)
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@ import android.view.ViewGroup
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.utils.Event
|
||||
import com.github.apognu.otter.utils.EventBus
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.views.LoadingImageView
|
||||
import com.preference.PowerPreference
|
||||
import kotlinx.android.synthetic.main.row_radio.view.*
|
||||
import kotlinx.android.synthetic.main.row_radio_header.view.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
@ -22,43 +22,105 @@ class RadiosAdapter(val context: Context?, private val listener: OnRadioClickLis
|
|||
fun onClick(holder: ViewHolder, radio: Radio)
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
enum class RowType {
|
||||
Header,
|
||||
InstanceRadio,
|
||||
UserRadio
|
||||
}
|
||||
|
||||
private val instanceRadios: List<Radio> by lazy {
|
||||
context?.let {
|
||||
return@lazy when (val username = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("actor_username")) {
|
||||
"" -> listOf(
|
||||
Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description))
|
||||
)
|
||||
|
||||
else -> listOf(
|
||||
Radio(0, "actor_content", context.getString(R.string.radio_your_content_title), context.getString(R.string.radio_your_content_description), username),
|
||||
Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description)),
|
||||
Radio(0, "favorites", context.getString(R.string.favorites), context.getString(R.string.radio_favorites_description)),
|
||||
Radio(0, "less-listened", context.getString(R.string.radio_less_listened_title), context.getString(R.string.radio_less_listened_description))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
listOf<Radio>()
|
||||
}
|
||||
|
||||
private fun getRadioAt(position: Int): Radio {
|
||||
return when (getItemViewType(position)) {
|
||||
RowType.InstanceRadio.ordinal -> instanceRadios[position - 1]
|
||||
else -> data[position - instanceRadios.size - 2]
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = instanceRadios.size + data.size + 2
|
||||
|
||||
override fun getItemId(position: Int) = data[position].id.toLong()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RadiosAdapter.ViewHolder {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.row_radio, parent, false)
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when {
|
||||
position == 0 || position == instanceRadios.size + 1 -> RowType.Header.ordinal
|
||||
position <= instanceRadios.size -> RowType.InstanceRadio.ordinal
|
||||
else -> RowType.UserRadio.ordinal
|
||||
}
|
||||
}
|
||||
|
||||
return ViewHolder(view, listener).also {
|
||||
view.setOnClickListener(it)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RadiosAdapter.ViewHolder {
|
||||
return when (viewType) {
|
||||
RowType.InstanceRadio.ordinal, RowType.UserRadio.ordinal -> {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.row_radio, parent, false)
|
||||
|
||||
ViewHolder(view, listener).also {
|
||||
view.setOnClickListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
else -> ViewHolder(LayoutInflater.from(context).inflate(R.layout.row_radio_header, parent, false), null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RadiosAdapter.ViewHolder, position: Int) {
|
||||
val radio = data[position]
|
||||
|
||||
holder.art.visibility = View.VISIBLE
|
||||
holder.name.text = radio.name
|
||||
holder.description.text = radio.description
|
||||
|
||||
context?.let { context ->
|
||||
val icon = when (radio.radio_type) {
|
||||
"random" -> R.drawable.shuffle
|
||||
"less-listened" -> R.drawable.sad
|
||||
else -> null
|
||||
when (getItemViewType(position)) {
|
||||
RowType.Header.ordinal -> {
|
||||
context?.let {
|
||||
when (position) {
|
||||
0 -> holder.label.text = context.getString(R.string.radio_instance_radios)
|
||||
instanceRadios.size + 1 -> holder.label.text = context.getString(R.string.radio_user_radios)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
icon?.let {
|
||||
holder.native = true
|
||||
RowType.InstanceRadio.ordinal, RowType.UserRadio.ordinal -> {
|
||||
val radio = getRadioAt(position)
|
||||
|
||||
holder.art.setImageDrawable(context.getDrawable(icon))
|
||||
holder.art.alpha = 0.7f
|
||||
holder.art.setColorFilter(context.getColor(R.color.controlForeground))
|
||||
holder.art.visibility = View.VISIBLE
|
||||
holder.name.text = radio.name
|
||||
holder.description.text = radio.description
|
||||
|
||||
context?.let { context ->
|
||||
val icon = when (radio.radio_type) {
|
||||
"actor_content" -> R.drawable.library
|
||||
"favorites" -> R.drawable.favorite
|
||||
"random" -> R.drawable.shuffle
|
||||
"less-listened" -> R.drawable.sad
|
||||
else -> null
|
||||
}
|
||||
|
||||
icon?.let {
|
||||
holder.native = true
|
||||
|
||||
holder.art.setImageDrawable(context.getDrawable(icon))
|
||||
holder.art.alpha = 0.7f
|
||||
holder.art.setColorFilter(context.getColor(R.color.controlForeground))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View, private val listener: OnRadioClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||
inner class ViewHolder(view: View, private val listener: OnRadioClickListener?) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||
val label = view.label
|
||||
val art = view.art
|
||||
val name = view.name
|
||||
val description = view.description
|
||||
|
@ -66,7 +128,7 @@ class RadiosAdapter(val context: Context?, private val listener: OnRadioClickLis
|
|||
var native = false
|
||||
|
||||
override fun onClick(view: View?) {
|
||||
listener.onClick(this, data[layoutPosition])
|
||||
listener?.onClick(this, getRadioAt(layoutPosition))
|
||||
}
|
||||
|
||||
fun spin() {
|
||||
|
|
|
@ -12,7 +12,6 @@ import android.os.Build
|
|||
import android.os.IBinder
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.view.KeyEvent
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.android.exoplayer2.C
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.github.apognu.otter.repositories.FavoritedRepository
|
|||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.Gson
|
||||
|
@ -18,7 +19,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class RadioSessionBody(val radio_type: String, var custom_radio: Int? = null)
|
||||
data class RadioSessionBody(val radio_type: String, var custom_radio: Int? = null, var related_object_id: String? = null)
|
||||
data class RadioSession(val id: Int)
|
||||
data class RadioTrackBody(val session: Int)
|
||||
data class RadioTrack(val position: Int, val track: RadioTrackID)
|
||||
|
@ -29,6 +30,7 @@ class RadioPlayer(val context: Context) {
|
|||
|
||||
private var currentRadio: Radio? = null
|
||||
private var session: Int? = null
|
||||
private var cookie: String? = null
|
||||
|
||||
private val favoritedRepository = FavoritedRepository(context)
|
||||
|
||||
|
@ -36,8 +38,11 @@ class RadioPlayer(val context: Context) {
|
|||
Cache.get(context, "radio_type")?.readLine()?.let { radio_type ->
|
||||
Cache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id ->
|
||||
Cache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session ->
|
||||
currentRadio = Radio(radio_id, radio_type, "", "")
|
||||
session = radio_session
|
||||
Cache.get(context, "radio_cookie")?.readLine()?.let { radio_cookie ->
|
||||
currentRadio = Radio(radio_id, radio_type, "", "")
|
||||
session = radio_session
|
||||
cookie = radio_cookie
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +64,7 @@ class RadioPlayer(val context: Context) {
|
|||
Cache.delete(context, "radio_type")
|
||||
Cache.delete(context, "radio_id")
|
||||
Cache.delete(context, "radio_session")
|
||||
Cache.delete(context, "radio_cookie")
|
||||
}
|
||||
|
||||
fun isActive() = currentRadio != null && session != null
|
||||
|
@ -66,24 +72,26 @@ class RadioPlayer(val context: Context) {
|
|||
private suspend fun createSession() {
|
||||
currentRadio?.let { radio ->
|
||||
try {
|
||||
val request = RadioSessionBody(radio.radio_type).apply {
|
||||
val request = RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply {
|
||||
if (radio_type == "custom") {
|
||||
custom_radio = radio.id
|
||||
}
|
||||
}
|
||||
|
||||
val body = Gson().toJson(request)
|
||||
val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/"))
|
||||
val (_, response, result) = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/"))
|
||||
.authorize()
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.awaitObjectResult(gsonDeserializerOf(RadioSession::class.java))
|
||||
.awaitObjectResponseResult(gsonDeserializerOf(RadioSession::class.java))
|
||||
|
||||
session = result.get().id
|
||||
cookie = response.header("set-cookie").joinToString(";")
|
||||
|
||||
Cache.set(context, "radio_type", radio.radio_type.toByteArray())
|
||||
Cache.set(context, "radio_id", radio.id.toString().toByteArray())
|
||||
Cache.set(context, "radio_session", session.toString().toByteArray())
|
||||
Cache.set(context, "radio_cookie", cookie.toString().toByteArray())
|
||||
|
||||
prepareNextTrack(true)
|
||||
} catch (e: Exception) {
|
||||
|
@ -101,6 +109,11 @@ class RadioPlayer(val context: Context) {
|
|||
val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/"))
|
||||
.authorize()
|
||||
.header("Content-Type", "application/json")
|
||||
.apply {
|
||||
cookie?.let {
|
||||
header("cookie", it)
|
||||
}
|
||||
}
|
||||
.body(body)
|
||||
.awaitObjectResult(gsonDeserializerOf(RadioTrack::class.java))
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.FunkwhaleResponse
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import com.github.apognu.otter.utils.RadiosCache
|
||||
|
@ -19,15 +18,7 @@ class RadiosRepository(override val context: Context?) : Repository<Radio, Radio
|
|||
|
||||
override fun onDataFetched(data: List<Radio>): List<Radio> {
|
||||
return data
|
||||
.map { radio ->
|
||||
radio.apply { radio_type = "custom" }
|
||||
}
|
||||
.map { radio -> radio.apply { radio_type = "custom" } }
|
||||
.toMutableList()
|
||||
.apply {
|
||||
context?.let { context ->
|
||||
add(0, Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description)))
|
||||
add(1, Radio(0, "less-listened", context.getString(R.string.radio_less_listened_title), context.getString(R.string.radio_less_listened_description)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,10 @@ package com.github.apognu.otter.utils
|
|||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.preference.PowerPreference
|
||||
|
||||
data class User(
|
||||
val full_username: String
|
||||
)
|
||||
|
||||
sealed class CacheItem<D : Any>(val data: List<D>)
|
||||
class ArtistsCache(data: List<Artist>) : CacheItem<Artist>(data)
|
||||
class AlbumsCache(data: List<Album>) : CacheItem<Album>(data)
|
||||
|
@ -147,7 +151,8 @@ data class Radio(
|
|||
val id: Int,
|
||||
var radio_type: String,
|
||||
val name: String,
|
||||
val description: String
|
||||
val description: String,
|
||||
var related_object_id: String? = null
|
||||
)
|
||||
|
||||
data class DownloadInfo(
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package com.github.apognu.otter.utils
|
||||
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.github.kittinunf.result.Result
|
||||
import com.preference.PowerPreference
|
||||
|
||||
object Userinfo {
|
||||
suspend fun get(): User? {
|
||||
try {
|
||||
val hostname = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
|
||||
val (_, _, result) = Fuel.get("$hostname/api/v1/users/users/me/")
|
||||
.authorize()
|
||||
.awaitObjectResponseResult(gsonDeserializerOf(User::class.java))
|
||||
|
||||
return when (result) {
|
||||
is Result.Success -> {
|
||||
val user = result.get()
|
||||
|
||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
||||
setString("actor_username", user.full_username)
|
||||
}
|
||||
|
||||
user
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,7h-3v5.5c0,1.38 -1.12,2.5 -2.5,2.5S10,13.88 10,12.5s1.12,-2.5 2.5,-2.5c0.57,0 1.08,0.19 1.5,0.51L14,5h4v2zM4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6z"/>
|
||||
</vector>
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/colorPrimary"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label"
|
||||
style="@style/AppTheme.ListHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
|
@ -10,6 +10,7 @@
|
|||
<string name="login_logging_in">Connexion</string>
|
||||
<string name="login_error_hostname">Cela ne semble pas être un nom d\hôte valide</string>
|
||||
<string name="login_error_hostname_https">Le nom d\'hôte Funkwhale devrait être sécurisé à travers HTTPS</string>
|
||||
<string name="login_error_userinfo">Nous n\'avons pas pu récupérer les informations à propos de votre utilisateur</string>
|
||||
<string name="toolbar_search">Rechercher</string>
|
||||
<string name="title_downloads">Téléchargements</string>
|
||||
<string name="title_settings">Paramètres</string>
|
||||
|
@ -88,6 +89,11 @@
|
|||
<string name="track_info_details_track_bitrate">Bitrate</string>
|
||||
<string name="track_info_details_track_instance">Instance Funkwhale</string>
|
||||
<string name="radio_playback_error">Une erreur s\'est produite lors de la lecture de cette radio</string>
|
||||
<string name="radio_instance_radios">Radios de l\'instance</string>
|
||||
<string name="radio_user_radios">Radios des utilisateurs</string>
|
||||
<string name="radio_your_content_title">Votre contenu</string>
|
||||
<string name="radio_your_content_description">Une sélection de votre propre bibliothèque.</string>
|
||||
<string name="radio_favorites_description">Jouez vos morceaux favoris dans une boucle allègre infinie.</string>
|
||||
<string name="radio_random_title">Aléatoire</string>
|
||||
<string name="radio_random_description">Choix de pistes totalement aléatoires, vous découvrirez peut-être quelque chose ?</string>
|
||||
<string name="radio_less_listened_title">Moins écoutées</string>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<string name="login_logging_in">Logging in</string>
|
||||
<string name="login_error_hostname">This could not be understood as a valid URL</string>
|
||||
<string name="login_error_hostname_https">The Funkwhale hostname should be secure through HTTPS</string>
|
||||
<string name="login_error_userinfo">We could not retrieve information about your user</string>
|
||||
<string name="toolbar_search">Search</string>
|
||||
<string name="title_downloads">Downloads</string>
|
||||
<string name="title_settings">Settings</string>
|
||||
|
@ -89,6 +90,11 @@
|
|||
<string name="track_info_details_track_bitrate">Bitrate</string>
|
||||
<string name="track_info_details_track_instance">Funkwhale instance</string>
|
||||
<string name="radio_playback_error">There was an error while trying to play this radio</string>
|
||||
<string name="radio_instance_radios">Instance radios</string>
|
||||
<string name="radio_user_radios">User radios</string>
|
||||
<string name="radio_your_content_title">Your content</string>
|
||||
<string name="radio_your_content_description">Picks from your own libraries</string>
|
||||
<string name="radio_favorites_description"> Play your favorites tunes in a never-ending happiness loop.</string>
|
||||
<string name="radio_random_title">Random</string>
|
||||
<string name="radio_random_description">Totally random picks, maybe you\'ll discover new things?</string>
|
||||
<string name="radio_less_listened_title">Less listened</string>
|
||||
|
|
|
@ -93,4 +93,10 @@
|
|||
<item name="android:textColor">@color/controlColor</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.ListHeader">
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:textColor">@android:color/white</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue