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
lateinit var localRssRepository: LocalRssRepository
@Inject
lateinit var feverRssRepository: FeverRssRepository
@Inject
lateinit var opmlRepository: OpmlRepository

View File

@ -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<SyncState>
abstract suspend fun updateArticleInfo(article: 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,
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)

View File

@ -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 ?: "",
)
)

View File

@ -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()}")
}

View File

@ -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<FeverApiDto.Feed>
@Multipart
@POST("fever.php/?api=&groups=")
fun groups(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Groups>
@Multipart
@POST("fever.php?api&feeds")
fun feeds(@Part("api_key") apiKey: RequestBody?="1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Feed>
@POST("fever.php/?api=&items=")
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 {
private var instance: FeverApiDataSource? = null

View File

@ -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<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 feedsGroups: List<FeedsGroupsItem>,
val feeds_groups: List<FeedsGroupsItem>,
)
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": "<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 lastRefreshedOnTime: Long,
val feeds: List<FeedItem>,
val feedsGroups: List<FeedsGroupsItem>,
val last_refreshed_on_time: Long,
val total_items: Int,
val items: List<Item>,
)
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<FaviconItem>,
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,
)
}

View File

@ -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) {