Skip to content

Commit

Permalink
Add support to listen to cached songs, very experimental
Browse files Browse the repository at this point in the history
  • Loading branch information
brahmkshatriya committed Oct 11, 2024
1 parent 823ae8f commit be26dd3
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package dev.brahmkshatriya.echo.di

import android.app.Application
import android.content.SharedPreferences
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.cache.SimpleCache
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -21,9 +24,11 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
class ExtensionModule {

@OptIn(UnstableApi::class)
@Provides
@Singleton
fun provideOfflineExtension(context: Application) = OfflineExtension(context)
fun provideOfflineExtension(context: Application, cache: SimpleCache) =
OfflineExtension(context, cache)

@Provides
@Singleton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dev.brahmkshatriya.echo.offline

import android.content.Context
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.cache.SimpleCache
import dev.brahmkshatriya.echo.R
import dev.brahmkshatriya.echo.common.clients.AlbumClient
import dev.brahmkshatriya.echo.common.clients.ArtistClient
Expand Down Expand Up @@ -45,14 +48,19 @@ import dev.brahmkshatriya.echo.offline.MediaStoreUtils.editPlaylist
import dev.brahmkshatriya.echo.offline.MediaStoreUtils.moveSongInPlaylist
import dev.brahmkshatriya.echo.offline.MediaStoreUtils.removeSongFromPlaylist
import dev.brahmkshatriya.echo.offline.MediaStoreUtils.searchBy
import dev.brahmkshatriya.echo.playback.MediaItemUtils.toIdAndIsVideo
import dev.brahmkshatriya.echo.utils.getFromCache
import dev.brahmkshatriya.echo.utils.saveToCache
import dev.brahmkshatriya.echo.utils.toData
import dev.brahmkshatriya.echo.utils.toJson

class OfflineExtension(val context: Context) : ExtensionClient, HomeFeedClient, TrackClient,
AlbumClient, ArtistClient, PlaylistClient, RadioClient, SearchClient, LibraryClient,
TrackLikeClient, PlaylistEditorListenerClient, SettingsChangeListenerClient {
@OptIn(UnstableApi::class)
class OfflineExtension(
val context: Context,
val cache: SimpleCache
) : ExtensionClient, HomeFeedClient, TrackClient, AlbumClient, ArtistClient, PlaylistClient,
RadioClient, SearchClient, LibraryClient, TrackLikeClient, PlaylistEditorListenerClient,
SettingsChangeListenerClient {

companion object {
val metadata = Metadata(
Expand Down Expand Up @@ -107,8 +115,15 @@ class OfflineExtension(val context: Context) : ExtensionClient, HomeFeedClient,
var library = MediaStoreUtils.getAllSongs(context, settings)
private fun refreshLibrary() {
library = MediaStoreUtils.getAllSongs(context, settings)
cachedTracks = getCachedTracks()
}

@OptIn(UnstableApi::class)
private fun getCachedTracks() = cache.keys.mapNotNull { key ->
val (id, _) = key.toIdAndIsVideo() ?: return@mapNotNull null
context.getFromCache<Pair<String, Track>>(id, "track")
}.reversed()

override fun setSettings(settings: Settings) {}

private fun find(artist: Artist) =
Expand Down Expand Up @@ -237,10 +252,11 @@ class OfflineExtension(val context: Context) : ExtensionClient, HomeFeedClient,
}

override suspend fun loadPlaylist(playlist: Playlist) =
find(playlist)!!.toPlaylist()
if (playlist.id == "cached") playlist else find(playlist)!!.toPlaylist()

override fun loadTracks(playlist: Playlist): PagedData<Track> = PagedData.Single {
find(playlist)!!.songList.map { it }
if (playlist.id == "cached") cachedTracks.map { it.second }
else find(playlist)!!.songList.map { it }
}

override fun getShelves(playlist: Playlist) = PagedData.Single<Shelf> {
Expand Down Expand Up @@ -378,15 +394,24 @@ class OfflineExtension(val context: Context) : ExtensionClient, HomeFeedClient,
"Playlists", "Folders"
).map { Tab(it, it) }

private var cachedTracks = listOf<Pair<String, Track>>()
override fun getLibraryFeed(tab: Tab?): PagedData<Shelf> {
if (refreshLibrary) refreshLibrary()
return when (tab?.id) {
"Folders" -> library.folderStructure.folderList.entries.first().value
.toShelf(context, null).items!!

else -> {
library.playlistList.map { it.toPlaylist().toMediaItem().toShelf() }
.toPaged()
val cached = if (cachedTracks.isNotEmpty()) Playlist(
id = "cached",
title = context.getString(R.string.cached_songs),
isEditable = false,
cover = cachedTracks.first().second.cover,
description = context.getString(R.string.cache_playlist_warning),
tracks = cachedTracks.size
).toMediaItem().toShelf() else null
val playlists = library.playlistList.map { it.toPlaylist().toMediaItem().toShelf() }
(listOfNotNull(cached) + playlists).toPaged()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dev.brahmkshatriya.echo.playback

import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import androidx.core.net.toUri
import androidx.core.os.bundleOf
Expand Down Expand Up @@ -79,9 +78,8 @@ object MediaItemUtils {
item.build()
}

fun Uri.toIdAndIsVideo() = runCatching {
val string = toString()
if (string.startsWith('{')) string.toData<Pair<String, Boolean>>()
fun String.toIdAndIsVideo() = runCatching {
if (startsWith('{')) toData<Pair<String, Boolean>>()
else null
}.getOrNull()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ class PlayerEventListener(

private fun updateCurrentFlow() {
currentFlow.value = player.currentMediaItem?.let {
Current(player.currentMediaItemIndex, it, it.isLoaded, player.isPlaying)
val isPlaying = player.isPlaying && player.playbackState == Player.STATE_READY
Current(player.currentMediaItemIndex, it, it.isLoaded, isPlaying)
}
}

Expand All @@ -75,6 +76,11 @@ class PlayerEventListener(
updateCustomLayout()
}

override fun onPlaybackStateChanged(playbackState: Int) {
updateCurrentFlow()
updateCustomLayout()
}

override fun onIsPlayingChanged(isPlaying: Boolean) {
updateCurrentFlow()
updateCurrent()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dev.brahmkshatriya.echo.playback.source

import android.net.Uri
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.BaseDataSource
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.SimpleCache
import dev.brahmkshatriya.echo.playback.source.MediaResolver.Companion.LOCAL

@OptIn(UnstableApi::class)
class CustomCacheDataSource(
cache: SimpleCache,
private val upstream: DataSource.Factory
) : BaseDataSource(true) {

class Factory(
private val cache: SimpleCache,
private val upstream: DataSource.Factory
) : DataSource.Factory {
override fun createDataSource() = CustomCacheDataSource(cache, upstream)
}

private val cacheFactory = CacheDataSource
.Factory().setCache(cache)
.setUpstreamDataSourceFactory(upstream)

var source: DataSource? = null
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
return source?.read(buffer, offset, length) ?: throw Exception("Source not opened")
}

override fun open(dataSpec: DataSpec): Long {
val source = if (dataSpec.uri == LOCAL) upstream.createDataSource()
else cacheFactory.createDataSource()
this.source = source
return source.open(dataSpec)
}

override fun getUri(): Uri? {
return source?.uri
}

override fun close() {
source?.close()
source = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.ResolvingDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
Expand Down Expand Up @@ -50,9 +49,7 @@ class MediaFactory(

private val mediaResolver = MediaResolver(context, extListFlow)
private val dataSource = ResolvingDataSource.Factory(
CacheDataSource
.Factory().setCache(cache)
.setUpstreamDataSourceFactory(MediaDataSource.Factory(context)),
CustomCacheDataSource.Factory(cache, MediaDataSource.Factory(context)),
mediaResolver
)
private val default = lazily { DefaultMediaSourceFactory(dataSource) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import dev.brahmkshatriya.echo.R
import dev.brahmkshatriya.echo.common.MusicExtension
import dev.brahmkshatriya.echo.common.models.Streamable
import dev.brahmkshatriya.echo.offline.OfflineExtension
import dev.brahmkshatriya.echo.playback.MediaItemUtils.audioIndex
import dev.brahmkshatriya.echo.playback.MediaItemUtils.clientId
import dev.brahmkshatriya.echo.playback.MediaItemUtils.toIdAndIsVideo
import dev.brahmkshatriya.echo.playback.MediaItemUtils.track
import dev.brahmkshatriya.echo.playback.MediaItemUtils.video
import dev.brahmkshatriya.echo.playback.source.DelayedSource.Companion.getMediaItemById
import dev.brahmkshatriya.echo.playback.source.DelayedSource.Companion.getTrackClient
import dev.brahmkshatriya.echo.playback.source.MediaDataSource.Companion.copy
import dev.brahmkshatriya.echo.utils.saveToCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
Expand All @@ -36,7 +39,7 @@ class MediaResolver(

@UnstableApi
override fun resolveDataSpec(dataSpec: DataSpec): DataSpec {
val (id, isVideo) = dataSpec.uri.toIdAndIsVideo() ?: return dataSpec
val (id, isVideo) = dataSpec.uri.toString().toIdAndIsVideo() ?: return dataSpec

val (_, mediaItem) = runBlocking(Dispatchers.Main) {
player.getMediaItemById(id)
Expand All @@ -45,8 +48,16 @@ class MediaResolver(
val streamable = if (isVideo) mediaItem.video!! else
runBlocking(Dispatchers.IO) { runCatching { loadAudio(mediaItem) } }.getOrThrow()

val uri = if (mediaItem.clientId == OfflineExtension.metadata.id) LOCAL
else {
if(!isVideo) {
val track = mediaItem.track
context.saveToCache(track.id, mediaItem.clientId to track, "track")
}
dataSpec.uri
}
return dataSpec.copy(
uri = streamable.hashCode().toString().toUri(),
uri = uri,
customData = streamable
)
}
Expand All @@ -66,6 +77,8 @@ class MediaResolver(

companion object {

val LOCAL = "local".toUri()

@OptIn(UnstableApi::class)
fun getPlayer(
context: Context, cache: SimpleCache, video: Streamable.Media.WithVideo.Only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import dev.brahmkshatriya.echo.ui.adapter.MediaItemViewHolder.Companion.bind
import dev.brahmkshatriya.echo.ui.adapter.MediaItemViewHolder.Companion.icon
import dev.brahmkshatriya.echo.ui.adapter.MediaItemViewHolder.Companion.placeHolder
import dev.brahmkshatriya.echo.ui.adapter.MediaItemViewHolder.Companion.toolTipOnClick
import dev.brahmkshatriya.echo.utils.animateVisibility
import dev.brahmkshatriya.echo.utils.loadInto
import dev.brahmkshatriya.echo.utils.observe

Expand Down Expand Up @@ -64,7 +65,7 @@ class GridViewHolder(
binding.isPlaying.toolTipOnClick()
observe(listener.current) {
val playing = it.isPlaying(media.id)
binding.isPlaying.isVisible = playing
binding.isPlaying.animateVisibility(playing)
if (playing) (binding.isPlaying.icon as Animatable).start()
}
media
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import dev.brahmkshatriya.echo.databinding.NewItemMediaProfileBinding
import dev.brahmkshatriya.echo.databinding.NewItemMediaTitleBinding
import dev.brahmkshatriya.echo.databinding.NewItemMediaTrackBinding
import dev.brahmkshatriya.echo.playback.Current.Companion.isPlaying
import dev.brahmkshatriya.echo.utils.animateVisibility
import dev.brahmkshatriya.echo.utils.loadInto
import dev.brahmkshatriya.echo.utils.loadWith
import dev.brahmkshatriya.echo.utils.observe
Expand Down Expand Up @@ -173,7 +174,7 @@ sealed class MediaItemViewHolder(
this.icon.setImageResource(item.icon())
isPlaying.toolTipOnClick()
return { playing: Boolean ->
isPlaying.isVisible = playing
isPlaying.animateVisibility(playing)
if (playing) (isPlaying.icon as Animatable).start()
}
}
Expand All @@ -193,7 +194,7 @@ sealed class MediaItemViewHolder(
albumImage(item.size, listImageContainer1, listImageContainer2)
isPlaying.toolTipOnClick()
return { playing: Boolean ->
isPlaying.isVisible = playing
isPlaying.animateVisibility(playing)
if (playing) (isPlaying.icon as Animatable).start()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import dev.brahmkshatriya.echo.R
import dev.brahmkshatriya.echo.common.models.EchoMediaItem
import dev.brahmkshatriya.echo.common.models.Shelf
import dev.brahmkshatriya.echo.databinding.ItemTrackBinding
import dev.brahmkshatriya.echo.playback.Current.Companion.isPlaying
import dev.brahmkshatriya.echo.ui.adapter.MediaItemViewHolder.Companion.toolTipOnClick
import dev.brahmkshatriya.echo.ui.item.TrackAdapter
import dev.brahmkshatriya.echo.utils.animateVisibility
import dev.brahmkshatriya.echo.utils.loadInto
import dev.brahmkshatriya.echo.utils.observe
import dev.brahmkshatriya.echo.utils.toTimeString
Expand Down Expand Up @@ -53,8 +55,8 @@ class TrackViewHolder(
}
binding.isPlaying.toolTipOnClick()
observe(listener.current) {
val playing = it?.mediaItem?.mediaId == track.id
binding.isPlaying.isVisible = playing
val playing = it.isPlaying(track.id)
binding.isPlaying.animateVisibility(playing)
if(playing) (binding.isPlaying.icon as Animatable).start()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ fun NavigationBarView.animateTranslation(
}
}

fun View.animateVisibility(visible: Boolean, animate: Boolean) {
fun View.animateVisibility(visible: Boolean, animate: Boolean = true) {
if (animations && animate && isVisible != visible) {
isVisible = true
startAnimation(
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@
<string name="unhide">Unhide</string>
<string name="hidden_track">Hidden %1$s</string>
<string name="unhidden_track">Unhidden %1$s</string>
<string name="cache_playlist_warning">Experimental Feature.\nAnything except listening to them is not supported.</string>
<string name="cached_songs">Cached Songs</string>
<string-array name="stream_qualities">
<item>Highest</item>
<item>Medium</item>
Expand Down

0 comments on commit be26dd3

Please sign in to comment.