Skip to content

Commit

Permalink
Added display of article image inside reader
Browse files Browse the repository at this point in the history
  • Loading branch information
spacecowboy committed Dec 2, 2023
1 parent 694ad69 commit 4efc9d7
Show file tree
Hide file tree
Showing 23 changed files with 1,297 additions and 250 deletions.
755 changes: 755 additions & 0 deletions app/schemas/com.nononsenseapps.feeder.db.room.AppDatabase/32.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.nononsenseapps.feeder.db.room

import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import com.nononsenseapps.feeder.FeederApplication
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import kotlin.test.assertEquals

@RunWith(AndroidJUnit4::class)
@LargeTest
class TestMigrationFrom31To32 : DIAware {
private val dbName = "testDb"
private val feederApplication: FeederApplication = ApplicationProvider.getApplicationContext()
override val di: DI by closestDI(feederApplication)

@Rule
@JvmField
val testHelper: MigrationTestHelper =
MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
emptyList(),
FrameworkSQLiteOpenHelperFactory(),
)

@Test
fun migrate() {
@Suppress("SimpleRedundantLet")
testHelper.createDatabase(dbName, FROM_VERSION).let { oldDB ->
oldDB.execSQL(
"""
INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash, fulltext_by_default, open_articles_with, alternate_id, currently_syncing, when_modified, site_fetched)
VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666, 0, '', 0, 0, 0, 0)
""".trimIndent(),
)
oldDB.execSQL(
"""
INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked, fulltext_downloaded, read_time, unread, word_count, word_count_full)
VALUES(8, 'http://item1', 'title', 'ptitle', 'psnippet', 0, 1, 0, 0, 1, 0, 0, 0, 1, 5, 900)
""".trimIndent(),
)
}
val db =
testHelper.runMigrationsAndValidate(
dbName,
TO_VERSION,
true,
MigrationFrom31To32(di),
)

db.query(
"""
select image_from_body from feed_items
""".trimIndent(),
).use {
assert(it.count == 1)
assert(it.moveToFirst())
assertEquals(0, it.getInt(0))
}
}

companion object {
private const val FROM_VERSION = 31
private const val TO_VERSION = 32
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,8 @@ data class Article(
val bookmarked: Boolean = item?.bookmarked ?: false
val wordCount: Int = item?.wordCount ?: 0
val wordCountFull: Int = item?.wordCountFull ?: 0
val image: String? = item?.imageUrl
val imageFromBody: Boolean = item?.imageFromBody ?: false
}

enum class TextToDisplay {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const val COL_GUID = "guid"
const val COL_PLAINTITLE = "plain_title"
const val COL_PLAINSNIPPET = "plain_snippet"
const val COL_IMAGEURL = "image_url"
const val COL_IMAGE_FROM_BODY = "image_from_body"
const val COL_ENCLOSURELINK = "enclosure_link"
const val COL_ENCLOSURE_TYPE = "enclosure_type"
const val COL_LINK = "link"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private const val LOG_TAG = "FEEDER_APPDB"
RemoteFeed::class,
SyncDevice::class,
],
version = 31,
version = 32,
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
Expand Down Expand Up @@ -126,12 +126,23 @@ fun getAllMigrations(di: DI) =
MigrationFrom28To29(di),
MigrationFrom29To30(di),
MigrationFrom30To31(di),
MigrationFrom31To32(di),
)

