Fever API support is coming

This commit is contained in:
Ash 2022-03-18 02:11:52 +08:00
parent d105735453
commit f99fc8698a
9 changed files with 330 additions and 43 deletions

View File

@ -32,6 +32,9 @@ class App : Application() {
@Inject @Inject
lateinit var localRssRepository: LocalRssRepository lateinit var localRssRepository: LocalRssRepository
@Inject
lateinit var feverRssRepository: FeverRssRepository
@Inject @Inject
lateinit var opmlRepository: OpmlRepository lateinit var opmlRepository: OpmlRepository

View File

@ -6,6 +6,7 @@ import androidx.paging.PagingSource
import androidx.work.* import androidx.work.*
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import me.ash.reader.DataStoreKeys import me.ash.reader.DataStoreKeys
import me.ash.reader.data.account.AccountDao import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article import me.ash.reader.data.article.Article
@ -42,6 +43,8 @@ abstract class AbstractRssRepository constructor(
val isNotSyncing: Boolean = !isSyncing val isNotSyncing: Boolean = !isSyncing
} }
abstract fun getSyncState(): StateFlow<SyncState>
abstract suspend fun updateArticleInfo(article: Article) abstract suspend fun updateArticleInfo(article: Article)
abstract suspend fun subscribe(feed: Feed, articles: List<Article>) abstract suspend fun subscribe(feed: Feed, articles: List<Article>)

View File

@ -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<Article>) {
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<Int, Int>()
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<Article>()
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 = ""
)
}
}
}
}

View File

@ -41,8 +41,10 @@ class LocalRssRepository @Inject constructor(
context, accountDao, articleDao, groupDao, context, accountDao, articleDao, groupDao,
feedDao, rssNetworkDataSource, workManager, feedDao, rssNetworkDataSource, workManager,
) { ) {
val syncState = MutableStateFlow(SyncState())
private val mutex = Mutex() private val mutex = Mutex()
private val syncState = MutableStateFlow(SyncState())
override fun getSyncState() = syncState
override suspend fun updateArticleInfo(article: Article) { override suspend fun updateArticleInfo(article: Article) {
articleDao.update(article) articleDao.update(article)

View File

@ -46,10 +46,7 @@ class RssHelper @Inject constructor(
author = it.author, author = it.author,
rawDescription = it.description.toString(), rawDescription = it.description.toString(),
shortDescription = (Readability4JExtended("", it.description.toString()) shortDescription = (Readability4JExtended("", it.description.toString())
.parse().textContent ?: "").trim().run { .parse().textContent ?: "").take(100).trim(),
if (this.length > 100) this.substring(0, 100)
else this
},
link = it.link ?: "", link = it.link ?: "",
) )
) )
@ -112,10 +109,7 @@ class RssHelper @Inject constructor(
author = it.author, author = it.author,
rawDescription = it.description.toString(), rawDescription = it.description.toString(),
shortDescription = (Readability4JExtended("", it.description.toString()) shortDescription = (Readability4JExtended("", it.description.toString())
.parse().textContent ?: "").trim().run { .parse().textContent ?: "").take(100).trim(),
if (this.length > 100) this.substring(0, 100)
else this
},
link = it.link ?: "", link = it.link ?: "",
) )
) )

View File

