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 { ext {
extName = 'Manga Livre' extName = 'Manga Livre'
extClass = '.MangaLivre' extClass = '.MangaLivre'
extVersionCode = 1 extVersionCode = 2
isNsfw = true 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 package eu.kanade.tachiyomi.extension.pt.mangalivre
import android.annotation.SuppressLint
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy import org.jsoup.Jsoup
import java.text.SimpleDateFormat 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" override val name = "Manga Livre"
@ -25,126 +28,179 @@ class MangaLivre : HttpSource() {
override val lang = "pt-BR" 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() override val client = network.cloudflareClient.newBuilder()
.rateLimit(2) .rateLimit(2)
.build() .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 popularMangaParse(response: Response) = searchMangaParse(response)
override fun popularMangaSelector() = throw UnsupportedOperationException()
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Latest =============================== // ============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = private val latestFilter = FilterList(
throw UnsupportedOperationException() SelectionList("", listOf(Tag(value = "date", query = "sort"))),
)
override fun latestUpdatesParse(response: Response) = override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
throw UnsupportedOperationException() 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 =============================== // ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$API_URL/manga".toHttpUrl().newBuilder() val url = "$baseUrl/page/$page".toHttpUrl().newBuilder()
.addQueryParameter("per_page", "50") .addQueryParameter("s", query)
.addQueryParameter("page", "$page")
.addQueryParameter("name", query) filters.forEach { filter ->
.build() when (filter) {
return GET(url, headers) 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 { override fun searchMangaParse(response: Response): MangasPage {
val page = response.parseAs<MangaLivreDto>() if (filterList.isEmpty()) {
val mangas = page.mangas.map { filterParse(response)
SManga.create().apply {
title = it.name
description = it.synopsis
thumbnail_url = "$CDN_URL/${it.photo}"
url = "/manga/slug/${it.slug}#${it.id}"
}
} }
return MangasPage(mangas, page.hasNextPage()) return super.searchMangaParse(response)
} }
// ============================== Details =============================== // ============================== Details ===============================
override fun getMangaUrl(manga: SManga): String { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val slug = manga.url title = document.selectFirst("h1")!!.text()
.substringAfterLast("/") description = document.selectFirst(".excerpt p")?.text()
.removeComment() thumbnail_url = document.selectFirst(".details-right-con img")?.absUrl("src")
return "$baseUrl/manga/$slug" 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) = private fun String.toMangaStatus(): Int {
GET("$API_URL${manga.url.removeComment()}", headers) return when (this.lowercase().trim()) {
"publishing" -> SManga.ONGOING
override fun mangaDetailsParse(response: Response): SManga { "finished" -> SManga.COMPLETED
val mangaDto = response.parseAs<MangaDto>() "discontinued" -> SManga.CANCELLED
return SManga.create().apply { "on hiatus" -> SManga.ON_HIATUS
title = mangaDto.name else -> SManga.UNKNOWN
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}"
} }
} }
// ============================== Chapters =============================== // ============================== Chapters ============================
override fun chapterListRequest(manga: SManga): Request { override fun chapterListSelector() = ".chapter-list li a"
val id = manga.url.substringAfterLast("#")
return GET("$API_URL/chapter/manga/all/$id", headers) override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".title")!!.text()
setUrlWithoutDomain(element.absUrl("href"))
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response) = super.chapterListParse(response).reversed()
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
// ============================== Pages =============================== // ============================== Pages ===============================
override fun pageListRequest(chapter: SChapter) = GET("$API_URL${chapter.url}", headers) override fun pageListParse(document: Document): List<Page> {
return document.select(".chapter-images .chapter-item img").mapIndexed { index, element ->
override fun pageListParse(response: Response): List<Page> { Page(index, imageUrl = element.absUrl("src"))
return response.parseAs<MangaPageDto>().pages.mapIndexed { index, page ->
Page(index, imageUrl = "$CDN_URL/${page.url}")
} }
} }
override fun imageUrlParse(response: Response) = "" override fun imageUrlParse(document: Document) = ""
// ============================= Utilities ============================== // ============================= Filters ==============================
private inline fun <reified T> Response.parseAs(): T = use { private var filterList = emptyList<SelectionList>()
json.decodeFromStream(it.body.byteStream())
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 { companion object {
const val API_URL = "https://api.mangalivre.one" const val PREFIX_SEARCH = "id:"
const val CDN_URL = "https://cdn.mangalivre.one"
@SuppressLint("SimpleDateFormat")
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")
} }
} }

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)
}
}