/*
* 6 represents legacy database
* 7 represents new Room database
*/
class MigrationFrom31To32(override val di: DI) : Migration(31, 32), DIAware {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
alter table feed_items add column image_from_body integer not null default 0
""".trimIndent(),
)
}
}

class MigrationFrom30To31(override val di: DI) : Migration(30, 31), DIAware {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
Expand Down
25 changes: 9 additions & 16 deletions app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.nononsenseapps.feeder.db.COL_FULLTEXT_DOWNLOADED
import com.nononsenseapps.feeder.db.COL_GUID
import com.nononsenseapps.feeder.db.COL_ID
import com.nononsenseapps.feeder.db.COL_IMAGEURL
import com.nononsenseapps.feeder.db.COL_IMAGE_FROM_BODY
import com.nononsenseapps.feeder.db.COL_LINK
import com.nononsenseapps.feeder.db.COL_NOTIFIED
import com.nononsenseapps.feeder.db.COL_PLAINSNIPPET
Expand All @@ -27,11 +28,10 @@ import com.nononsenseapps.feeder.db.COL_TITLE
import com.nononsenseapps.feeder.db.COL_WORD_COUNT
import com.nononsenseapps.feeder.db.COL_WORD_COUNT_FULL
import com.nononsenseapps.feeder.db.FEED_ITEMS_TABLE_NAME
import com.nononsenseapps.feeder.model.ParsedArticle
import com.nononsenseapps.feeder.model.ParsedFeed
import com.nononsenseapps.feeder.model.host
import com.nononsenseapps.feeder.ui.text.HtmlToPlainTextConverter
import com.nononsenseapps.feeder.util.relativeLinkIntoAbsolute
import com.nononsenseapps.feeder.util.sloppyLinkToStrictURL
import com.nononsenseapps.jsonfeed.Item
import java.net.URI
import java.time.Instant
import java.time.ZoneOffset
Expand Down Expand Up @@ -75,6 +75,7 @@ data class FeedItem
@ColumnInfo(name = COL_PLAINTITLE) var plainTitle: String = "",
@ColumnInfo(name = COL_PLAINSNIPPET) var plainSnippet: String = "",
@ColumnInfo(name = COL_IMAGEURL) var imageUrl: String? = null,
@ColumnInfo(name = COL_IMAGE_FROM_BODY) var imageFromBody: Boolean = false,
@ColumnInfo(name = COL_ENCLOSURELINK) var enclosureLink: String? = null,
@ColumnInfo(name = COL_ENCLOSURE_TYPE) var enclosureType: String? = null,
@ColumnInfo(name = COL_AUTHOR) var author: String? = null,
Expand Down Expand Up @@ -117,9 +118,9 @@ data class FeedItem
get() = readTime == null

fun updateFromParsedEntry(
entry: Item,
entry: ParsedArticle,
entryGuid: String,
feed: com.nononsenseapps.jsonfeed.Feed,
feed: ParsedFeed,
) {
val converter = HtmlToPlainTextConverter()
// Be careful about nulls.
Expand All @@ -141,26 +142,18 @@ data class FeedItem
// Make double sure no base64 images are used as thumbnails
val safeImage =
when {
entry.image?.startsWith("data") == true -> null
entry.image?.url?.startsWith("data") == true -> null
else -> entry.image
}

val absoluteImage =
when {
feed.feed_url != null && safeImage != null -> {
relativeLinkIntoAbsolute(sloppyLinkToStrictURL(feed.feed_url), safeImage)
}

else -> safeImage
}

this.guid = entryGuid
entry.title?.let { this.plainTitle = it.take(MAX_TITLE_LENGTH) }
@Suppress("DEPRECATION")
this.title = this.plainTitle
this.plainSnippet = summary

this.imageUrl = absoluteImage
this.imageUrl = safeImage?.url
this.imageFromBody = safeImage?.fromBody ?: false
val firstEnclosure = entry.attachments?.firstOrNull()
this.enclosureLink = firstEnclosure?.url
this.enclosureType = firstEnclosure?.mime_type?.lowercase()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.nononsenseapps.feeder.db.COL_FULLTEXT_BY_DEFAULT
import com.nononsenseapps.feeder.db.COL_GUID
import com.nononsenseapps.feeder.db.COL_ID
import com.nononsenseapps.feeder.db.COL_IMAGEURL
import com.nononsenseapps.feeder.db.COL_IMAGE_FROM_BODY
import com.nononsenseapps.feeder.db.COL_LINK
import com.nononsenseapps.feeder.db.COL_PLAINSNIPPET
import com.nononsenseapps.feeder.db.COL_PLAINTITLE
Expand Down Expand Up @@ -44,7 +45,8 @@ const val feedItemColumnsWithFeed = """
$FEEDS_TABLE_NAME.$COL_FULLTEXT_BY_DEFAULT AS $COL_FULLTEXT_BY_DEFAULT,
$COL_BOOKMARKED,
$COL_WORD_COUNT,
$COL_WORD_COUNT_FULL
$COL_WORD_COUNT_FULL,
$COL_IMAGE_FROM_BODY
"""

data class FeedItemWithFeed
Expand All @@ -57,6 +59,7 @@ data class FeedItemWithFeed
@ColumnInfo(name = COL_PLAINTITLE) var plainTitle: String = "",
@ColumnInfo(name = COL_PLAINSNIPPET) var plainSnippet: String = "",
@ColumnInfo(name = COL_IMAGEURL) var imageUrl: String? = null,
@ColumnInfo(name = COL_IMAGE_FROM_BODY) var imageFromBody: Boolean = false,
@ColumnInfo(name = COL_ENCLOSURELINK) var enclosureLink: String? = null,
@ColumnInfo(name = COL_ENCLOSURE_TYPE) var enclosureType: String? = null,
var author: String? = null,
Expand Down
67 changes: 59 additions & 8 deletions app/src/main/java/com/nononsenseapps/feeder/model/FeedParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import com.nononsenseapps.feeder.util.flatMap
import com.nononsenseapps.feeder.util.relativeLinkIntoAbsolute
import com.nononsenseapps.feeder.util.relativeLinkIntoAbsoluteOrThrow
import com.nononsenseapps.feeder.util.sloppyLinkToStrictURLOrNull
import com.nononsenseapps.jsonfeed.Attachment
import com.nononsenseapps.jsonfeed.Author
import com.nononsenseapps.jsonfeed.Feed
import com.nononsenseapps.jsonfeed.Item
import com.nononsenseapps.jsonfeed.JsonFeedParser
import com.rometools.rome.io.SyndFeedInput
import com.rometools.rome.io.XmlReader
Expand Down Expand Up @@ -204,7 +207,7 @@ class FeedParser(override val di: DI) : DIAware {
*/
private suspend fun curl(url: URL) = client.curl(url)

suspend fun parseFeedUrl(url: URL): Either<FeedParserError, Feed> {
suspend fun parseFeedUrl(url: URL): Either<FeedParserError, ParsedFeed> {
return client.curlAndOnResponse(url) {
parseFeedResponse(it)
}
Expand All @@ -214,7 +217,7 @@ class FeedParser(override val di: DI) : DIAware {
}
}

internal fun parseFeedResponse(response: Response): Either<FeedParserError, Feed> {
internal fun parseFeedResponse(response: Response): Either<FeedParserError, ParsedFeed> {
return response.body?.use {
// OkHttp string method handles BOM and Content-Type header in request
parseFeedResponse(
Expand All @@ -230,15 +233,15 @@ class FeedParser(override val di: DI) : DIAware {
fun parseFeedResponse(
url: URL,
responseBody: ResponseBody,
): Either<FeedParserError, Feed> {
): Either<FeedParserError, ParsedFeed> {
return when (responseBody.contentType()?.subtype?.contains("json")) {
true ->
Either.catching(
onCatch = { t ->
JsonFeedParseError(url = url.toString(), throwable = t)
},
) {
jsonFeedParser.parseJson(responseBody)
jsonFeedParser.parseJson(responseBody).asParsedFeed()
}

else -> parseRssAtom(url, responseBody)
Expand All @@ -260,15 +263,15 @@ class FeedParser(override val di: DI) : DIAware {
url: URL,
body: String,
contentType: MediaType?,
): Either<FeedParserError, Feed> {
): Either<FeedParserError, ParsedFeed> {
return when (contentType?.subtype?.contains("json")) {
true ->
Either.catching(
onCatch = { t ->
JsonFeedParseError(url = url.toString(), throwable = t)
},
) {
jsonFeedParser.parseJson(body)
jsonFeedParser.parseJson(body).asParsedFeed()
}

else -> parseRssAtom(url, body)
Expand All @@ -286,7 +289,7 @@ class FeedParser(override val di: DI) : DIAware {
private fun parseRssAtom(
url: URL,
responseBody: ResponseBody,
): Either<FeedParserError, Feed> {
): Either<FeedParserError, ParsedFeed> {
val contentType = responseBody.contentType()
val validMimeType =
when (contentType?.type) {
Expand Down Expand Up @@ -334,7 +337,7 @@ class FeedParser(override val di: DI) : DIAware {
internal fun parseRssAtom(
baseUrl: URL,
body: String,
): Either<FeedParserError, Feed> {
): Either<FeedParserError, ParsedFeed> {
return Either.catching(
onCatch = { t ->
RSSParseError(url = baseUrl.toString(), throwable = t)
Expand All @@ -359,6 +362,54 @@ class FeedParser(override val di: DI) : DIAware {
}
}

private fun Feed.asParsedFeed() =
ParsedFeed(
title = title,
home_page_url = home_page_url,
feed_url = feed_url,
description = description,
user_comment = user_comment,
next_url = next_url,
icon = icon,
favicon = favicon,
author = author?.asParsedAuthor(),
expired = expired,
items = items?.map { it.asParsedArticle() },
)

private fun Item.asParsedArticle() =
ParsedArticle(
id = id,
url = url,
external_url = external_url,
title = title,
content_html = content_html,
content_text = content_text,
summary = summary,
image = image?.let { MediaImage(url = it, width = null, height = null) },
date_published = date_published,
date_modified = date_modified,
author = author?.asParsedAuthor(),
tags = tags,
attachments = attachments?.map { it.asParsedEnclosure() },
)

private fun Attachment.asParsedEnclosure() =
ParsedEnclosure(
title = title,
url = url,
mime_type = mime_type,
size_in_bytes = size_in_bytes,
duration_in_seconds = duration_in_seconds,
)

private fun Author?.asParsedAuthor() =
ParsedAuthor(
name = this?.name,
url = this?.url,
avatar = this?.avatar,
)

class FeedParsingError(val url: URL, e: Throwable) : Exception(e.message, e)

suspend fun OkHttpClient.getResponse(
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/com/nononsenseapps/feeder/model/ParsedArticle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.nononsenseapps.feeder.model

data class ParsedArticle(
val id: String?,
val url: String? = null,
val external_url: String? = null,
val title: String? = null,
val content_html: String? = null,
val content_text: String? = null,
val summary: String? = null,
val image: ThumbnailImage? = null,
val date_published: String? = null,
val date_modified: String? = null,
val author: ParsedAuthor? = null,
val tags: List<String>? = null,
val attachments: List<ParsedEnclosure>? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.nononsenseapps.feeder.model

data class ParsedAuthor(
val name: String? = null,
val url: String? = null,
val avatar: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.nononsenseapps.feeder.model

data class ParsedEnclosure(
val url: String?,
val mime_type: String? = null,
val title: String? = null,
val size_in_bytes: Long? = null,
val duration_in_seconds: Long? = null,
)
Loading

0 comments on commit 4efc9d7

Please sign in to comment.