Skip to content
This repository has been archived by the owner on Aug 7, 2024. It is now read-only.

Commit

Permalink
feat: support for autoplay via hyperpipe
Browse files Browse the repository at this point in the history
  • Loading branch information
Bnyro committed Apr 5, 2024
1 parent b759ef8 commit 7b629c3
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package app.suhasdissa.vibeyou.backend.api

import app.suhasdissa.vibeyou.backend.models.hyper.NextSongsResponse
import app.suhasdissa.vibeyou.utils.Pref
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import retrofit2.http.GET
import retrofit2.http.Path

interface HyperpipeApi {
/**
*
*/
@GET("https://{instance}/next/{videoId}")
suspend fun getNext(
@Path("instance") instance: String = Pref.sharedPreferences
.getString(Pref.hyperpipeApiUrlKey, "")
?.toHttpUrlOrNull()
?.host.orEmpty(),
@Path("videoId") videoId: String
): NextSongsResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package app.suhasdissa.vibeyou.backend.models.hyper

import kotlinx.serialization.Serializable

@Serializable
data class MediaSession(
val album: String,
val thumbnails: List<Thumbnail>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.suhasdissa.vibeyou.backend.models.hyper

import kotlinx.serialization.Serializable

@Serializable
data class NextSongsResponse(
val lyricsId: String,
val mediaSession: MediaSession,
val songs: List<Song>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package app.suhasdissa.vibeyou.backend.models.hyper

import androidx.core.net.toUri
import app.suhasdissa.vibeyou.backend.data.Song
import app.suhasdissa.vibeyou.backend.database.entities.SongEntity
import kotlinx.serialization.Serializable

@Serializable
data class Song(
val id: String,
val subtitle: String,
val thumbnails: List<Thumbnail>,
val title: String
) {
val asSong: Song get() {
return Song(
id = id,
title = title,
artistsText = subtitle,
durationText = null,
thumbnailUri = thumbnails.maxByOrNull { it.height }?.url?.toUri()
)
}

val asSongEntity: SongEntity get() {
return SongEntity(
id = id,
title = title,
artistsText = subtitle,
durationText = null,
thumbnailUrl = thumbnails.maxByOrNull { it.height }?.url
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.suhasdissa.vibeyou.backend.models.hyper

import kotlinx.serialization.Serializable

@Serializable
data class Thumbnail(
val height: Int,
val url: String,
val width: Int
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.suhasdissa.vibeyou.backend.repository
import android.net.Uri
import android.util.Log
import androidx.core.net.toUri
import androidx.media3.common.MediaItem
import app.suhasdissa.vibeyou.backend.data.Album
import app.suhasdissa.vibeyou.backend.data.Artist
import app.suhasdissa.vibeyou.backend.data.Song
Expand All @@ -17,6 +18,7 @@ import app.suhasdissa.vibeyou.utils.Pref
import app.suhasdissa.vibeyou.utils.RetrofitHelper
import app.suhasdissa.vibeyou.utils.asAlbum
import app.suhasdissa.vibeyou.utils.asArtist
import app.suhasdissa.vibeyou.utils.asMediaItem
import app.suhasdissa.vibeyou.utils.asSong
import app.suhasdissa.vibeyou.utils.asSongEntity

Expand All @@ -25,24 +27,22 @@ class PipedMusicRepository(
private val searchDao: SearchDao
) {
var pipedApi = RetrofitHelper.createPipedApi()
private val hyperApi = RetrofitHelper.createHyperpipeApi()

suspend fun getAudioSource(id: String): Uri? {
return runCatching { pipedApi.getStreams(vidId = id) }
.getOrNull()
?.audioStreams
?.get(1)
?.randomOrNull()
?.url
?.toUri()
}
//
// suspend fun getRecommendedSongs(id: String): List<MediaItem> {
// val relatedSongs =
// pipedApi.getStreams(vidId = id).relatedStreams.slice(0..1).map {
// it.asSong
// }
// songsDao.addSongs(relatedSongs)
// return relatedSongs.map { it.asMediaItem }
// }

suspend fun getRecommendedSongs(id: String): List<MediaItem> {
val relatedSongs = hyperApi.getNext(videoId = id).songs
songsDao.addSongs(relatedSongs.map { it.asSongEntity })
return relatedSongs.map { it.asSong.asMediaItem }
}

suspend fun getPlaylistInfo(playlistId: String): PlaylistInfo =
pipedApi.getPlaylistInfo(playlistId = playlistId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.graphics.Rect
import android.media.audiofx.LoudnessEnhancer
import android.net.Uri
import android.os.Handler
import android.util.Log
import androidx.annotation.ColorInt
import androidx.core.graphics.drawable.toBitmap
import androidx.media3.common.AudioAttributes
Expand Down Expand Up @@ -41,6 +42,7 @@ import androidx.media3.session.MediaSessionService
import app.suhasdissa.vibeyou.MellowMusicApplication
import app.suhasdissa.vibeyou.utils.DynamicDataSource
import app.suhasdissa.vibeyou.utils.Pref
import app.suhasdissa.vibeyou.utils.enqueue
import coil.ImageLoader
import coil.request.ErrorResult
import coil.request.ImageRequest
Expand All @@ -52,6 +54,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext

class PlayerService : MediaSessionService(), MediaSession.Callback, Player.Listener {
private var mediaSession: MediaSession? = null
Expand Down Expand Up @@ -218,6 +221,7 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, Player.Liste
val url = runBlocking {
container.pipedMusicRepository.getAudioSource(videoId)
}
appendToQueue(videoId)
url?.let {
dataSpec.withUri(it).subrange(dataSpec.uriPositionOffset, chunkLength)
} ?: error("Stream not found")
Expand All @@ -226,6 +230,20 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, Player.Liste
return DynamicDataSource.Companion.Factory(resolvingDataSource, defaultDataSource)
}

private fun appendToQueue(videoId: String) = CoroutineScope(Dispatchers.Main).launch {
// enough other videos left in the queue
if (player.mediaItemCount - player.currentMediaItemIndex > 5) return@launch

try {
val nextSongs = withContext(Dispatchers.IO) {
container.pipedMusicRepository.getRecommendedSongs(videoId)
}
player.addMediaItems(nextSongs.take(3))
} catch (e: Exception) {
Log.e("hyperpipe: error fetching next", e.stackTrace.contentToString())
}
}

@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun createMediaSourceFactory(): MediaSource.Factory {
return DefaultMediaSourceFactory(createDataSourceFactory())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ fun NetworkSettingsScreen() {
currentServer = it
}
}

item {
TextFieldPref(
key = Pref.hyperpipeApiUrlKey,
defaultValue = "",
title = stringResource(id = R.string.hyperpipe_api_url)
)
}
}
}
if (showDialog) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package app.suhasdissa.vibeyou.ui.screens.settings

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import app.suhasdissa.vibeyou.utils.Pref

@Composable
fun TextFieldPref(
key: String,
defaultValue: String,
title: String,
onValueChange: (String) -> Unit = {}
) {
var value by remember {
mutableStateOf(Pref.sharedPreferences.getString(key, defaultValue).orEmpty())
}

OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
value = value,
onValueChange = {
value = it
Pref.sharedPreferences.edit(true) { putString(key, it) }
onValueChange(it)
},
label = {
Text(text = title)
}
)
}
1 change: 1 addition & 0 deletions app/src/main/java/app/suhasdissa/vibeyou/utils/Pref.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ object Pref {
const val latestReverseSongsPrefKey = "LatestReverseSongsPrefKey"
const val customPipedInstanceKey = "CustomPipedInstanceKey"
const val disableSearchHistoryKey = "DisableSearchHistory"
const val hyperpipeApiUrlKey = "HyperpipeApiUrl"

lateinit var sharedPreferences: SharedPreferences

Expand Down
19 changes: 16 additions & 3 deletions app/src/main/java/app/suhasdissa/vibeyou/utils/RetrofitHelper.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
package app.suhasdissa.vibeyou.utils

import app.suhasdissa.vibeyou.backend.api.HyperpipeApi
import app.suhasdissa.vibeyou.backend.api.PipedApi
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.create

object RetrofitHelper {
fun createPipedApi(): PipedApi {
val json = Json { ignoreUnknownKeys = true }
val json by lazy {
Json { ignoreUnknownKeys = true }
}

fun createPipedApi(): PipedApi {
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://pipedapi.kavin.rocks/")
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()

return retrofit.create(PipedApi::class.java)
return retrofit.create()
}

fun createHyperpipeApi(): HyperpipeApi {
val retrofit = Retrofit.Builder()
.baseUrl("https://hyperpipeapi.onrender.com")
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()

return retrofit.create()
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,5 @@
<string name="delete_playlist_and_songs">Delete playlist and songs</string>
<string name="clear_playlist">Clear Playlist</string>
<string name="disable_search_history">Disable search history</string>
<string name="hyperpipe_api_url">Hyperpipe API URL</string>
</resources>

0 comments on commit 7b629c3

Please sign in to comment.