MangaLivre: Migrate theme (#5826)

* Migrate theme

* Typo
This commit is contained in:
Chopper 2024-11-03 08:50:07 -03:00 committed by Draff
parent 404c86ac09
commit 5e42af04cb
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
10 changed files with 199 additions and 140 deletions

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.mangalivre.MangaLivreUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="mangalivre.one"
android:pathPattern="/manga/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,7 +1,7 @@
ext {
extName = 'Manga Livre'
extClass = '.MangaLivre'
extVersionCode = 1
extVersionCode = 2
isNsfw = true
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,23 +1,26 @@
package eu.kanade.tachiyomi.extension.pt.mangalivre
import android.annotation.SuppressLint
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class MangaLivre : HttpSource() {
/**
* Etoshore - Manga Theme
*/
class MangaLivre : ParsedHttpSource() {
override val name = "Manga Livre"
@ -25,126 +28,179 @@ class MangaLivre : HttpSource() {
override val lang = "pt-BR"
override val supportsLatest = false
override val supportsLatest = true
private val json: Json by injectLazy()
override val versionId = 2
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
// ============================== Popular ===============================
// ============================== Popular ==============================
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList())
private val popularFilter = FilterList(
SelectionList("", listOf(Tag(value = "views", query = "sort"))),
)
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun popularMangaSelector() = throw UnsupportedOperationException()
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException()
private val latestFilter = FilterList(
SelectionList("", listOf(Tag(value = "date", query = "sort"))),
)
override fun latestUpdatesParse(response: Response) =
throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$API_URL/manga".toHttpUrl().newBuilder()
.addQueryParameter("per_page", "50")
.addQueryParameter("page", "$page")
.addQueryParameter("name", query)
.build()
return GET(url, headers)
val url = "$baseUrl/page/$page".toHttpUrl().newBuilder()
.addQueryParameter("s", query)
filters.forEach { filter ->
when (filter) {
is SelectionList -> {
val selected = filter.selected()
url.addQueryParameter(selected.query, selected.value)
}
else -> {}
}
}
return GET(url.build(), headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.substringAfter(PREFIX_SEARCH)
return fetchMangaDetails(SManga.create().apply { url = "/manga/$slug/" })
.map { manga -> MangasPage(listOf(manga), false) }
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaSelector() = ".search-posts .chapter-box .poster a"
override fun searchMangaNextPageSelector() = ".navigation .naviright:has(a)"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.attr("title")
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(element.absUrl("href"))
}
override fun searchMangaParse(response: Response): MangasPage {
val page = response.parseAs<MangaLivreDto>()
val mangas = page.mangas.map {
SManga.create().apply {
title = it.name
description = it.synopsis
thumbnail_url = "$CDN_URL/${it.photo}"
url = "/manga/slug/${it.slug}#${it.id}"
}
if (filterList.isEmpty()) {
filterParse(response)
}
return MangasPage(mangas, page.hasNextPage())
return super.searchMangaParse(response)
}
// ============================== Details ===============================
override fun getMangaUrl(manga: SManga): String {
val slug = manga.url
.substringAfterLast("/")
.removeComment()
return "$baseUrl/manga/$slug"
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1")!!.text()
description = document.selectFirst(".excerpt p")?.text()
thumbnail_url = document.selectFirst(".details-right-con img")?.absUrl("src")
genre = document.select("div.meta-item span.meta-title:contains(Genres) + span a")
.joinToString { it.text() }
author = document.selectFirst("div.meta-item span.meta-title:contains(Author) + span a")
?.text()
document.selectFirst(".status")?.text()?.let {
status = it.toMangaStatus()
}
setUrlWithoutDomain(document.location())
}
override fun mangaDetailsRequest(manga: SManga) =
GET("$API_URL${manga.url.removeComment()}", headers)
override fun mangaDetailsParse(response: Response): SManga {
val mangaDto = response.parseAs<MangaDto>()
return SManga.create().apply {
title = mangaDto.name
description = mangaDto.synopsis
thumbnail_url = "$CDN_URL/${mangaDto.photo}"
genre = mangaDto.genre?.joinToString { it.value }
mangaDto.status?.let {
status = when (it.value) {
"Ativo" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
url = "/manga/slug/${mangaDto.slug}#${mangaDto.id}"
private fun String.toMangaStatus(): Int {
return when (this.lowercase().trim()) {
"publishing" -> SManga.ONGOING
"finished" -> SManga.COMPLETED
"discontinued" -> SManga.CANCELLED
"on hiatus" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
// ============================== Chapters ===============================
// ============================== Chapters ============================
override fun chapterListRequest(manga: SManga): Request {
val id = manga.url.substringAfterLast("#")
return GET("$API_URL/chapter/manga/all/$id", headers)
override fun chapterListSelector() = ".chapter-list li a"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".title")!!.text()
setUrlWithoutDomain(element.absUrl("href"))
}
override fun chapterListParse(response: Response): List<SChapter> {
return response.parseAs<List<ChapterDto>>().map {
SChapter.create().apply {
name = "${it.chapter} - ${it.title}"
date_upload = it.createdAt.toDate()
chapter_number = it.chapter.toFloat()
url = "/chapter/${it.id}"
}
}
}
private fun String.toDate(): Long = dateFormat.parse(this)?.time ?: 0L
override fun chapterListParse(response: Response) = super.chapterListParse(response).reversed()
// ============================== Pages ===============================
override fun pageListRequest(chapter: SChapter) = GET("$API_URL${chapter.url}", headers)
override fun pageListParse(response: Response): List<Page> {
return response.parseAs<MangaPageDto>().pages.mapIndexed { index, page ->
Page(index, imageUrl = "$CDN_URL/${page.url}")
override fun pageListParse(document: Document): List<Page> {
return document.select(".chapter-images .chapter-item img").mapIndexed { index, element ->
Page(index, imageUrl = element.absUrl("src"))
}
}
override fun imageUrlParse(response: Response) = ""
override fun imageUrlParse(document: Document) = ""
// ============================= Utilities ==============================
// ============================= Filters ==============================
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
private var filterList = emptyList<SelectionList>()
override fun getFilterList(): FilterList {
val filters = filterList.takeIf(List<*>::isNotEmpty)
?: listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros"))
return FilterList(filters)
}
private fun String.removeComment() = this.substringBeforeLast("#")
private fun parseSelection(document: Document, selector: String): SelectionList? {
val selectorFilter = "#filter-form $selector .select-item-head .text"
return document.selectFirst(selectorFilter)?.text()?.let { displayName ->
val values = document.select("#filter-form $selector li").map { element ->
element.selectFirst("input")!!.let { input ->
Tag(
name = element.selectFirst(".text")!!.text(),
value = input.attr("value"),
query = input.attr("name"),
)
}
}
SelectionList(displayName, values)
}
}
private val filterListSelector: List<String> = listOf(
".filter-genre",
".filter-status",
".filter-type",
".filter-sort",
)
private fun filterParse(response: Response) {
val document = Jsoup.parseBodyFragment(response.peekBody(Long.MAX_VALUE).string())
filterList = filterListSelector.mapNotNull { selector -> parseSelection(document, selector) }
}
private data class Tag(val name: String = "", val value: String = "", val query: String = "")
private open class SelectionList(displayName: String, private val vals: List<Tag>, state: Int = 0) :
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
fun selected() = vals[state]
}
companion object {
const val API_URL = "https://api.mangalivre.one"
const val CDN_URL = "https://cdn.mangalivre.one"
@SuppressLint("SimpleDateFormat")
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")
const val PREFIX_SEARCH = "id:"
}
}

View File

@ -1,56 +0,0 @@
package eu.kanade.tachiyomi.extension.pt.mangalivre
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class MangaLivreDto(
@SerialName("data")
val mangas: List<MangaDto>,
val path: String,
@SerialName("current_page")
val currentPage: Int,
@SerialName("last_page")
val lastPage: Int,
) {
fun hasNextPage(): Boolean = currentPage < lastPage
}
@Serializable
data class MangaDto(
val id: Int,
val name: String,
val photo: String,
val slug: String,
val synopsis: String,
val status: Name?,
@SerialName("categories")
val genre: List<Name>?,
)
@Serializable
data class Name(
@SerialName("name")
val value: String,
)
@Serializable
data class ChapterDto(
val id: Int,
val title: String,
val chapter: String,
@SerialName("created_at")
val createdAt: String,
)
@Serializable
data class MangaPageDto(
@SerialName("manga_pages")
val pages: List<PageDto>,
)
@Serializable
data class PageDto(
@SerialName("page")
val url: String,
)

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.extension.pt.mangalivre
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class MangaLivreUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${MangaLivre.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}