@ -12,13 +12,13 @@ class RssRepository @Inject constructor(
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
private val localRssRepository: LocalRssRepository, private val localRssRepository: LocalRssRepository,
// private val feverRssRepository: FeverRssRepository, private val feverRssRepository: FeverRssRepository,
// private val googleReaderRssRepository: GoogleReaderRssRepository, // private val googleReaderRssRepository: GoogleReaderRssRepository,
) { ) {
fun get() = when (getAccountType()) { fun get() = when (getAccountType()) {
// Account.Type.LOCAL -> localRssRepository
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 // Account.Type.GOOGLE_READER -> googleReaderRssRepository
else -> throw IllegalStateException("Unknown account type: ${getAccountType()}") else -> throw IllegalStateException("Unknown account type: ${getAccountType()}")
} }

View File

@ -10,15 +10,38 @@ import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Multipart import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.Query
interface FeverApiDataSource { interface FeverApiDataSource {
@Multipart @Multipart
@POST("fever.php?api&groups") @POST("fever.php/?api=&feeds=")
fun feeds(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Feed>
@Multipart
@POST("fever.php/?api=&groups=")
fun groups(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Groups> fun groups(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Groups>
@Multipart @Multipart
@POST("fever.php?api&feeds") @POST("fever.php/?api=&items=")
fun feeds(@Part("api_key") apiKey: RequestBody?="1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Feed> fun itemsBySince(
@Query("since_id") since: Long,
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()
): Call<FeverApiDto.Items>
@Multipart
@POST("fever.php/?api=&unread_item_ids=")
fun itemsByUnread(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.ItemsByUnread>
@Multipart
@POST("fever.php/?api=&saved_item_ids=")
fun itemsByStarred(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.ItemsByStarred>
@Multipart
@POST("fever.php/?api=&items=")
fun itemsByIds(
@Query("with_ids") ids: String,
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()
): Call<FeverApiDto.Items>
companion object { companion object {
private var instance: FeverApiDataSource? = null private var instance: FeverApiDataSource? = null

View File

@ -1,13 +1,78 @@
package me.ash.reader.data.source package me.ash.reader.data.source
object FeverApiDto { 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 auth: Int,
val lastRefreshedOnTime: Long, val last_refreshed_on_time: Long,
val feeds: List<FeedItem>,
val feeds_groups: List<FeedsGroupsItem>,
)
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<GroupItem>, val groups: List<GroupItem>,
val feedsGroups: List<FeedsGroupsItem>, val feeds_groups: List<FeedsGroupsItem>,
) )
data class GroupItem( data class GroupItem(
@ -16,39 +81,85 @@ object FeverApiDto {
) )
data class FeedsGroupsItem( data class FeedsGroupsItem(
val groupId: Int, val group_id: Int,
val feedsIds: String, val feed_ids: String,
) )
// &feeds /**
data class Feed( * @link fever.php/?api=&items=&with_ids={ids}
val apiVersion: Int, * @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": "<blockquote>\n<p data-track=\"48\">博雯 发自 凹非寺</p>\n<p d...",
* "url": "https://www.qbitai.com/2022/03/33402.html",
* "is_saved": 0,
* "is_read": 0,
* "created_on_time": 1647442680
* }
* ]
* {
*/
data class Items(
val api_version: Int,
val auth: Int, val auth: Int,
val lastRefreshedOnTime: Long, val last_refreshed_on_time: Long,
val feeds: List<FeedItem>, val total_items: Int,
val feedsGroups: List<FeedsGroupsItem>, val items: List<Item>,
) )
data class FeedItem( data class Item(
val id: Int, val id: String,
val favicon_id: Int, val feed_id: Int,
val title: String, val title: String,
val author: String,
val html: String,
val url: String, val url: String,
val siteUrl: String, val is_saved: Int,
val isSpark: Int, val is_read: Int,
val lastRefreshedOnTime: Long, val created_on_time: Long,
) )
// &favicons /**
data class Favicons( * @link fever.php/?api=&unread_item_ids=
val apiVersion: Int, * @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 auth: Int,
val lastRefreshedOnTime: Long, val last_refreshed_on_time: Long,
val favicons: List<FaviconItem>, val unread_item_ids: String,
) )
data class FaviconItem( /**
val id: Int, * @link fever.php/?api=&saved_item_ids=
val data: String, * @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,
) )
} }

View File

@ -31,7 +31,7 @@ class HomeViewModel @Inject constructor(
private val _filterState = MutableStateFlow(FilterState()) private val _filterState = MutableStateFlow(FilterState())
val filterState = _filterState.asStateFlow() val filterState = _filterState.asStateFlow()
val syncState = rssRepository.get().syncState val syncState = rssRepository.get().getSyncState()
fun dispatch(action: HomeViewAction) { fun dispatch(action: HomeViewAction) {
when (action) { when (action) {