From f99fc8698ac161ec3f27e9f159e9352fd270af00 Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 18 Mar 2022 02:11:52 +0800 Subject: [PATCH] Fever API support is coming --- app/src/main/java/me/ash/reader/App.kt | 3 + .../data/repository/AbstractRssRepository.kt | 3 + .../data/repository/FeverRssRepository.kt | 151 ++++++++++++++++ .../data/repository/LocalRssRepository.kt | 4 +- .../ash/reader/data/repository/RssHelper.kt | 10 +- .../reader/data/repository/RssRepository.kt | 6 +- .../reader/data/source/FeverApiDataSource.kt | 29 ++- .../me/ash/reader/data/source/FeverApiDto.kt | 165 +++++++++++++++--- .../ash/reader/ui/page/home/HomeViewModel.kt | 2 +- 9 files changed, 330 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt diff --git a/app/src/main/java/me/ash/reader/App.kt b/app/src/main/java/me/ash/reader/App.kt index 3db0055..05ae60f 100644 --- a/app/src/main/java/me/ash/reader/App.kt +++ b/app/src/main/java/me/ash/reader/App.kt @@ -32,6 +32,9 @@ class App : Application() { @Inject lateinit var localRssRepository: LocalRssRepository + @Inject + lateinit var feverRssRepository: FeverRssRepository + @Inject lateinit var opmlRepository: OpmlRepository diff --git a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt index bff153d..430571b 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt @@ -6,6 +6,7 @@ import androidx.paging.PagingSource import androidx.work.* import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import me.ash.reader.DataStoreKeys import me.ash.reader.data.account.AccountDao import me.ash.reader.data.article.Article @@ -42,6 +43,8 @@ abstract class AbstractRssRepository constructor( val isNotSyncing: Boolean = !isSyncing } + abstract fun getSyncState(): StateFlow + abstract suspend fun updateArticleInfo(article: Article) abstract suspend fun subscribe(feed: Feed, articles: List
) diff --git a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt new file mode 100644 index 0000000..90de987 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt @@ -0,0 +1,151 @@ +package me.ash.reader.data.repository + +import android.content.Context +import android.util.Log +import androidx.work.WorkManager +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import me.ash.reader.DataStoreKeys +import me.ash.reader.data.account.AccountDao +import me.ash.reader.data.article.Article +import me.ash.reader.data.article.ArticleDao +import me.ash.reader.data.feed.Feed +import me.ash.reader.data.feed.FeedDao +import me.ash.reader.data.group.Group +import me.ash.reader.data.group.GroupDao +import me.ash.reader.data.source.FeverApiDataSource +import me.ash.reader.data.source.RssNetworkDataSource +import me.ash.reader.dataStore +import me.ash.reader.get +import net.dankito.readability4j.extended.Readability4JExtended +import java.util.* +import javax.inject.Inject + +class FeverRssRepository @Inject constructor( + @ApplicationContext + private val context: Context, + private val articleDao: ArticleDao, + private val feedDao: FeedDao, + private val groupDao: GroupDao, + private val rssHelper: RssHelper, + private val feverApiDataSource: FeverApiDataSource, + rssNetworkDataSource: RssNetworkDataSource, + accountDao: AccountDao, + workManager: WorkManager, +) : AbstractRssRepository( + context, accountDao, articleDao, groupDao, + feedDao, rssNetworkDataSource, workManager, +) { + private val mutex = Mutex() + private val syncState = MutableStateFlow(SyncState()) + + override fun getSyncState() = syncState + + override suspend fun updateArticleInfo(article: Article) { + articleDao.update(article) + } + + override suspend fun subscribe(feed: Feed, articles: List
) { + feedDao.insert(feed) + articleDao.insertList(articles.map { + it.copy(feedId = feed.id) + }) + } + + override suspend fun sync( + context: Context, + accountDao: AccountDao, + articleDao: ArticleDao, + feedDao: FeedDao, + rssNetworkDataSource: RssNetworkDataSource + ) { + mutex.withLock { + val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) + ?: return + + syncState.update { + it.copy( + feedCount = 1, + syncedCount = 1, + currentFeedName = "Fever" + ) + } + + if (feedDao.queryAll(accountId).isNullOrEmpty()) { + // Temporary add feeds + val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds + val feverGroupsBody = feverApiDataSource.groups().execute().body()!! + Log.i("RLog", "Fever groups: $feverGroupsBody") + feverGroupsBody.groups.forEach { + groupDao.insert( + Group( + id = it.id.toString(), + name = it.title, + accountId = accountId, + ) + ) + } + val feverFeedsGroupsMap = mutableMapOf() + feverGroupsBody.feeds_groups.forEach { item -> + item.feed_ids + .split(",") + .map { it.toInt() } + .forEach { id -> + feverFeedsGroupsMap[id] = item.group_id + } + } + val feeds = feverFeeds.map { + Feed( + id = it.id.toString(), + name = it.title, + url = it.url, + groupId = feverFeedsGroupsMap[it.id].toString(), + accountId = accountId + ) + } + feedDao.insertList(feeds) + } + + // Add articles + val articles = mutableListOf
() + feverApiDataSource.itemsBySince(since = 1647444325925621L) + .execute().body()!!.items + .forEach { + articles.add( + Article( + id = it.id, + date = Date(it.created_on_time * 1000), + title = it.title, + author = it.author, + rawDescription = it.html, + shortDescription = ( + Readability4JExtended("", it.html) + .parse().textContent ?: "" + ).take(100).trim(), + link = it.url, + accountId = accountId, + feedId = it.feed_id.toString(), + isUnread = it.is_read == 0, + isStarred = it.is_saved == 1, + ) + ) + } + articleDao.insertList(articles) + + // Complete sync + accountDao.update(accountDao.queryById(accountId)!!.apply { + updateAt = Date() + }) + syncState.update { + it.copy( + feedCount = 0, + syncedCount = 0, + currentFeedName = "" + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt index 958be7d..a7d7dcd 100644 --- a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt @@ -41,8 +41,10 @@ class LocalRssRepository @Inject constructor( context, accountDao, articleDao, groupDao, feedDao, rssNetworkDataSource, workManager, ) { - val syncState = MutableStateFlow(SyncState()) private val mutex = Mutex() + private val syncState = MutableStateFlow(SyncState()) + + override fun getSyncState() = syncState override suspend fun updateArticleInfo(article: Article) { articleDao.update(article) diff --git a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt index 694b949..2a62ec8 100644 --- a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt @@ -46,10 +46,7 @@ class RssHelper @Inject constructor( author = it.author, rawDescription = it.description.toString(), shortDescription = (Readability4JExtended("", it.description.toString()) - .parse().textContent ?: "").trim().run { - if (this.length > 100) this.substring(0, 100) - else this - }, + .parse().textContent ?: "").take(100).trim(), link = it.link ?: "", ) ) @@ -112,10 +109,7 @@ class RssHelper @Inject constructor( author = it.author, rawDescription = it.description.toString(), shortDescription = (Readability4JExtended("", it.description.toString()) - .parse().textContent ?: "").trim().run { - if (this.length > 100) this.substring(0, 100) - else this - }, + .parse().textContent ?: "").take(100).trim(), link = it.link ?: "", ) ) diff --git a/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt index 7b352da..39c9e82 100644 --- a/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt @@ -12,13 +12,13 @@ class RssRepository @Inject constructor( @ApplicationContext private val context: Context, private val localRssRepository: LocalRssRepository, -// private val feverRssRepository: FeverRssRepository, + private val feverRssRepository: FeverRssRepository, // private val googleReaderRssRepository: GoogleReaderRssRepository, ) { fun get() = when (getAccountType()) { -// Account.Type.LOCAL -> localRssRepository Account.Type.LOCAL -> localRssRepository -// Account.Type.FEVER -> feverRssRepository +// Account.Type.LOCAL -> feverRssRepository + Account.Type.FEVER -> feverRssRepository // Account.Type.GOOGLE_READER -> googleReaderRssRepository else -> throw IllegalStateException("Unknown account type: ${getAccountType()}") } diff --git a/app/src/main/java/me/ash/reader/data/source/FeverApiDataSource.kt b/app/src/main/java/me/ash/reader/data/source/FeverApiDataSource.kt index d300a35..9a5b587 100644 --- a/app/src/main/java/me/ash/reader/data/source/FeverApiDataSource.kt +++ b/app/src/main/java/me/ash/reader/data/source/FeverApiDataSource.kt @@ -10,15 +10,38 @@ import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part +import retrofit2.http.Query interface FeverApiDataSource { @Multipart - @POST("fever.php?api&groups") + @POST("fever.php/?api=&feeds=") + fun feeds(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call + + @Multipart + @POST("fever.php/?api=&groups=") fun groups(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call @Multipart - @POST("fever.php?api&feeds") - fun feeds(@Part("api_key") apiKey: RequestBody?="1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call + @POST("fever.php/?api=&items=") + fun itemsBySince( + @Query("since_id") since: Long, + @Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody() + ): Call + + @Multipart + @POST("fever.php/?api=&unread_item_ids=") + fun itemsByUnread(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call + + @Multipart + @POST("fever.php/?api=&saved_item_ids=") + fun itemsByStarred(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call + + @Multipart + @POST("fever.php/?api=&items=") + fun itemsByIds( + @Query("with_ids") ids: String, + @Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody() + ): Call companion object { private var instance: FeverApiDataSource? = null diff --git a/app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt b/app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt index 8a50cde..5c01ca2 100644 --- a/app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt +++ b/app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt @@ -1,13 +1,78 @@ package me.ash.reader.data.source object FeverApiDto { - // &groups - data class Groups( - val apiVersion: Int, + + /** + * @link fever.php/?api=&feeds= + * @sample + * { + * "api_version": 3, + * "auth": 1, + * "last_refreshed_on_time": 1647530101, + * "feeds": [ + * { + * "id": 2, + * "favicon_id": 2, + * "title": "Ash's Knowledge Base", + * "url": "https://www.ashinch.com/feed", + * "site_url": "http://ashinch.com/", + * "is_spark": 0, + * "last_updated_on_time": 1647530101 + * } + * ], + * "feeds_groups": [ + * { + * "group_id": 2, + * "feed_ids": "2,3,4" + * } + * ] + * } + */ + data class Feed( + val api_version: Int, val auth: Int, - val lastRefreshedOnTime: Long, + val last_refreshed_on_time: Long, + val feeds: List, + val feeds_groups: List, + ) + + data class FeedItem( + val id: Int, + val favicon_id: Int, + val title: String, + val url: String, + val site_url: String, + val is_spark: Int, + val last_refreshed_on_time: Long, + ) + + /** + * @link fever.php/?api=&groups= + * @sample + * { + * "api_version": 3, + * "auth": 1, + * "last_refreshed_on_time": 1647534602, + * "groups": [ + * { + * "id": 1, + * "title": "未分类" + * } + * ], + * "feeds_groups": [ + * { + * "group_id": 2, + * "feed_ids": "2,3,4" + * }, + * ] + * } + */ + data class Groups( + val api_version: Int, + val auth: Int, + val last_refreshed_on_time: Long, val groups: List, - val feedsGroups: List, + val feeds_groups: List, ) data class GroupItem( @@ -16,39 +81,85 @@ object FeverApiDto { ) data class FeedsGroupsItem( - val groupId: Int, - val feedsIds: String, + val group_id: Int, + val feed_ids: String, ) - // &feeds - data class Feed( - val apiVersion: Int, + /** + * @link fever.php/?api=&items=&with_ids={ids} + * @link fever.php/?api=&items=&since_id={since} + * @sample + * { + * "api_version": 3, + * "auth": 1, + * "last_refreshed_on_time": 1647534602, + * "total_items": 853, + * "items": [ + * { + * "id": "1647445533955157", + * "feed_id": 37, + * "title": "智能音箱自己把自己黑了:随机购物拨号,自主开灯关门,平均成功率达88%", + * "author": "博雯", + * "html": "
\n

博雯 发自 凹非寺

\n

, - val feedsGroups: List, + val last_refreshed_on_time: Long, + val total_items: Int, + val items: List, ) - data class FeedItem( - val id: Int, - val favicon_id: Int, + data class Item( + val id: String, + val feed_id: Int, val title: String, + val author: String, + val html: String, val url: String, - val siteUrl: String, - val isSpark: Int, - val lastRefreshedOnTime: Long, + val is_saved: Int, + val is_read: Int, + val created_on_time: Long, ) - // &favicons - data class Favicons( - val apiVersion: Int, + /** + * @link fever.php/?api=&unread_item_ids= + * @sample + * { + * "api_version": 3, + * "auth": 1, + * "last_refreshed_on_time": 1647530135, + * "unread_item_ids": "1646660589277217,1646660589277218" + * } + */ + data class ItemsByUnread( + val api_version: Int, val auth: Int, - val lastRefreshedOnTime: Long, - val favicons: List, + val last_refreshed_on_time: Long, + val unread_item_ids: String, ) - data class FaviconItem( - val id: Int, - val data: String, + /** + * @link fever.php/?api=&saved_item_ids= + * @sample + * { + * "api_version": 3, + * "auth": 1, + * "last_refreshed_on_time": 1647534602, + * "saved_item_ids": "1647441026698935,1646660589277218" + * } + */ + data class ItemsByStarred( + val api_version: Int, + val auth: Int, + val last_refreshed_on_time: Long, + val saved_item_ids: String, ) } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt index 6de72d6..3a5a3a8 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt @@ -31,7 +31,7 @@ class HomeViewModel @Inject constructor( private val _filterState = MutableStateFlow(FilterState()) val filterState = _filterState.asStateFlow() - val syncState = rssRepository.get().syncState + val syncState = rssRepository.get().getSyncState() fun dispatch(action: HomeViewAction) { when (action) {