Skip to content

[FreshRSS] Add support for image enclosures #1097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1037.0)
aws-sdk-core (3.215.1)
aws-eventstream (1.3.2)
aws-partitions (1.1090.0)
aws-sdk-core (3.222.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
logger
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.177.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-s3 (1.183.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
Expand All @@ -35,7 +37,7 @@ GEM
highline (~> 2.0.0)
csv (3.3.2)
declarative (0.0.20)
digest-crc (0.6.5)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
Expand Down Expand Up @@ -131,12 +133,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
Expand All @@ -154,9 +156,10 @@ GEM
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.8.3)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.9.1)
json (2.10.2)
jwt (2.10.1)
base64
logger (1.6.4)
Expand All @@ -178,7 +181,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.0)
rexml (3.4.1)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
Expand Down Expand Up @@ -210,7 +213,7 @@ GEM
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.capyreader.app.ui.articles

import com.capyreader.app.preferences.AppPreferences
import com.capyreader.app.ui.articles.feeds.edit.EditFeedViewModel
import com.jocmp.capy.articles.ArticleRenderer
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.capyreader.app.ui.articles
package com.capyreader.app.ui.articles.feeds.edit

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.capyreader.app.ui.articles
package com.capyreader.app.ui.articles.feeds.edit

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.capyreader.app.ui.articles
package com.capyreader.app.ui.articles.feeds.edit

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.capyreader.app.ui.articles
package com.capyreader.app.ui.articles.feeds.edit

import androidx.lifecycle.ViewModel
import com.capyreader.app.preferences.AppPreferences
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.res.stringResource
import com.capyreader.app.R
import com.capyreader.app.ui.articles.EditFeedDialog
import com.capyreader.app.ui.articles.feeds.edit.EditFeedDialog
import com.capyreader.app.ui.articles.FeedActionMenuItems
import com.capyreader.app.ui.articles.RemoveFeedDialog
import com.capyreader.app.ui.settings.localSnackbarDisplay
Expand Down
13 changes: 8 additions & 5 deletions capy/src/main/java/com/jocmp/capy/Account.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.jocmp.capy.db.Database
import com.jocmp.capy.opml.ImportProgress
import com.jocmp.capy.opml.OPMLImporter
import com.jocmp.capy.persistence.ArticleRecords
import com.jocmp.capy.persistence.EnclosureRecords
import com.jocmp.capy.persistence.FeedRecords
import com.jocmp.capy.persistence.FolderRecords
import com.jocmp.capy.persistence.SavedSearchRecords
Expand Down Expand Up @@ -69,10 +70,11 @@ data class Account(
)
}
) {
internal val articleRecords: ArticleRecords = ArticleRecords(database)
private val feedRecords: FeedRecords = FeedRecords(database)
private val folderRecords: FolderRecords = FolderRecords(database)
private val taggingRecords: TaggingRecords = TaggingRecords(database)
internal val articleRecords = ArticleRecords(database)
private val enclosureRecords = EnclosureRecords(database)
private val feedRecords = FeedRecords(database)
private val folderRecords = FolderRecords(database)
private val taggingRecords = TaggingRecords(database)
private val savedSearchRecords = SavedSearchRecords(database)

private val articleContent = ArticleContent(httpClient = localHttpClient)
Expand Down Expand Up @@ -204,7 +206,8 @@ data class Account(
return null
}

return articleRecords.find(articleID = articleID)
val enclosures = enclosureRecords.byArticle(articleID)
return articleRecords.find(articleID = articleID)?.copy(enclosures = enclosures)
}

