Merge branch 'feature/favicons'
commit
c5c84c8630
@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="TestsAndReport" type="CompoundRunConfigurationType">
|
||||||
|
<toRun name="MangaParserTest" type="GradleRunConfiguration" />
|
||||||
|
<toRun name="kotatsu-parsers [generateTestsReport]" type="GradleRunConfiguration" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
|
||||||
|
class Favicon internal constructor(
|
||||||
|
val url: String,
|
||||||
|
val size: Int,
|
||||||
|
internal val rel: String?,
|
||||||
|
) : Comparable<Favicon> {
|
||||||
|
|
||||||
|
val type: String = url.toHttpUrl().pathSegments.last()
|
||||||
|
.substringAfterLast('.', "").lowercase()
|
||||||
|
|
||||||
|
override fun compareTo(other: Favicon): Int {
|
||||||
|
val res = size.compareTo(other.size)
|
||||||
|
if (res != 0) {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return relWeightOf(rel).compareTo(relWeightOf(other.rel))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Favicon
|
||||||
|
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (size != other.size) return false
|
||||||
|
if (rel != other.rel) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = url.hashCode()
|
||||||
|
result = 31 * result + size
|
||||||
|
result = 31 * result + rel.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Favicon(size=$size, type='$type', rel='$rel', url='$url')"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun relWeightOf(rel: String?) = when (rel) {
|
||||||
|
"apple-touch-icon" -> 1 // Prefer apple-touch-icon because it has a better quality
|
||||||
|
"mask-icon" -> -1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
|
class Favicons internal constructor(
|
||||||
|
favicons: Collection<Favicon>,
|
||||||
|
val referer: String,
|
||||||
|
) : Collection<Favicon> {
|
||||||
|
|
||||||
|
private val icons = favicons.sortedDescending()
|
||||||
|
|
||||||
|
override val size: Int
|
||||||
|
get() = icons.size
|
||||||
|
|
||||||
|
override fun contains(element: Favicon): Boolean = icons.contains(element)
|
||||||
|
|
||||||
|
override fun containsAll(elements: Collection<Favicon>): Boolean = icons.containsAll(elements)
|
||||||
|
|
||||||
|
override fun isEmpty(): Boolean = icons.isEmpty()
|
||||||
|
|
||||||
|
override fun iterator(): Iterator<Favicon> = icons.iterator()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a favicon whose size in pixels is greater than or equal to the specified size.
|
||||||
|
* If such icon is not available returns the largest icon
|
||||||
|
* @param size in pixels
|
||||||
|
* @param types supported file types, e.g. png, svg, ico. May be null but not empty
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun find(size: Int, types: Set<String>? = null): Favicon? {
|
||||||
|
if (icons.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
var result: Favicon? = null
|
||||||
|
for (icon in icons) {
|
||||||
|
if (types != null && icon.type !in types) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (result == null || icon.size >= size) {
|
||||||
|
result = icon
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.util
|
||||||
|
|
||||||
|
import okhttp3.Headers
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Favicon
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Favicons
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||||
|
|
||||||
|
class FaviconParser(
|
||||||
|
private val context: MangaLoaderContext,
|
||||||
|
private val domain: String,
|
||||||
|
private val headers: Headers?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun parseFavicons(): Favicons {
|
||||||
|
val url = "https://$domain"
|
||||||
|
val doc = context.httpGet(url, headers).parseHtml()
|
||||||
|
val result = HashSet<Favicon>()
|
||||||
|
val manifestLink = doc.getElementsByAttributeValue("rel", "manifest").firstOrNull()
|
||||||
|
?.attrAsAbsoluteUrlOrNull("href")
|
||||||
|
if (manifestLink != null) {
|
||||||
|
result += parseManifest(manifestLink)
|
||||||
|
}
|
||||||
|
val links = doc.getElementsByAttributeValueContaining("rel", "icon")
|
||||||
|
links.mapNotNullTo(result) { link ->
|
||||||
|
parseLink(link)
|
||||||
|
}
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
result.add(createFallback())
|
||||||
|
}
|
||||||
|
return Favicons(result, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseLink(link: Element): Favicon? {
|
||||||
|
val href = link.attrAsAbsoluteUrlOrNull("href")
|
||||||
|
if (href == null || href.endsWith('/')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val sizes = link.attr("sizes")
|
||||||
|
return Favicon(
|
||||||
|
url = href,
|
||||||
|
size = parseSize(sizes),
|
||||||
|
rel = link.attrOrNull("rel"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseSize(sizes: String): Int {
|
||||||
|
if (sizes.isEmpty() || sizes == "any") {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return sizes.substringBefore(' ')
|
||||||
|
.split('x', 'X', '*')
|
||||||
|
.firstNotNullOfOrNull { it.toIntOrNull() }
|
||||||
|
?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseManifest(url: String): List<Favicon> {
|
||||||
|
val json = context.httpGet(url, headers).parseJson()
|
||||||
|
val icons = json.getJSONArray("icons")
|
||||||
|
return icons.mapJSON { jo ->
|
||||||
|
Favicon(
|
||||||
|
url = jo.getString("src").resolveLink(),
|
||||||
|
size = parseSize(jo.getString("sizes")),
|
||||||
|
rel = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFallback(): Favicon {
|
||||||
|
val href = "https://$domain/favicon.ico"
|
||||||
|
return Favicon(
|
||||||
|
url = href,
|
||||||
|
size = 0,
|
||||||
|
rel = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.resolveLink(): String {
|
||||||
|
return when {
|
||||||
|
startsWith("http:") || startsWith("https:") -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
startsWith('/') -> {
|
||||||
|
"https://$domain$this"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
"https://$domain/$this"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue