Improve icons quality for local account feeds

This commit is contained in:
Shinokuni 2024-11-18 17:57:07 +01:00
parent db04cdddb7
commit 6459957168
5 changed files with 229 additions and 685 deletions

View File

@ -3,7 +3,6 @@ package com.readrops.api.localfeed
import com.gitlab.mvysny.konsumexml.Konsumer
import com.gitlab.mvysny.konsumexml.Names
import com.readrops.api.utils.extensions.checkRoot
import java.io.InputStream
object LocalRSSHelper {
@ -26,12 +25,11 @@ object LocalRSSHelper {
RSS_1_CONTENT_TYPE -> RSSType.RSS_1
RSS_2_CONTENT_TYPE -> RSSType.RSS_2
ATOM_CONTENT_TYPE -> RSSType.ATOM
JSON_CONTENT_TYPE, JSONFEED_CONTENT_TYPE -> RSSType.JSONFEED
JSONFEED_CONTENT_TYPE -> RSSType.JSONFEED
else -> RSSType.UNKNOWN
}
}
@JvmStatic
fun isRSSType(type: String?): Boolean =
if (type != null) getRSSType(type) != RSSType.UNKNOWN else false

View File

@ -2,10 +2,13 @@ package com.readrops.api.utils
import android.nfc.FormatException
import com.readrops.api.localfeed.LocalRSSHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
data class ParsingResult(
val url: String,
@ -14,75 +17,96 @@ data class ParsingResult(
object HtmlParser {
@Throws(FormatException::class)
suspend fun getFaviconLink(url: String, client: OkHttpClient): String? {
val document = getHTMLHeadFromUrl(url, client)
val elements = document.select("link")
for (element in elements) {
if (element.attributes()["rel"].lowercase().contains("icon")) {
return element.absUrl("href")
}
}
val links = document.select("link")
.filter { element -> element.attributes()["rel"].contains("icon") }
.sortedWith(compareByDescending<Element> {
it.attributes()["rel"] == "apple-touch-icon"
}.thenByDescending { element ->
val sizes = element.attr("sizes")
return null
if (sizes.isNotEmpty()) {
try {
sizes.filter { it.isDigit() }
.toInt()
} catch (e: Exception) {
0
}
} else {
0
}
})
return links.firstOrNull()
?.absUrl("href")
}
@Throws(FormatException::class)
suspend fun getFeedLink(url: String, client: OkHttpClient): List<ParsingResult> {
val results = mutableListOf<ParsingResult>()
val document = getHTMLHeadFromUrl(url, client)
val elements = document.select("link")
for (element in elements) {
val type = element.attributes()["type"]
if (LocalRSSHelper.isRSSType(type)) {
results += ParsingResult(
url = element.absUrl("href"),
label = element.attributes()["title"]
return document.select("link")
.filter { element ->
val type = element.attributes()["type"]
LocalRSSHelper.isRSSType(type)
}.map {
ParsingResult(
url = it.absUrl("href"),
label = it.attributes()["title"]
)
}
}
return results
}
private fun getHTMLHeadFromUrl(url: String, client: OkHttpClient): Document {
client.newCall(Request.Builder().url(url).build()).execute().use { response ->
if (response.header(ApiUtils.CONTENT_TYPE_HEADER)!!.contains(ApiUtils.HTML_CONTENT_TYPE)
) {
val body = response.body!!.source()
private suspend fun getHTMLHeadFromUrl(url: String, client: OkHttpClient): Document =
withContext(Dispatchers.IO) {
client.newCall(
Request.Builder()
.url(url)
.build()
).execute()
.use { response ->
if (response.header(ApiUtils.CONTENT_TYPE_HEADER)!!
.contains(ApiUtils.HTML_CONTENT_TYPE)
) {
val body = response.body!!.source()
val stringBuilder = StringBuilder()
var collectionStarted = false
val stringBuilder = StringBuilder()
var collectionStarted = false
while (!body.exhausted()) {
val currentLine = body.readUtf8LineStrict()
while (!body.exhausted()) {
val currentLine = body.readUtf8LineStrict()
when {
currentLine.contains("<head>") -> {
stringBuilder.append(currentLine)
collectionStarted = true
when {
currentLine.contains("<head>") -> {
stringBuilder.append(currentLine)
collectionStarted = true
}
currentLine.contains("</head>") -> {
stringBuilder.append(currentLine)
break
}
collectionStarted -> {
stringBuilder.append(currentLine)
}
}
}
currentLine.contains("</head>") -> {
stringBuilder.append(currentLine)
break
}
collectionStarted -> {
stringBuilder.append(currentLine)
if (!stringBuilder.contains("<head>") || !stringBuilder.contains("</head>")) {
body.close()
throw FormatException("Failed to get HTML head from $url")
}
body.close()
Jsoup.parse(stringBuilder.toString(), url)
} else {
response.close()
throw FormatException("Response from $url is not a html file")
}
}
if (!stringBuilder.contains("<head>") || !stringBuilder.contains("</head>"))
throw FormatException("Failed to get HTML head")
body.close()
return Jsoup.parse(stringBuilder.toString(), url)
} else {
throw FormatException("The response is not a html file")
}
}
}
}

View File

@ -2,19 +2,21 @@ package com.readrops.api.utils
import android.nfc.FormatException
import com.readrops.api.TestUtils
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.Buffer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.koin.dsl.module
import org.koin.test.KoinTest
import org.koin.test.KoinTestRule
import org.koin.test.get
import java.net.HttpURLConnection
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
@ -34,18 +36,18 @@ class HtmlParserTest : KoinTest {
})
}
@Test
@Before
fun before() {
mockServer.start()
}
@Test
@After
fun after() {
mockServer.shutdown()
}
@Test
fun getFeedLinkTest() {
fun getFeedLinkTest() = runTest {
val stream = TestUtils.loadResource("utils/file.html")
mockServer.enqueue(
@ -54,19 +56,14 @@ class HtmlParserTest : KoinTest {
.setBody(Buffer().readFrom(stream))
)
runBlocking {
val result =
HtmlParser.getFeedLink(mockServer.url("/rss").toString(), koinTestRule.koin.get())
val links = HtmlParser.getFeedLink(mockServer.url("/rss").toString(), get())
assertTrue { result.size == 1 }
assertTrue { result.first().url.endsWith("/rss") }
assertEquals("RSS", result.first().label)
}
assertTrue { links.size == 2 }
assertTrue { links.all { it.label!!.contains("The Mozilla Blog") } }
}
@Test(expected = FormatException::class)
fun getFeedLinkWithoutHeadTest() {
fun getFeedLinkWithoutHeadTest() = runTest {
val stream = TestUtils.loadResource("utils/file_without_head.html")
mockServer.enqueue(
@ -75,21 +72,21 @@ class HtmlParserTest : KoinTest {
.setBody(Buffer().readFrom(stream))
)
runBlocking { HtmlParser.getFeedLink(mockServer.url("/rss").toString(), koinTestRule.koin.get()) }
HtmlParser.getFeedLink(mockServer.url("/rss").toString(), get())
}
@Test(expected = FormatException::class)
fun getFeedLinkNoHtmlFileTest() {
fun getFeedLinkNoHtmlFileTest() = runTest {
mockServer.enqueue(
MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
.addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/rss+xml"))
.addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/rss+xml")
)
runBlocking { HtmlParser.getFeedLink(mockServer.url("/rss").toString(), koinTestRule.koin.get()) }
HtmlParser.getFeedLink(mockServer.url("/rss").toString(), get())
}
@Test
fun getFaviconLinkTest() {
fun getFaviconLinkTest() = runTest {
val stream = TestUtils.loadResource("utils/file.html")
mockServer.enqueue(
@ -98,15 +95,12 @@ class HtmlParserTest : KoinTest {
.setBody(Buffer().readFrom(stream))
)
runBlocking {
val result = HtmlParser.getFaviconLink(mockServer.url("/rss").toString(), koinTestRule.koin.get())
assertTrue { result!!.contains("favicon.ico") }
}
val link = HtmlParser.getFaviconLink(mockServer.url("/rss").toString(), get())
assertTrue { link!!.contains("apple-touch-icon") }
}
@Test
fun getFaviconLinkWithoutHeadTest() {
fun getFaviconLinkWithoutHeadTest() = runTest {
val stream = TestUtils.loadResource("utils/file_without_icon.html")
mockServer.enqueue(
@ -115,10 +109,7 @@ class HtmlParserTest : KoinTest {
.setBody(Buffer().readFrom(stream))
)
runBlocking {
val result = HtmlParser.getFaviconLink(mockServer.url("/rss").toString(), koinTestRule.koin.get())
assertNull(result)
}
val link = HtmlParser.getFaviconLink(mockServer.url("/rss").toString(), get())
assertNull(link)
}
}

File diff suppressed because one or more lines are too long

View File

@ -129,7 +129,7 @@ class LocalRSSRepository(
feedUrl?.let { color = FeedColors.getFeedColor(it) }
}
} catch (e: Exception) {
Log.d("LocalRSSRepository", "insertFeed: ${e.message}")
Log.e("LocalRSSRepository", "insertFeed: ${e.message}")
}
id = database.feedDao().insert(this).toInt()