suspend fun addStar(articleID: String): Result<Unit> {
Expand Down
1 change: 1 addition & 0 deletions capy/src/main/java/com/jocmp/capy/Article.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ data class Article(
val enableStickyFullContent: Boolean = false,
val fullContent: FullContentState = FullContentState.NONE,
val content: String = contentHTML.ifBlank { summary },
val enclosures: List<Enclosure> = emptyList(),
) {
val defaultContent = contentHTML.ifBlank { summary }

Expand Down
10 changes: 10 additions & 0 deletions capy/src/main/java/com/jocmp/capy/Enclosure.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.jocmp.capy

import java.net.URL

data class Enclosure(
val url: URL,
val type: String,
val itunesDurationSeconds: Long?,
val itunesImage: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.jocmp.capy.common.transactionWithErrorHandling
import com.jocmp.capy.common.withResult
import com.jocmp.capy.db.Database
import com.jocmp.capy.persistence.ArticleRecords
import com.jocmp.capy.persistence.EnclosureRecords
import com.jocmp.capy.persistence.FeedRecords
import com.jocmp.capy.persistence.SavedSearchRecords
import com.jocmp.capy.persistence.TaggingRecords
Expand Down Expand Up @@ -43,6 +44,7 @@ internal class FeedbinAccountDelegate(
private val feedbin: Feedbin
) : AccountDelegate {
private val articleRecords = ArticleRecords(database)
private val enclosureRecords = EnclosureRecords(database)
private val feedRecords = FeedRecords(database)
private val taggingRecords = TaggingRecords(database)
private val savedSearchRecords = SavedSearchRecords(database)
Expand Down Expand Up @@ -379,6 +381,16 @@ internal class FeedbinAccountDelegate(
read = true
)

entry.enclosure?.let {
enclosureRecords.create(
url = it.enclosure_url,
type = it.enclosure_type,
articleID = articleID,
itunesDurationSeconds = it.itunes_duration,
itunesImage = it.itunes_image,
)
}

if (savedSearchID != null) {
savedSearchRecords.upsertArticle(
articleID = articleID,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.jocmp.capy.accounts.reader

import com.jocmp.capy.Enclosure
import com.jocmp.capy.common.optionalURL
import com.jocmp.capy.common.unescapingHTMLCharacters
import com.jocmp.readerclient.Item
import org.jsoup.Jsoup

internal object EnclosureParsing {
internal fun parsedImageURL(item: Item): String? {
val imageHref = item.images.firstOrNull()?.href

if (imageHref != null) {
return imageHref.unescapingHTMLCharacters
}

val content = item.summary.content

return Jsoup.parse(content).selectFirst("img")?.attr("src")
}

internal fun validEnclosures(item: Item): List<Enclosure> {
return item.enclosure.orEmpty().mapNotNull(::toEnclosure)
}

private fun toEnclosure(enclosure: Item.Enclosure): Enclosure? {
val type = enclosure.type ?: return null
val url = optionalURL(enclosure.href?.unescapingHTMLCharacters) ?: return null

return Enclosure(url = url, type = type, itunesDurationSeconds = null, itunesImage = null)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.jocmp.capy.common.withResult
import com.jocmp.capy.db.Database
import com.jocmp.capy.logging.CapyLog
import com.jocmp.capy.persistence.ArticleRecords
import com.jocmp.capy.persistence.EnclosureRecords
import com.jocmp.capy.persistence.FeedRecords
import com.jocmp.capy.persistence.SavedSearchRecords
import com.jocmp.capy.persistence.TaggingRecords
Expand Down Expand Up @@ -47,6 +48,7 @@ internal class ReaderAccountDelegate(
private var postToken = AtomicReference<String?>(null)
private val articleRecords = ArticleRecords(database)
private val feedRecords = FeedRecords(database)
private val enclosureRecords = EnclosureRecords(database)
private val taggingRecords = TaggingRecords(database)
private val savedSearchRecords = SavedSearchRecords(database)

Expand Down Expand Up @@ -445,7 +447,7 @@ internal class ReaderAccountDelegate(
extracted_content_url = null,
summary = Jsoup.parse(item.summary.content).text(),
url = item.canonical.firstOrNull()?.href,
image_url = parsedImageURL(item),
image_url = EnclosureParsing.parsedImageURL(item),
published_at = item.published
)

Expand All @@ -464,6 +466,14 @@ internal class ReaderAccountDelegate(
)
}
}

EnclosureParsing.validEnclosures(item).forEach {
enclosureRecords.create(
url = it.url.toString(),
type = it.type,
articleID = item.hexID,
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.jocmp.capy.articles

import com.jocmp.capy.Article
import org.jsoup.nodes.Element

fun Article.imageEnclosures(): Element? {
val images = enclosures.filter { it.type.startsWith("image/") }

if (images.isEmpty()) {
return null
}

return Element("div").apply {
enclosures.forEach { enclosure ->
val image = Element("img").apply {
attr("src", enclosure.url.toString())
}

appendChild(image)
}
}
}
13 changes: 10 additions & 3 deletions capy/src/main/java/com/jocmp/capy/articles/ArticleRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.jocmp.capy.Article
import com.jocmp.capy.MacroProcessor
import com.jocmp.capy.preferences.Preference
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import com.jocmp.capy.R as CapyRes

class ArticleRenderer(
Expand Down Expand Up @@ -67,14 +68,17 @@ class ArticleRenderer(

HtmlPostProcessor.clean(contentHTML, hideImages = hideImages)

document.getElementById("article-body-content")
?.append(parseHtml(article, contentHTML, hideImages = hideImages))
document.content?.append(parseHtml(article, contentHTML, hideImages = hideImages))
} else {
document.getElementById("article-body-content")?.append(article.content)
document.content?.append(article.content)

HtmlPostProcessor.clean(document, hideImages = hideImages)
}

article.imageEnclosures()?.let {
document.content?.appendChild(it)
}

return document.html()
}

Expand Down Expand Up @@ -104,6 +108,9 @@ class ArticleRenderer(
}
}

private val Document.content
get() = getElementById("article-body-content")

private fun Article.externalLink(): String {
val potentialURL = url ?: siteURL

Expand Down
Loading