Merge branch 'refs/heads/master' into develop

# Conflicts:
#	api/src/test/java/com/readrops/api/services/nextcloudnews/NextcloudNewsDataSourceTest.kt
#	db/src/main/java/com/readrops/db/dao/ItemDao.kt
This commit is contained in:
Shinokuni 2024-12-02 16:48:07 +01:00
commit 1a727f1f1b
16 changed files with 181 additions and 171 deletions

View File

@ -1,3 +1,8 @@
# v2.0.3
- Fix Fever API compatibility with TinyTiny RSS and yarr, should also fix other providers (#228 + #229)
- Fix Nextcloud News item duplicates when syncing which would made the app unusable
- Fix Nextcloud News item parsing: items with no title will be ignored
# v2.0.2
- Fix crash when opening app from a notification (#223)
- Fix Fever API synchronization error (#228)

View File

@ -1,12 +1,10 @@
package com.readrops.api.services.fever.adapters
import com.readrops.api.utils.exceptions.ParseException
import com.readrops.api.utils.extensions.skipField
import com.readrops.api.utils.extensions.toBoolean
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonReader.Token
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
@ -21,21 +19,17 @@ class FeverAPIAdapter : JsonAdapter<Boolean>() {
override fun fromJson(reader: JsonReader): Boolean = with(reader) {
return try {
beginObject()
skipField()
var authenticated = 0
if (nextName() == "auth") {
authenticated = nextInt()
} else {
skipValue()
}
while (peek() == Token.NAME) {
skipField()
var authenticated = false
while (hasNext()) {
when (nextName()) {
"auth" -> authenticated = nextInt().toBoolean()
else -> skipValue()
}
}
endObject()
authenticated.toBoolean()
authenticated
} catch (e: Exception) {
throw ParseException(e.message)
}

View File

@ -2,7 +2,6 @@ package com.readrops.api.services.fever.adapters
import android.annotation.SuppressLint
import com.readrops.api.utils.exceptions.ParseException
import com.readrops.api.utils.extensions.skipField
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.ToJson
@ -19,7 +18,6 @@ class FeverFaviconsAdapter {
@ToJson
fun toJson(favicons: List<Favicon>) = ""
@OptIn(ExperimentalEncodingApi::class)
@SuppressLint("CheckResult")
@FromJson
fun fromJson(reader: JsonReader): List<Favicon> = with(reader) {
@ -27,47 +25,54 @@ class FeverFaviconsAdapter {
val favicons = arrayListOf<Favicon>()
beginObject()
repeat(3) {
skipField()
}
nextName() // beginning of favicon array
beginArray()
while (hasNext()) {
beginObject()
when (nextName()) {
"favicons" -> {
beginArray()
var id = 0
var data: ByteArray? = null
while (hasNext()) {
beginObject()
parseFavicon(reader)?.let { favicons += it }
while (hasNext()) {
when (selectName(NAMES)) {
0 -> id = nextInt()
1 -> data = Base64.decode(nextString().substringAfter("base64,"))
else -> skipValue()
endObject()
}
endArray()
}
else -> skipValue()
}
if (id > 0 && data != null) {
favicons += Favicon(
id = id,
data = data,
)
}
endObject()
}
endArray()
endObject()
favicons
} catch (e: Exception) {
throw ParseException(e.message)
}
}
@OptIn(ExperimentalEncodingApi::class)
private fun parseFavicon(reader: JsonReader): Favicon? = with(reader) {
var id = 0
var data: ByteArray? = null
while (hasNext()) {
when (selectName(NAMES)) {
0 -> id = nextInt()
1 -> data = Base64.decode(nextString().substringAfter("base64,"))
else -> skipValue()
}
}
if (id > 0 && data != null) {
return Favicon(
id = id,
data = data,
)
} else {
null
}
}
companion object {
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "data")
}

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import com.readrops.api.utils.exceptions.ParseException
import com.readrops.api.utils.extensions.nextNonEmptyString
import com.readrops.api.utils.extensions.nextNullableString
import com.readrops.api.utils.extensions.skipField
import com.readrops.db.entities.Feed
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
@ -30,63 +29,34 @@ class FeverFeedsAdapter : JsonAdapter<FeverFeeds>() {
val feedsGroups = mutableMapOf<Int, List<Int>>()
beginObject()
while (nextName() != "feeds") {
skipValue()
}
// feeds array
beginArray()
while (hasNext()) {
beginObject()
when (nextName()) {
"feeds" -> {
beginArray()
while (hasNext()) {
beginObject()
feeds += parseFeed(reader, favicons)
val feed = Feed()
while (hasNext()) {
with(feed) {
when (selectName(NAMES)) {
0 -> remoteId = nextInt().toString()
1 -> favicons[nextInt()] = remoteId!!
2 -> name = nextNonEmptyString()
3 -> url = nextNonEmptyString()
4 -> siteUrl = nextNullableString()
else -> skipValue()
endObject()
}
endArray()
}
}
"feeds_groups" -> {
beginArray()
while (hasNext()) {
beginObject()
feeds += feed
endObject()
}
val (folderId, feedsIds) = parseFeedsGroups(reader)
folderId?.let { feedsGroups[it] = feedsIds }
endArray()
endObject()
}
while (nextName() != "feeds_groups") {
skipValue()
}
// feeds_groups array
beginArray()
while (hasNext()) {
beginObject()
var folderId: Int? = null
val feedsIds = mutableListOf<Int>()
while (hasNext()) {
when (selectName(JsonReader.Options.of("group_id", "feed_ids"))) {
0 -> folderId = nextInt()
1 -> feedsIds += nextNonEmptyString().split(",").map { it.toInt() }
else -> skipValue()
endArray()
}
else -> skipValue()
}
folderId?.let { feedsGroups[it] = feedsIds }
endObject()
}
endArray()
while (peek() != JsonReader.Token.END_OBJECT) {
skipField()
}
endObject()
@ -101,6 +71,39 @@ class FeverFeedsAdapter : JsonAdapter<FeverFeeds>() {
}
}
private fun parseFeed(reader: JsonReader, favicons: MutableMap<Int, String>): Feed = with(reader) {
val feed = Feed()
while (hasNext()) {
with(feed) {
when (selectName(NAMES)) {
0 -> remoteId = nextInt().toString()
1 -> favicons[nextInt()] = remoteId!!
2 -> name = nextNonEmptyString()
3 -> url = nextNonEmptyString()
4 -> siteUrl = nextNullableString()
else -> skipValue()
}
}
}
return feed
}
private fun parseFeedsGroups(reader: JsonReader): Pair<Int?, List<Int>> = with(reader) {
var folderId: Int? = null
val feedsIds = mutableListOf<Int>()
while (hasNext()) {
when (selectName(JsonReader.Options.of("group_id", "feed_ids"))) {
0 -> folderId = nextInt()
1 -> feedsIds += nextNonEmptyString().split(",").map { it.toInt() }
else -> skipValue()
}
}
folderId to feedsIds
}
companion object {
val NAMES: JsonReader.Options =
JsonReader.Options.of("id", "favicon_id", "title", "url", "site_url")

View File

@ -3,8 +3,6 @@ package com.readrops.api.services.fever.adapters
import android.annotation.SuppressLint
import com.readrops.api.utils.exceptions.ParseException
import com.readrops.api.utils.extensions.nextNonEmptyString
import com.readrops.api.utils.extensions.skipField
import com.readrops.api.utils.extensions.skipToEnd
import com.readrops.db.entities.Folder
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
@ -22,35 +20,35 @@ class FeverFoldersAdapter {
val folders = arrayListOf<Folder>()
beginObject()
repeat(3) {
skipField()
}
nextName() // beginning of folders array
beginArray()
while (hasNext()) {
beginObject()
when (nextName()) {
"groups" -> {
beginArray()
val folder = Folder()
while (hasNext()) {
with(folder) {
when (selectName(NAMES)) {
0 -> remoteId = nextInt().toString()
1 -> name = nextNonEmptyString()
while (hasNext()) {
beginObject()
val folder = Folder()
while (hasNext()) {
with(folder) {
when (selectName(NAMES)) {
0 -> remoteId = nextInt().toString()
1 -> name = nextNonEmptyString()
}
}
}
folders += folder
endObject()
}
}
}
folders += folder
endObject()
endArray()
}
else -> skipValue()
}
}
endArray()
skipToEnd()
endObject()
folders
} catch (e: Exception) {
throw ParseException(e.message)

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import com.readrops.api.utils.exceptions.ParseException
import com.readrops.api.utils.extensions.nextNonEmptyString
import com.readrops.api.utils.extensions.nextNullableString
import com.readrops.api.utils.extensions.skipField
import com.readrops.api.utils.extensions.toBoolean
import com.readrops.db.entities.Item
import com.readrops.db.util.DateUtils
@ -24,56 +23,59 @@ class FeverItemsAdapter {
val items = arrayListOf<Item>()
beginObject()
while (nextName() != "items") {
skipValue()
}
beginArray()
while (hasNext()) {
beginObject()
when (nextName()) {
"items" -> {
beginArray()
while (hasNext()) {
beginObject()
items += parseItem(reader)
val item = Item()
while (hasNext()) {
with(item) {
when (selectName(NAMES)) {
0 -> {
remoteId = if (reader.peek() == JsonReader.Token.STRING) {
nextNonEmptyString()
} else {
nextInt().toString()
}
}
1 -> feedRemoteId = nextNonEmptyString()
2 -> title = nextNonEmptyString()
3 -> author = nextNullableString()
4 -> content = nextNullableString()
5 -> link = nextNullableString()
6 -> isRead = nextInt().toBoolean()
7 -> isStarred = nextInt().toBoolean()
8 -> pubDate = DateUtils.fromEpochSeconds(nextLong())
else -> skipValue()
endObject()
}
endArray()
}
else -> skipValue()
}
items += item
endObject()
}
endArray()
while (peek() != JsonReader.Token.END_OBJECT) {
skipField()
}
endObject()
items
} catch (e: Exception) {
throw ParseException(e.message)
}
}
private fun parseItem(reader: JsonReader): Item = with(reader) {
val item = Item()
while (hasNext()) {
with(item) {
when (selectName(NAMES)) {
0 -> {
remoteId = if (reader.peek() == JsonReader.Token.STRING) {
nextNonEmptyString()
} else {
nextInt().toString()
}
}
1 -> feedRemoteId = nextNonEmptyString()
2 -> title = nextNonEmptyString()
3 -> author = nextNullableString()
4 -> content = nextNullableString()
5 -> link = nextNullableString()
6 -> isRead = nextInt().toBoolean()
7 -> isStarred = nextInt().toBoolean()
8 -> pubDate = DateUtils.fromEpochSeconds(nextLong())
else -> skipValue()
}
}
}
return item
}
companion object {
val NAMES: JsonReader.Options = JsonReader.Options.of(
"id", "feed_id", "title", "author", "html", "url",

View File

@ -2,7 +2,6 @@ package com.readrops.api.services.fever.adapters
import android.annotation.SuppressLint
import com.readrops.api.utils.exceptions.ParseException
import com.readrops.api.utils.extensions.skipField
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.ToJson
@ -17,12 +16,14 @@ class FeverItemsIdsAdapter {
fun fromJson(reader: JsonReader): List<String> = with(reader) {
return try {
beginObject()
repeat(3) {
skipField()
}
nextName() // (unread|saved)_item_ids field
val ids = nextString().split(",")
val ids = arrayListOf<String>()
while (hasNext()) {
when (nextName()) {
"unread_item_ids" -> ids.addAll(nextString().split(","))
else -> skipValue()
}
}
endObject()
ids

View File

@ -4,8 +4,8 @@ import com.readrops.api.TestUtils
import com.squareup.moshi.Moshi
import okio.Buffer
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class FeverAPIAdapterTest {
@ -18,15 +18,13 @@ class FeverAPIAdapterTest {
fun authenticatedTest() {
val stream = TestUtils.loadResource("services/fever/successful_auth.json")
val value = adapter.fromJson(Buffer().readFrom(stream))!!
assertEquals(value, true)
assertTrue { adapter.fromJson(Buffer().readFrom(stream))!! }
}
@Test
fun unauthenticatedTest() {
val stream = TestUtils.loadResource("services/fever/unsuccessful_auth.json")
val value = adapter.fromJson(Buffer().readFrom(stream))!!
assertFalse { value }
assertFalse { adapter.fromJson(Buffer().readFrom(stream))!! }
}
}

View File

@ -24,7 +24,7 @@ class FeverFaviconsAdapterTest {
assertEquals(favicons.size, 3)
with(favicons[0]) {
with(favicons.first()) {
assertEquals(id, 85)
assertNotNull(data)
}

View File

@ -21,7 +21,7 @@ class FeverFeedsAdapterTest {
assertEquals(feverFeeds.feeds.size, 1)
with(feverFeeds.feeds[0]) {
with(feverFeeds.feeds.first()) {
assertEquals(name, "xda-developers")
assertEquals(url, "https://www.xda-developers.com/feed/")
assertEquals(siteUrl, "https://www.xda-developers.com/")

View File

@ -21,7 +21,7 @@ class FeverFoldersAdapterTest {
val folders = adapter.fromJson(Buffer().readFrom(stream))!!
with(folders[0]) {
with(folders.first()) {
assertEquals(name, "Libre")
assertEquals(remoteId, "4")
}

View File

@ -23,7 +23,7 @@ class FeverItemsAdapterTest {
val items = adapter.fromJson(Buffer().readFrom(stream))!!
with(items[0]) {
with(items.first()) {
assertEquals(title, "FreshRSS 1.9.0")
assertEquals(author, "Alkarex")
assertEquals(link, "https://github.com/FreshRSS/FreshRSS/releases/tag/1.9.0")

View File

@ -12,8 +12,8 @@ android {
defaultConfig {
applicationId = "com.readrops.app"
versionCode = 19
versionName = "2.0.2"
versionCode = 20
versionName = "2.0.3"
}
buildTypes {

View File

@ -162,7 +162,7 @@ class NextcloudNewsRepository(
itemsFeedsIds[item.feedRemoteId] = feedId
}
if (!initialSync && feedId > 0 && database.itemDao().itemExists(item.remoteId!!, feedId)) {
if (!initialSync && feedId > 0 && database.itemDao().itemExists(item.remoteId!!, account.id)) {
database.itemDao()
.updateReadAndStarState(item.remoteId!!, item.isRead, item.isStarred)
continue

View File

@ -67,6 +67,7 @@ interface ItemDao : BaseDao<Item> {
fun selectFeedUnreadItemsCount(query: SupportSQLiteQuery):
Flow<Map<@MapColumn(columnName = "feed_id") Int, @MapColumn(columnName = "item_count") Int>>
@Query("Select case When :remoteId In (Select Item.remote_id From Item Inner Join Feed on Item.feed_id = Feed.id and account_id = :accountId) Then 1 else 0 end")
@Query("""Select case When Exists(Select 1 From Item Inner Join Feed on Item.feed_id = Feed.id
Where Item.remote_id = :remoteId And account_id = :accountId) Then 1 else 0 end""")
suspend fun itemExists(remoteId: String, accountId: Int): Boolean
}

View File

@ -0,0 +1,3 @@
* Fix Fever API compatibility with TinyTiny RSS and yarr, should also fix other providers (#228 + #229)
* Fix Nextcloud News item duplicates when syncing which would made the app unusable
* Fix Nextcloud News item parsing: items with no title will be ignored