diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/api/HyperpipeApi.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/api/HyperpipeApi.kt new file mode 100644 index 00000000..9a14b05b --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/api/HyperpipeApi.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/MediaSession.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/MediaSession.kt new file mode 100644 index 00000000..18e86948 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/MediaSession.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/NextSongsResponse.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/NextSongsResponse.kt new file mode 100644 index 00000000..20540e87 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/NextSongsResponse.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/Song.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/Song.kt new file mode 100644 index 00000000..bd93d064 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/Song.kt @@ -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, + 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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/Thumbnail.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/Thumbnail.kt new file mode 100644 index 00000000..2d1fdee2 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/models/hyper/Thumbnail.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/PipedMusicRepository.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/PipedMusicRepository.kt index 2e216ff4..c098a934 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/PipedMusicRepository.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/PipedMusicRepository.kt @@ -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 @@ -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 @@ -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 { -// 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 { + 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) diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/services/PlayerService.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/services/PlayerService.kt index 82eec39d..046649ee 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/services/PlayerService.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/services/PlayerService.kt @@ -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 @@ -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 @@ -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 @@ -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") @@ -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()) diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/settings/NetworkSettingsScreen.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/settings/NetworkSettingsScreen.kt index 91dba9e3..b82a77c5 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/settings/NetworkSettingsScreen.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/settings/NetworkSettingsScreen.kt @@ -67,6 +67,14 @@ fun NetworkSettingsScreen() { currentServer = it } } + + item { + TextFieldPref( + key = Pref.hyperpipeApiUrlKey, + defaultValue = "", + title = stringResource(id = R.string.hyperpipe_api_url) + ) + } } } if (showDialog) { diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/settings/TextFieldPref.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/settings/TextFieldPref.kt new file mode 100644 index 00000000..ac863542 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/settings/TextFieldPref.kt @@ -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) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/suhasdissa/vibeyou/utils/Pref.kt b/app/src/main/java/app/suhasdissa/vibeyou/utils/Pref.kt index aa446aef..993ab235 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/utils/Pref.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/utils/Pref.kt @@ -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 diff --git a/app/src/main/java/app/suhasdissa/vibeyou/utils/RetrofitHelper.kt b/app/src/main/java/app/suhasdissa/vibeyou/utils/RetrofitHelper.kt index 1d94aa9d..d6b62af1 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/utils/RetrofitHelper.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/utils/RetrofitHelper.kt @@ -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() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 776c27e3..2851b2bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,4 +85,5 @@ Delete playlist and songs Clear Playlist Disable search history + Hyperpipe API URL