New source: Blackout Comics (#325)

* feat: Create Blackout Comics base

* feat: Implement popular manga page

* feat: Implement latest updates page

* feat: Implement search page

* feat: Implement manga details page

* feat: Implement chapter list page

* feat: Implement page list

* chore: Add source icon
This commit is contained in:
Claudemirovsky 2024-01-17 16:41:10 -03:00 committed by Draff
parent 9a571a9625
commit 3616d62946
9 changed files with 249 additions and 0 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.blackoutcomics.BlackoutComicsUrlActivity"
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="blackoutcomics.com"
android:pathPattern="/comics/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,8 @@
ext {
extName = 'Blackout Comics'
extClass = '.BlackoutComics'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,178 @@
package eu.kanade.tachiyomi.extension.pt.blackoutcomics
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
class BlackoutComics : ParsedHttpSource() {
override val name = "Blackout Comics"
override val baseUrl = "https://blackoutcomics.com"
override val lang = "pt-BR"
override val supportsLatest = true
override val client by lazy {
network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
}
override fun headersBuilder() =
super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Accept-Language", "en-US,en;q=0.5")
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET("$baseUrl/ranking")
override fun popularMangaSelector() = "section > div.container div > a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img")?.absUrl("src")
title = element.selectFirst("p, span.text-comic")?.text() ?: "Manga"
}
override fun popularMangaNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/recentes")
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = null
// =============================== Search ===============================
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/comics/$id"))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
super.fetchSearchManga(page, query, filters)
}
}
private fun searchMangaByIdParse(response: Response): MangasPage {
val details = mangaDetailsParse(response.use { it.asJsoup() })
return MangasPage(listOf(details), false)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// Using URLBuilder just to prevent issues with strange queries
val url = "$baseUrl/comics".toHttpUrl().newBuilder()
.addQueryParameter("search", query)
.build()
return GET(url, headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = null
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val row = document.selectFirst("section > div.container > div.row")!!
thumbnail_url = row.selectFirst("img")?.absUrl("src")
title = row.selectFirst("div.trailer-content > h2")?.text() ?: "Manga"
with(row.selectFirst("div.trailer-content:has(h3:containsOwn(Detalhes))")!!) {
println(outerHtml())
artist = getInfo("Artista")
author = getInfo("Autor")
genre = getInfo("Genêros")
status = when (getInfo("Status")) {
"Completo" -> SManga.COMPLETED
"Em Lançamento" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
description = buildString {
// Synopsis
row.selectFirst("h3:containsOwn(Descrição) + p")?.ownText()?.also {
append("$it\n\n")
}
row.selectFirst("h2:contains($title) + p")?.ownText()?.also {
// Alternative title
append("Título alternativo: $it\n")
}
// Additional info
listOf("Editora", "Lançamento", "Scans", "Tradução", "Cleaner", "Vizualizações")
.forEach { item ->
selectFirst("p:contains($item)")
?.text()
?.also { append("$it\n") }
}
}
}
}
private fun Element.getInfo(text: String) =
selectFirst("p:contains($text)")?.run {
selectFirst("b")?.text() ?: ownText()
}
// ============================== Chapters ==============================
override fun chapterListSelector() = "section.relese > div.container > div.row h5:has(a)"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
element.selectFirst("form + a")!!.run {
setUrlWithoutDomain(attr("href"))
name = text()
chapter_number = name.substringAfter(" ").toFloatOrNull() ?: 1F
}
date_upload = element.selectFirst("form + a + span")?.text().orEmpty().toDate()
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
return document.select("div.chapter-image canvas").mapIndexed { index, item ->
Page(index, "", item.absUrl("data-src"))
}
}
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
companion object {
const val PREFIX_SEARCH = "id:"
private val DATE_FORMATTER by lazy {
SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH)
}
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.extension.pt.blackoutcomics
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://blackoutcomics.com/comics/<item> intents
* and redirects them to the main Tachiyomi process.
*/
class BlackoutComicsUrlActivity : 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", "${BlackoutComics.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)
}
}