package audio.funkwhale.ffa.playback import android.content.Context import android.net.Uri import audio.funkwhale.ffa.FFA import audio.funkwhale.ffa.R import audio.funkwhale.ffa.utils.Cache import audio.funkwhale.ffa.utils.Command import audio.funkwhale.ffa.utils.CommandBus import audio.funkwhale.ffa.utils.Event import audio.funkwhale.ffa.utils.EventBus import audio.funkwhale.ffa.utils.OAuth import audio.funkwhale.ffa.utils.QueueCache import audio.funkwhale.ffa.utils.Settings import audio.funkwhale.ffa.utils.Track import audio.funkwhale.ffa.utils.mustNormalizeUrl import com.github.kittinunf.fuel.gson.gsonDeserializerOf import com.google.android.exoplayer2.source.ConcatenatingMediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory import com.google.android.exoplayer2.upstream.FileDataSource import com.google.android.exoplayer2.upstream.cache.CacheDataSource import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory import com.google.android.exoplayer2.util.Util import com.google.gson.Gson class QueueManager(val context: Context) { var metadata: MutableList = mutableListOf() val datasources = ConcatenatingMediaSource() var current = -1 companion object { fun factory(context: Context): CacheDataSourceFactory { val http = DefaultHttpDataSourceFactory( Util.getUserAgent(context, context.getString(R.string.app_name)) ) .apply { defaultRequestProperties.apply { if (!Settings.isAnonymous()) { set("Authorization", "Bearer ${OAuth.state().accessToken}") } } } val playbackCache = CacheDataSourceFactory(FFA.get().exoCache, OAuth2DatasourceFactory(context, http)) return CacheDataSourceFactory( FFA.get().exoDownloadCache, playbackCache, FileDataSource.Factory(), null, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, null ) } } init { Cache.get(context, "queue")?.let { json -> gsonDeserializerOf(QueueCache::class.java).deserialize(json)?.let { cache -> metadata = cache.data.toMutableList() val factory = factory(context) datasources.addMediaSources(metadata.map { track -> val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "") ProgressiveMediaSource.Factory(factory).setTag(track.title) .createMediaSource(Uri.parse(url)) }) } } Cache.get(context, "current")?.let { string -> current = string.readLine().toInt() } } private fun persist() { Cache.set( context, "queue", Gson().toJson(QueueCache(metadata)).toByteArray() ) } fun replace(tracks: List) { val factory = factory(context) val sources = tracks.map { track -> val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "") ProgressiveMediaSource.Factory(factory).setTag(track.title).createMediaSource(Uri.parse(url)) } metadata = tracks.toMutableList() datasources.clear() datasources.addMediaSources(sources) persist() EventBus.send(Event.QueueChanged) } fun append(tracks: List) { val factory = factory(context) val missingTracks = tracks.filter { metadata.indexOf(it) == -1 } val sources = missingTracks.map { track -> val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "") ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url)) } metadata.addAll(tracks) datasources.addMediaSources(sources) persist() EventBus.send(Event.QueueChanged) } fun insertNext(track: Track) { val factory = factory(context) val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "") if (metadata.indexOf(track) == -1) { ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url)).let { datasources.addMediaSource(current + 1, it) metadata.add(current + 1, track) } } else { move(metadata.indexOf(track), current + 1) } persist() EventBus.send(Event.QueueChanged) } fun remove(track: Track) { metadata.indexOf(track).let { if (it < 0) { return } datasources.removeMediaSource(it) metadata.removeAt(it) if (it == current) { CommandBus.send(Command.NextTrack) } if (it < current) { current-- } } if (metadata.isEmpty()) { current = -1 } persist() EventBus.send(Event.QueueChanged) } fun move(oldPosition: Int, newPosition: Int) { datasources.moveMediaSource(oldPosition, newPosition) metadata.add(newPosition, metadata.removeAt(oldPosition)) persist() } fun get() = metadata.mapIndexed { index, track -> track.current = index == current track } fun get(index: Int): Track = metadata[index] fun current(): Track? { if (current == -1) { return metadata.getOrNull(0) } return metadata.getOrNull(current) } fun clear() { metadata = mutableListOf() datasources.clear() current = -1 persist() } fun shuffle() { if (metadata.size < 2) return if (current == -1) { replace(metadata.shuffled()) } else { move(current, 0) current = 0 val shuffled = metadata .drop(1) .shuffled() while (metadata.size > 1) { datasources.removeMediaSource(metadata.size - 1) metadata.removeAt(metadata.size - 1) } append(shuffled) } persist() EventBus.send(Event.QueueChanged) } }