Advancement with Couchbase Lite.

This commit is contained in:
Antoine POPINEAU 2020-09-26 17:26:42 +02:00
parent 1126d47a1a
commit 4e3da9160e
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
26 changed files with 415 additions and 171 deletions

View File

@ -7,13 +7,12 @@ plugins {
id("kotlin-android")
id("kotlin-android-extensions")
id("kotlin-kapt")
id("realm-android")
id("org.jlleitschuh.gradle.ktlint") version "8.1.0"
id("com.gladed.androidgitversion") version "0.4.10"
id("com.github.triplet.play") version "2.4.2"
kotlin("plugin.serialization") version "1.3.70"
kotlin("plugin.serialization") version "1.3.72"
}
val props = Properties().apply {
@ -149,13 +148,15 @@ dependencies {
implementation("com.aliassadi:power-preference-lib:1.4.1")
implementation("com.github.kittinunf.fuel:fuel:2.2.3")
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-android:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-gson:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.2.3")
implementation("com.github.kittinunf.fuel:fuel-android:2.2.3")
implementation("com.github.kittinunf.fuel:fuel-kotlinx-serialization:2.2.3")
implementation("com.squareup.picasso:picasso:2.71828")
implementation("jp.wasabeef:picasso-transformations:2.2.1")
implementation("com.couchbase.lite:couchbase-lite-android:2.7.1")
implementation("com.github.MOLO17:couchbase-lite-kotlin:1.0.0")
debugImplementation("com.amitshekhar.android:debug-db:1.0.6")
kapt("androidx.room:room-compiler:2.2.5")

View File

@ -17,7 +17,8 @@
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
android:usesCleartextTraffic="true"
tools:replace="android:label">
<activity
android:name=".activities.SplashActivity"

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.widget.SearchView
import androidx.appcompat.app.AppCompatDelegate
import androidx.room.Room
import com.couchbase.lite.*
import com.github.apognu.otter.activities.MainActivity
import com.github.apognu.otter.activities.SearchActivity
import com.github.apognu.otter.adapters.*
@ -14,10 +15,7 @@ import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.playback.MediaSession
import com.github.apognu.otter.playback.QueueManager.Companion.factory
import com.github.apognu.otter.repositories.*
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.Command
import com.github.apognu.otter.utils.Event
import com.github.apognu.otter.utils.*
import com.github.apognu.otter.viewmodels.*
import com.google.android.exoplayer2.database.ExoDatabaseProvider
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
@ -28,7 +26,6 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.preference.PowerPreference
import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BroadcastChannel
import org.koin.android.ext.koin.androidContext
@ -38,6 +35,7 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
@ -86,7 +84,7 @@ class Otter : Application() {
override fun onCreate() {
super.onCreate()
Realm.init(this)
CouchbaseLite.init(this)
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
@ -107,6 +105,13 @@ class Otter : Application() {
}
}
single {
Database("otter", DatabaseConfiguration()).apply {
createIndex("type", IndexBuilder.valueIndex(ValueIndexItem.expression(Expression.property("type"))))
createIndex("order", IndexBuilder.valueIndex(ValueIndexItem.expression(Expression.property("order"))))
}
}
fragment { BrowseFragment() }
fragment { LandscapeQueueFragment() }
@ -114,7 +119,7 @@ class Otter : Application() {
single { ArtistsRepository(get(), get()) }
factory { (id: Int) -> ArtistTracksRepository(get(), get(), id) }
viewModel { ArtistsViewModel(get(), get()) }
viewModel { ArtistsViewModel(get()) }
factory { (context: Context?, listener: ArtistsFragment.OnArtistClickListener) -> ArtistsAdapter(context, listener) }
factory { (id: Int?) -> AlbumsRepository(get(), get(), id) }
@ -122,7 +127,7 @@ class Otter : Application() {
factory { (context: Context?, adapter: AlbumsAdapter.OnAlbumClickListener) -> AlbumsAdapter(context, adapter) }
factory { (context: Context?, adapter: AlbumsGridAdapter.OnAlbumClickListener) -> AlbumsGridAdapter(context, adapter) }
factory { (id: Int?) -> TracksRepository(get(), get(), id) }
factory { (id: Int?) -> TracksRepository(get(), get(), get(), id) }
viewModel { (id: Int) -> TracksViewModel(get { parametersOf(id) }, get(), id) }
factory { (context: Context?, favoriteListener: TracksAdapter.OnFavoriteListener?) -> TracksAdapter(context, favoriteListener) }
@ -149,8 +154,6 @@ class Otter : Application() {
single { ArtistsSearchRepository(get(), get()) }
single { AlbumsSearchRepository(get(), get { parametersOf(null) }) }
single { TracksSearchRepository(get(), get { parametersOf(null) }) }
single { Mediator(get(), get(), get()) }
})
}
@ -164,6 +167,8 @@ class Otter : Application() {
fun deleteAllData() {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).clear()
filesDir.deleteRecursively()
cacheDir.listFiles()?.forEach {
it.delete()
}

View File

@ -25,10 +25,6 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
fun onClick(holder: View?, artist: Artist)
}
// override fun getItemCount() = data.size
// override fun getItemId(position: Int) = data[position].id.toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)

View File

@ -9,7 +9,7 @@ import com.github.apognu.otter.fragments.*
class BrowseTabsAdapter(val context: Fragment, manager: FragmentManager) : FragmentPagerAdapter(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
var tabs = mutableListOf<Fragment>()
override fun getCount() = 5
override fun getCount() = 1
override fun getItem(position: Int): Fragment {
tabs.getOrNull(position)?.let {

View File

@ -50,7 +50,7 @@ class AlbumsFragment : OtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>() {
companion object {
fun new(artist: Artist, _art: String? = null): AlbumsFragment {
val art = _art ?: if (artist.albums?.isNotEmpty() == true) artist.cover() else ""
val art = _art ?: (artist.cover() ?: "")
return AlbumsFragment().apply {
arguments = bundleOf(

View File

@ -8,10 +8,7 @@ import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.transition.Fade
import androidx.transition.Slide
import com.github.apognu.otter.R
@ -33,7 +30,7 @@ class ArtistsFragment : PagedOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapt
override val adapter by inject<ArtistsAdapter> { parametersOf(context, OnArtistClickListener()) }
override val viewModel by viewModel<ArtistsViewModel>()
override val liveData by lazy { viewModel.artistsPaged }
override val liveData by lazy { viewModel.artists }
override val viewRes = R.layout.fragment_artists
override val recycler: RecyclerView get() = artists

View File

@ -5,12 +5,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.lifecycle.*
import androidx.paging.PagingData
import androidx.paging.PagingDataAdapter
import androidx.paging.map
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
@ -21,11 +19,14 @@ import com.github.apognu.otter.utils.EventBus
import com.github.apognu.otter.utils.log
import com.github.apognu.otter.utils.untilNetwork
import kotlinx.android.synthetic.main.fragment_artists.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import org.koin.ext.scope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
var data: MutableList<D> = mutableListOf()
@ -154,8 +155,6 @@ abstract class OtterFragment<DAO : Any, D : Any, A : OtterAdapter<D, *>> : Fragm
}
abstract class PagedOtterFragment<DAO : Any, D : Any, A : PagingDataAdapter<D, *>> : Fragment() {
open val OFFSCREEN_PAGES = 10
abstract val repository: Repository<DAO>
abstract val adapter: A
open val viewModel: ViewModel? = null
@ -219,15 +218,4 @@ abstract class PagedOtterFragment<DAO : Any, D : Any, A : PagingDataAdapter<D, *
}
}
}
private fun needsMoreOffscreenPages(): Boolean {
view?.let {
val offset = recycler.computeVerticalScrollOffset()
val left = recycler.computeVerticalScrollRange() - recycler.height - offset
return left < (recycler.height * OFFSCREEN_PAGES)
}
return false
}
}

View File

@ -5,24 +5,29 @@ import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.github.apognu.otter.models.dao.DecoratedArtistEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.toDao
import com.couchbase.lite.Database
import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.repositories.ArtistsRepository
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.log
import kotlinx.coroutines.flow.collect
import com.molo17.couchbase.lite.doInBatch
import com.molo17.couchbase.lite.from
import com.molo17.couchbase.lite.select
import com.molo17.couchbase.lite.where
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
@OptIn(ExperimentalPagingApi::class)
class Mediator(private val context: Context, private val database: OtterDatabase, private val repository: ArtistsRepository) : RemoteMediator<Int, DecoratedArtistEntity>(), KoinComponent {
override suspend fun load(loadType: LoadType, state: PagingState<Int, DecoratedArtistEntity>): MediatorResult {
class Mediator(private val context: Context, private val database: Database, private val repository: ArtistsRepository) : RemoteMediator<Int, Artist>(), KoinComponent {
override suspend fun load(loadType: LoadType, state: PagingState<Int, Artist>): MediatorResult {
loadType.log()
return try {
val key = when (loadType) {
LoadType.REFRESH -> 1
@ -33,20 +38,28 @@ class Mediator(private val context: Context, private val database: OtterDatabase
}
}
key.log("fetching page")
val response = withContext(IO) {
repository.fetch((key - 1) * AppContext.PAGE_SIZE).take(1).first()
}
val response = repository.fetch((key - 1) * AppContext.PAGE_SIZE).take(1).first()
database.withTransaction {
database.doInBatch {
if (loadType == LoadType.REFRESH) {
Cache.delete(context, "key")
database.artists().deleteAll()
select("_id")
.from(database)
.where { "type" equalTo "artist" }
.execute()
.forEach { delete(getDocument(it.getString(0))) }
}
Cache.set(context, "key", (key + 1).toString().toByteArray())
response.data.forEach {
database.artists().insert(it.toDao())
FunkwhaleArtist.persist(database, response.data, (key + 1) * 100)
listeners.forEach {
it()
listeners.remove(it)
}
}
@ -55,4 +68,10 @@ class Mediator(private val context: Context, private val database: OtterDatabase
MediatorResult.Error(e)
}
}
private var listeners: MutableList<() -> Unit> = mutableListOf()
fun addListener(listener: () -> Unit) {
listeners.add(listener)
}
}

View File

@ -1,5 +1,8 @@
package com.github.apognu.otter.models.api
import com.couchbase.lite.Database
import com.couchbase.lite.MutableDocument
import com.github.apognu.otter.utils.toCouchbaseArray
import kotlinx.serialization.Serializable
@Serializable
@ -15,6 +18,34 @@ data class FunkwhaleAlbum(
data class Artist(val id: Int, val name: String)
fun cover() = cover?.urls?.original
companion object {
fun persist(database: Database, albums: List<FunkwhaleAlbum>, base: Int? = null) {
albums.forEachIndexed { index, album ->
val doc = database.getDocument("album:${album.id}")?.toMutable() ?: MutableDocument("album:${album.id}")
doc.run {
setString("type", "album")
setInt("id", album.id)
setInt("artist_id", album.artist.id)
setString("artist_name", album.artist.name)
setString("title", album.title)
setString("release_date", album.release_date)
album.cover?.urls?.original?.let { cover ->
setString("cover", cover)
}
base?.let {
setInt("order", base + index)
}
database.save(this)
}
}
}
}
}
@Serializable
@ -22,4 +53,3 @@ data class Covers(val urls: CoverUrls?)
@Serializable
data class CoverUrls(val original: String?)

View File

@ -1,5 +1,8 @@
package com.github.apognu.otter.models.api
import com.couchbase.lite.Database
import com.couchbase.lite.MutableDocument
import com.github.apognu.otter.utils.toCouchbaseArray
import kotlinx.serialization.Serializable
@Serializable
@ -16,4 +19,52 @@ data class FunkwhaleArtist(
val cover: Covers?,
val release_date: String?
)
companion object {
fun persist(database: Database, artists: List<FunkwhaleArtist>, base: Int? = null) {
artists
// .filter { it.albums?.isNotEmpty() ?: false }
.forEachIndexed { index, artist ->
artist.albums?.forEach { album ->
val albumDoc = database.getDocument("album:${album.id}")?.toMutable() ?: MutableDocument("album:${album.id}")
albumDoc.run {
setString("type", "album")
setInt("id", album.id)
setInt("artist_id", artist.id)
setString("artist_name", artist.name)
setString("title", album.title)
setString("release_date", album.release_date)
album.cover?.urls?.original?.let { cover ->
setString("cover", cover)
}
database.save(this)
}
}
val artistDoc = database.getDocument("artist:${artist.id}")?.toMutable() ?: MutableDocument("artist:${artist.id}")
artistDoc.run {
setString("type", "artist")
setInt("id", artist.id)
setString("name", artist.name)
setArray("albums", artist.albums?.map { it.id }.toCouchbaseArray())
artist.albums?.getOrNull(0)?.cover?.urls?.original?.let { cover ->
setString("cover", cover)
}
base?.let {
setInt("order", base + index)
}
database.save(this)
}
}
}
}
}

View File

@ -1,6 +1,9 @@
package com.github.apognu.otter.models.api
import com.couchbase.lite.Database
import com.couchbase.lite.MutableDocument
import com.github.apognu.otter.models.domain.SearchResult
import com.github.apognu.otter.utils.toCouchbaseArray
import com.google.android.exoplayer2.offline.Download
import kotlinx.serialization.ContextualSerialization
import kotlinx.serialization.Serializable
@ -30,6 +33,63 @@ data class FunkwhaleTrack(
album = FunkwhaleAlbum(0, FunkwhaleAlbum.Artist(0, ""), "", Covers(CoverUrls("")), ""),
uploads = listOf(FunkwhaleUpload(download.contentId, 0, 0))
)
fun persist(database: Database, tracks: List<FunkwhaleTrack>, base: Int? = null) {
tracks.forEachIndexed { index, track ->
val artistDoc = database.getDocument("artist:${track.artist.id}")?.toMutable() ?: MutableDocument("artist:${track.artist.id}")
artistDoc.run {
setString("type", "artist")
setInt("id", track.artist.id)
setString("name", track.artist.name)
database.save(this)
}
track.album?.let { album ->
val albumDoc = database.getDocument("album:${album.id}")?.toMutable() ?: MutableDocument("album:${album.id}")
albumDoc.run {
setString("type", "album")
setInt("id", album.id)
setInt("artist_id", album.artist.id)
setString("artist_name", album.artist.name)
setString("title", album.title)
setString("release_date", album.release_date)
album.cover?.urls?.original?.let { cover ->
setString("cover", cover)
}
database.save(this)
}
}
val doc = database.getDocument("track:${track.id}")?.toMutable() ?: MutableDocument("track:${track.id}")
doc.run {
setString("type", "track")
setInt("id", track.id)
track.album?.let { album ->
setInt("albumId", album.id)
}
setInt("artistId", track.artist.id)
setString("title", track.title)
setInt("position", track.position)
setInt("disc_number", track.disc_number ?: 0)
setString("copyright", track.copyright)
setString("license", track.license)
setString("uploads", track.uploads.getOrNull(0)?.listen_url ?: "")
database.save(this)
}
}
}
}
@Serializable

View File

@ -4,9 +4,6 @@ import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.room.*
import com.github.apognu.otter.models.api.FunkwhaleArtist
import io.realm.RealmObject
import io.realm.annotations.Required
@Entity(tableName = "artists")
data class ArtistEntity(
@PrimaryKey
@ -59,14 +56,3 @@ data class DecoratedArtistEntity(
val album_count: Int,
val album_cover: String?
)
open class RealmArtist(
@io.realm.annotations.PrimaryKey
var id: Int = 0,
@Required
var name: String = ""
) : RealmObject()
fun FunkwhaleArtist.toRealmDao() = run {
RealmArtist(id, name)
}

View File

@ -1,5 +1,6 @@
package com.github.apognu.otter.models.domain
import com.couchbase.lite.Result
import com.github.apognu.otter.models.dao.DecoratedAlbumEntity
data class Album(
@ -12,6 +13,17 @@ data class Album(
): SearchResult {
companion object {
fun from(album: Result) = album.getDictionary(0).run {
Album(
getInt("id"),
getString("title") ?: "N/A",
0,
getString("cover"),
getString("release_date"),
getString("artist_name") ?: "N/A"
)
}
fun fromDecoratedEntity(entity: DecoratedAlbumEntity): Album = entity.run {
Album(
id,

View File

@ -1,5 +1,6 @@
package com.github.apognu.otter.models.domain
import com.couchbase.lite.Result
import com.github.apognu.otter.models.dao.DecoratedArtistEntity
data class Artist(
@ -11,6 +12,15 @@ data class Artist(
) : SearchResult {
companion object {
fun from(artist: Result) = artist.getDictionary(0).run {
Artist(
getInt("id"),
getString("name") ?: "N/A",
getArray("albums")?.count() ?: 0,
getString("cover")
)
}
fun fromDecoratedEntity(entity: DecoratedArtistEntity): Artist = entity.run {
Artist(
id,

View File

@ -1,5 +1,6 @@
package com.github.apognu.otter.models.domain
import com.couchbase.lite.Result
import com.github.apognu.otter.models.dao.DecoratedTrackEntity
import com.preference.PowerPreference
@ -23,6 +24,17 @@ data class Track(
) : SearchResult {
companion object {
fun from(track: Result) = track.getDictionary(0).run {
Track(
getInt("id"),
getString("title") ?: "N/A",
getInt("position"),
getString("copyright"),
getString("license"),
false
)
}
fun fromDecoratedEntity(entity: DecoratedTrackEntity) = entity.run {
Track(
id,

View File

@ -2,15 +2,16 @@ package com.github.apognu.otter.repositories
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import com.couchbase.lite.*
import com.github.apognu.otter.models.api.FunkwhaleAlbum
import com.github.apognu.otter.models.dao.DecoratedAlbumEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.toDao
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import com.molo17.couchbase.lite.*
import kotlinx.coroutines.GlobalScope
class AlbumsRepository(override val context: Context, private val database: OtterDatabase, artistId: Int?) : Repository<FunkwhaleAlbum>() {
class AlbumsRepository(override val context: Context, private val couch: Database, artistId: Int?) : Repository<FunkwhaleAlbum>() {
override val upstream: Upstream<FunkwhaleAlbum> by lazy {
val url =
if (artistId == null) "/api/v1/albums/?playable=true&ordering=title"
@ -24,22 +25,36 @@ class AlbumsRepository(override val context: Context, private val database: Otte
}
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
data.forEach {
insert(it)
}
FunkwhaleAlbum.persist(couch, data)
return super.onDataFetched(data)
}
fun insert(album: FunkwhaleAlbum) = database.albums().insert(album.toDao())
fun all() = database.albums().allDecorated()
fun find(ids: List<Int>) = database.albums().findAllDecorated(ids)
fun insert(albums: List<FunkwhaleAlbum>) = FunkwhaleAlbum.persist(couch, albums)
fun ofArtist(id: Int): LiveData<List<DecoratedAlbumEntity>> {
fun all() =
select(SelectResult.all())
.from(couch)
.where { "type" equalTo "album"}
.asFlow()
.asLiveData()
fun find(ids: List<Int>) =
select(SelectResult.all())
.from(couch)
.where { ("type" equalTo "album") and (Meta.id.`in`(*ids.map { Expression.string("album:$it") }.toTypedArray())) }
.asFlow()
.asLiveData(GlobalScope.coroutineContext)
fun ofArtist(id: Int): LiveData<ResultSet> {
scope.launch(Dispatchers.IO) {
fetch().collect()
}
return database.albums().forArtistDecorated(id)
return select(SelectResult.all())
.from(couch)
.where { ("type" equalTo "album") and ("artist_id" equalTo id) }
.asFlow()
.asLiveData(GlobalScope.coroutineContext)
}
}

View File

@ -1,51 +1,64 @@
package com.github.apognu.otter.repositories
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.paging.Pager
import androidx.paging.PagingConfig
import com.couchbase.lite.Database
import com.couchbase.lite.Expression
import com.couchbase.lite.Meta
import com.github.apognu.otter.models.Mediator
import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.dao.DecoratedArtistEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.toDao
import com.github.apognu.otter.models.dao.toRealmDao
import io.realm.Realm
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.viewmodels.CouchbasePagingSource
import com.molo17.couchbase.lite.*
import kotlinx.coroutines.GlobalScope
class ArtistsRepository(override val context: Context, private val database: Database) : Repository<FunkwhaleArtist>() {
private val mediator = Mediator(context, database, this)
class ArtistsRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhaleArtist>() {
override val upstream =
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=id", FunkwhaleArtist.serializer())
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", FunkwhaleArtist.serializer())
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
scope.launch(IO) {
data.forEach { artist ->
database.artists().insert(artist.toDao())
Realm.getDefaultInstance().executeTransaction { realm ->
realm.insertOrUpdate(artist.toRealmDao())
}
artist.albums?.forEach { album ->
database.albums().insert(album.toDao(artist.id))
val pager = Pager(
// config = PagingConfig(pageSize = AppContext.PAGE_SIZE, initialLoadSize = AppContext.PAGE_SIZE * 5, prefetchDistance = 10 * AppContext.PAGE_SIZE, maxSize = 25 * AppContext.PAGE_SIZE, enablePlaceholders = false),
config = PagingConfig(pageSize = AppContext.PAGE_SIZE, initialLoadSize = 10, prefetchDistance = 2),
pagingSourceFactory = {
CouchbasePagingSource(this).apply {
mediator.addListener {
invalidate()
}
}
}
},
remoteMediator = mediator
)
return super.onDataFetched(data)
}
fun insert(artists: List<FunkwhaleArtist>) = FunkwhaleArtist.persist(database, artists)
fun insert(artist: FunkwhaleArtist) = database.artists().insert(artist.toDao())
fun all(page: Int) =
select(all())
.from(database)
.where { "type" equalTo "artist" }
.orderBy { "order".ascending() }
.limit(AppContext.PAGE_SIZE, AppContext.PAGE_SIZE * page)
.execute()
fun allPaged() = database.artists().allPaged()
fun get(id: Int) =
select(all())
.from(database)
.where {
("type" equalTo "artist") and ("_id" equalTo "artist:$id")
}
.limit(1)
.execute()
.next()
.run { Artist.from(this) }
fun all(): LiveData<List<DecoratedArtistEntity>> {
scope.launch(IO) {
fetch().collect()
}
return database.artists().allDecorated()
}
fun get(id: Int) = database.artists().getDecorated(id)
fun find(ids: List<Int>) = database.artists().findDecorated(ids)
fun find(ids: List<Int>) =
select(all())
.from(database)
.where { ("type" equalTo "artist") and (Meta.id.`in`(*ids.map { Expression.string("artist:$it") }.toTypedArray())) }
.asFlow()
.asLiveData(GlobalScope.coroutineContext)
}

View File

@ -8,6 +8,7 @@ import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.core.FuelError
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
import com.github.kittinunf.fuel.coroutines.awaitStringResponseResult
import com.github.kittinunf.result.Result
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.*
@ -49,8 +50,6 @@ class HttpUpstream<D : Any>(val behavior: Behavior, private val url: String, pri
}
},
{ error ->
error.log()
when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut)
else -> emit(Repository.Response(listOf(), page, false))
@ -67,13 +66,18 @@ class HttpUpstream<D : Any>(val behavior: Behavior, private val url: String, pri
}
}
val (_, response, result) = request.awaitObjectResponseResult(AppContext.deserializer(OtterResponseSerializer(serializer)))
val (_, response, result) = request.awaitStringResponseResult()
val items = AppContext.deserializer(OtterResponseSerializer(serializer)).deserialize(result.get())
if (response.statusCode == 401) {
return retryGet(url)
}
result
items?.let {
return Result.success(items)
}
Result.error(FuelError.wrap(Exception("")))
} catch (e: Exception) {
Result.error(FuelError.wrap(e))
}

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.map
import com.couchbase.lite.Database
import com.github.apognu.otter.models.api.FunkwhaleAlbum
import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.api.FunkwhaleTrack
@ -24,13 +25,13 @@ class ArtistsSearchRepository(override val context: Context?, private val reposi
private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
val results: LiveData<List<Artist>> = Transformations.switchMap(_ids) {
repository.find(it).map { artists -> artists.map { artist -> Artist.fromDecoratedEntity(artist) } }
repository.find(it).map { result ->
result.map { artist -> Artist.from(artist) }
}
}
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
data.forEach {
repository.insert(it)
}
repository.insert(data)
ids.addAll(data.map { it.id })
_ids.postValue(ids)
@ -57,13 +58,13 @@ class AlbumsSearchRepository(override val context: Context?, private val reposit
private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
val results: LiveData<List<Album>> = Transformations.switchMap(_ids) {
repository.find(it).map { albums -> albums.map { album -> Album.fromDecoratedEntity(album) } }
repository.find(it).map { result ->
result.map { album -> Album.from(album) }
}
}
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
data.forEach {
repository.insert(it)
}
repository.insert(data)
ids.addAll(data.map { it.id })
_ids.postValue(ids)
@ -91,13 +92,13 @@ class TracksSearchRepository(override val context: Context?, private val reposit
private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
val results: LiveData<List<Track>> = Transformations.switchMap(_ids) {
repository.find(it).map { tracks -> tracks.map { track -> Track.fromDecoratedEntity(track) } }
repository.find(it).map { result ->
result.map { track -> Track.from(track) }
}
}
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> {
data.forEach {
repository.insert(it)
}
repository.insert(data)
ids.addAll(data.map { it.id })
_ids.postValue(ids)

View File

@ -2,11 +2,13 @@ package com.github.apognu.otter.repositories
import android.content.Context
import androidx.lifecycle.LiveData
import com.couchbase.lite.*
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.api.FunkwhaleTrack
import com.github.apognu.otter.models.dao.DecoratedTrackEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.utils.asLiveData
import com.github.apognu.otter.utils.getMetadata
import com.github.apognu.otter.utils.maybeNormalizeUrl
import com.google.android.exoplayer2.offline.Download
@ -15,7 +17,7 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class TracksRepository(override val context: Context, private val database: OtterDatabase, albumId: Int?) : Repository<FunkwhaleTrack>() {
class TracksRepository(override val context: Context, private val database: OtterDatabase, private val couch: Database, albumId: Int?) : Repository<FunkwhaleTrack>() {
override val upstream =
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", FunkwhaleTrack.serializer())
@ -39,18 +41,21 @@ class TracksRepository(override val context: Context, private val database: Otte
}
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
data.forEach { track ->
database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), track)
}
FunkwhaleTrack.persist(couch, data)
data.sortedWith(compareBy({ it.disc_number }, { it.position }))
}
fun insert(track: FunkwhaleTrack) {
database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), track)
}
fun insert(tracks: List<FunkwhaleTrack>) = FunkwhaleTrack.persist(couch, tracks)
fun find(ids: List<Int>) = database.tracks().findAllDecorated(ids)
fun find(ids: List<Int>) = QueryBuilder
.select(SelectResult.all())
.from(DataSource.database(couch))
.where(
Expression.property("type").equalTo(Expression.string("track"))
.and(Meta.id.`in`(*ids.map { Expression.string("track:$it") }.toTypedArray()))
)
.asLiveData()
suspend fun ofArtistBlocking(id: Int) = database.tracks().ofArtistBlocking(id)

View File

@ -2,6 +2,9 @@ package com.github.apognu.otter.utils
import android.os.Build
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import com.couchbase.lite.*
import com.couchbase.lite.Array
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.BrowseFragment
import com.github.apognu.otter.models.api.DownloadInfo
@ -77,3 +80,32 @@ fun Request.authorize(): Request {
}
fun Download.getMetadata(): DownloadInfo? = AppContext.json.parse(DownloadInfo.serializer(), String(this.request.data))
class DocumentSetLiveData(val query: Query) : LiveData<List<Result>>() {
private var token: ListenerToken? = null
override fun onActive() {
token = query.addChangeListener {
postValue(query.execute().allResults())
}
query.execute()
}
override fun onInactive() {
token?.let {
query.removeChangeListener(it)
token = null
}
}
}
fun Query.asLiveData() = DocumentSetLiveData(this)
fun <T : Any> List<T>?.toCouchbaseArray(): Array {
this?.let {
return MutableArray(this)
}
return MutableArray(listOf())
}

View File

@ -12,11 +12,11 @@ class AlbumsViewModel(private val repository: AlbumsRepository, private val trac
val albums: LiveData<List<Album>> by lazy {
if (artistId == null) {
Transformations.map(repository.all()) {
it.map { album -> Album.fromDecoratedEntity(album) }
it.map { result -> Album.from(result) }
}
} else {
Transformations.map(repository.ofArtist(artistId)) {
it.map { album -> Album.fromDecoratedEntity(album) }
it.map { result -> Album.from(result) }
}
}
}

View File

@ -1,30 +1,37 @@
package com.github.apognu.otter.viewmodels
import androidx.lifecycle.*
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingSource
import androidx.paging.cachedIn
import androidx.paging.map
import com.github.apognu.otter.models.Mediator
import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.repositories.ArtistsRepository
import com.github.apognu.otter.utils.AppContext
import kotlinx.coroutines.flow.map
class ArtistsViewModel(repository: ArtistsRepository, mediator: Mediator) : ViewModel() {
private val pager = Pager(
config = PagingConfig(pageSize = AppContext.PAGE_SIZE, initialLoadSize = AppContext.PAGE_SIZE * 5, prefetchDistance = 10 * AppContext.PAGE_SIZE, maxSize = 25 * AppContext.PAGE_SIZE, enablePlaceholders = false),
pagingSourceFactory = repository.allPaged().asPagingSourceFactory(),
remoteMediator = mediator
)
class CouchbasePagingSource(val repository: ArtistsRepository) : PagingSource<Int, Artist>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Artist> {
return try {
val page = params.key ?: 0
val artistsPaged = pager
.flow
.map { artists -> artists.map { Artist.fromDecoratedEntity(it) } }
.cachedIn(viewModelScope)
.asLiveData()
val artists = repository.all(page).map { Artist.from(it) }
val prevKey = if (page > 0) page - 1 else null
val nextKey = if (artists.isNotEmpty() && artists.size == AppContext.PAGE_SIZE) page + 1 else null
val artists: LiveData<List<Artist>> = repository.all().map { artists ->
artists.map { Artist.fromDecoratedEntity(it) }
LoadResult.Page(
data = artists,
prevKey = prevKey,
nextKey = nextKey
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
class ArtistsViewModel(repository: ArtistsRepository) : ViewModel() {
val artists = repository.pager
.flow
.cachedIn(viewModelScope)
.asLiveData(viewModelScope.coroutineContext)
}

View File

@ -30,7 +30,7 @@ class FavoritesViewModel(private val repository: FavoritesRepository, private va
val ids = it.map { favorite -> favorite.track_id }
Transformations.map(tracksRepository.find(ids)) { tracks ->
tracks.map { track -> Track.fromDecoratedEntity(track) }.sortedBy { it.title }
tracks.map { track -> Track.from(track) }.sortedBy { it.title }
}
}
}

View File

@ -5,9 +5,8 @@ buildscript {
}
dependencies {
classpath("com.android.tools.build:gradle:4.0.0")
classpath("com.android.tools.build:gradle:4.0.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72")
classpath("io.realm:realm-gradle-plugin:10.0.0-BETA.6")
}
}