diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2b18952..842e60c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e44ec50..ae47dca 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> 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() } diff --git a/app/src/main/java/com/github/apognu/otter/adapters/ArtistsAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/ArtistsAdapter.kt index d4965ba..bd2d682 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/ArtistsAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/ArtistsAdapter.kt @@ -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) diff --git a/app/src/main/java/com/github/apognu/otter/adapters/BrowseTabsAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/BrowseTabsAdapter.kt index 3f5fe75..975f2ad 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/BrowseTabsAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/BrowseTabsAdapter.kt @@ -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() - override fun getCount() = 5 + override fun getCount() = 1 override fun getItem(position: Int): Fragment { tabs.getOrNull(position)?.let { diff --git a/app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt index 784fa8d..c37fca0 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt @@ -50,7 +50,7 @@ class AlbumsFragment : OtterFragment() { 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( diff --git a/app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt index fef5ad1..dbba14c 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt @@ -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 { parametersOf(context, OnArtistClickListener()) } override val viewModel by viewModel() - 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 diff --git a/app/src/main/java/com/github/apognu/otter/fragments/OtterFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/OtterFragment.kt index 1b710d8..faf1e34 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/OtterFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/OtterFragment.kt @@ -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 : RecyclerView.Adapter() { var data: MutableList = mutableListOf() @@ -154,8 +155,6 @@ abstract class OtterFragment> : Fragm } abstract class PagedOtterFragment> : Fragment() { - open val OFFSCREEN_PAGES = 10 - abstract val repository: Repository abstract val adapter: A open val viewModel: ViewModel? = null @@ -219,15 +218,4 @@ abstract class PagedOtterFragment(), KoinComponent { - override suspend fun load(loadType: LoadType, state: PagingState): MediatorResult { +class Mediator(private val context: Context, private val database: Database, private val repository: ArtistsRepository) : RemoteMediator(), KoinComponent { + override suspend fun load(loadType: LoadType, state: PagingState): 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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/models/api/Album.kt b/app/src/main/java/com/github/apognu/otter/models/api/Album.kt index 76227dc..f9f67a8 100644 --- a/app/src/main/java/com/github/apognu/otter/models/api/Album.kt +++ b/app/src/main/java/com/github/apognu/otter/models/api/Album.kt @@ -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, 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?) - diff --git a/app/src/main/java/com/github/apognu/otter/models/api/Artist.kt b/app/src/main/java/com/github/apognu/otter/models/api/Artist.kt index 09f03d1..ea593a6 100644 --- a/app/src/main/java/com/github/apognu/otter/models/api/Artist.kt +++ b/app/src/main/java/com/github/apognu/otter/models/api/Artist.kt @@ -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, 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) + } + } + } + } } diff --git a/app/src/main/java/com/github/apognu/otter/models/api/Track.kt b/app/src/main/java/com/github/apognu/otter/models/api/Track.kt index 38efaeb..eaeb871 100644 --- a/app/src/main/java/com/github/apognu/otter/models/api/Track.kt +++ b/app/src/main/java/com/github/apognu/otter/models/api/Track.kt @@ -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, 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 diff --git a/app/src/main/java/com/github/apognu/otter/models/dao/Artist.kt b/app/src/main/java/com/github/apognu/otter/models/dao/Artist.kt index a1ff94e..b9adf8f 100644 --- a/app/src/main/java/com/github/apognu/otter/models/dao/Artist.kt +++ b/app/src/main/java/com/github/apognu/otter/models/dao/Artist.kt @@ -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) -} \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/models/domain/Album.kt b/app/src/main/java/com/github/apognu/otter/models/domain/Album.kt index 8f10548..d53d5b2 100644 --- a/app/src/main/java/com/github/apognu/otter/models/domain/Album.kt +++ b/app/src/main/java/com/github/apognu/otter/models/domain/Album.kt @@ -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, diff --git a/app/src/main/java/com/github/apognu/otter/models/domain/Artist.kt b/app/src/main/java/com/github/apognu/otter/models/domain/Artist.kt index 256ecce..a2eda32 100644 --- a/app/src/main/java/com/github/apognu/otter/models/domain/Artist.kt +++ b/app/src/main/java/com/github/apognu/otter/models/domain/Artist.kt @@ -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, diff --git a/app/src/main/java/com/github/apognu/otter/models/domain/Track.kt b/app/src/main/java/com/github/apognu/otter/models/domain/Track.kt index e144eb8..a0c586e 100644 --- a/app/src/main/java/com/github/apognu/otter/models/domain/Track.kt +++ b/app/src/main/java/com/github/apognu/otter/models/domain/Track.kt @@ -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, diff --git a/app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt index f341509..36e94fb 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt @@ -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() { +class AlbumsRepository(override val context: Context, private val couch: Database, artistId: Int?) : Repository() { override val upstream: Upstream 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): List { - 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) = database.albums().findAllDecorated(ids) + fun insert(albums: List) = FunkwhaleAlbum.persist(couch, albums) - fun ofArtist(id: Int): LiveData> { + fun all() = + select(SelectResult.all()) + .from(couch) + .where { "type" equalTo "album"} + .asFlow() + .asLiveData() + + fun find(ids: List) = + 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 { 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) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt index 6ceb17a..dbc6704 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt @@ -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() { + private val mediator = Mediator(context, database, this) -class ArtistsRepository(override val context: Context, private val database: OtterDatabase) : Repository() { 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): List { - 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.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> { - scope.launch(IO) { - fetch().collect() - } - - return database.artists().allDecorated() - } - - fun get(id: Int) = database.artists().getDecorated(id) - fun find(ids: List) = database.artists().findDecorated(ids) + fun find(ids: List) = + select(all()) + .from(database) + .where { ("type" equalTo "artist") and (Meta.id.`in`(*ids.map { Expression.string("artist:$it") }.toTypedArray())) } + .asFlow() + .asLiveData(GlobalScope.coroutineContext) } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt b/app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt index 638edb4..64d92b1 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt @@ -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(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(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)) } diff --git a/app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt index efd3cc5..d871e5c 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt @@ -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> = MutableLiveData() val results: LiveData> = 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): List { - 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> = MutableLiveData() val results: LiveData> = 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): List { - 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> = MutableLiveData() val results: LiveData> = 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): List { - data.forEach { - repository.insert(it) - } + repository.insert(data) ids.addAll(data.map { it.id }) _ids.postValue(ids) diff --git a/app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt index a81aeb3..a242b27 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt @@ -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() { +class TracksRepository(override val context: Context, private val database: OtterDatabase, private val couch: Database, albumId: Int?) : Repository() { 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): List = 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.persist(couch, tracks) - fun find(ids: List) = database.tracks().findAllDecorated(ids) + fun find(ids: List) = 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) diff --git a/app/src/main/java/com/github/apognu/otter/utils/Extensions.kt b/app/src/main/java/com/github/apognu/otter/utils/Extensions.kt index e62b208..06f75c7 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/Extensions.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/Extensions.kt @@ -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>() { + 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 List?.toCouchbaseArray(): Array { + this?.let { + return MutableArray(this) + } + + return MutableArray(listOf()) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/viewmodels/AlbumsViewModel.kt b/app/src/main/java/com/github/apognu/otter/viewmodels/AlbumsViewModel.kt index 9f0a722..c596791 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/AlbumsViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/AlbumsViewModel.kt @@ -12,11 +12,11 @@ class AlbumsViewModel(private val repository: AlbumsRepository, private val trac val albums: LiveData> 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) } } } } diff --git a/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt b/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt index 4b5a106..5785c0b 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt @@ -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() { + override suspend fun load(params: LoadParams): LoadResult { + 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> = 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) +} diff --git a/app/src/main/java/com/github/apognu/otter/viewmodels/FavoritesViewModel.kt b/app/src/main/java/com/github/apognu/otter/viewmodels/FavoritesViewModel.kt index a347374..4d52d07 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/FavoritesViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/FavoritesViewModel.kt @@ -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 } } } } diff --git a/build.gradle.kts b/build.gradle.kts index fd414d4..bb64df9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") } }