smbc: add extension for smbc-comics.com (#10192)

* smbc: add extension for smbc-comics.com

Adds an extension for Saturday Morning Breakfast Comics

* hiveworks: remove references to Saturday Morning Breakfast Comics

Removes code that was made to handle reading SMBC specifically. If a
user still has the comic in the Hiveworks extension, they'll get a
warning to migrate to the SMBC extension.
This commit is contained in:
solkaz 2025-09-01 11:04:57 +02:00 committed by Draff
parent 804fd752e8
commit b6bce67308
Signed by: Draff
GPG Key ID: E8A89F3211677653
9 changed files with 148 additions and 61 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'Hiveworks Comics'
extClass = '.Hiveworks'
extVersionCode = 10
extVersionCode = 11
}
apply from: "$rootDir/common.gradle"

View File

@ -23,6 +23,10 @@ import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
/**
* Code that used to handle Saturday Morning Breakfast Comics has been split to its
* own separate extension at eu.kanade.tachiyomi.extension.en.saturdaymorningbreakfastcomics
*/
class Hiveworks : ParsedHttpSource() {
// Info
@ -39,28 +43,6 @@ class Hiveworks : ParsedHttpSource() {
.readTimeout(1, TimeUnit.MINUTES)
.retryOnConnectionFailure(true)
.followRedirects(true)
.addNetworkInterceptor { chain ->
val request = chain.request()
if (!request.url.toString().contains("smbc-comics")) {
return@addNetworkInterceptor chain.proceed(request)
}
val response = chain.proceed(request)
// As of March 2025, SMBC chapter list page returns status code 500 even
// though it still has correct data. Do not throw an error in this case.
//
// I reported this error to SMBC on 2025-05-28 and it was not fixed by
// 2025-06-11, but even if it is fixed eventually, the same problem might
// occur again in the future.
if (response.code == 500) {
val newResponse = response.newBuilder()
.code(200)
.build()
newResponse
} else {
response
}
}
.build()
// Popular
@ -216,6 +198,7 @@ class Hiveworks : ParsedHttpSource() {
when {
"sssscomic" in uri.toString() -> uri.appendQueryParameter("id", "archive") // sssscomic uses query string in url
"awkwardzombie" in uri.toString() -> uri.appendPath("awkward-zombie").appendPath("archive")
"smbc-comics" in uri.toString() -> throw Exception("Migrate to the Saturday Morning Breakfast Comics extension to read this comic")
else -> {
uri.appendPath("comic")
uri.appendPath("archive")
@ -268,10 +251,6 @@ class Hiveworks : ParsedHttpSource() {
// Site specific pages can be added here
when {
"smbc-comics" in url -> {
pages.add(Page(pages.size, "", document.select("div#aftercomic img").attr("src")))
pages.add(Page(pages.size, "", smbcTextHandler(document)))
}
"sssscomic" in url -> {
val urlPath = document.select("img.comicnormal").attr("src")
val urlimg = response.request.url.resolve("../../$urlPath").toString()
@ -497,40 +476,6 @@ class Hiveworks : ParsedHttpSource() {
return chapters
}
// Builds Image from mouse tooltip text
private fun smbcTextHandler(document: Document): String {
val title = document.select("title").text().trim()
val altText = document.select("div#cc-comicbody img").attr("title")
val titleWords: Sequence<String> = title.splitToSequence(" ")
val altTextWords: Sequence<String> = altText.splitToSequence(" ")
val builder = StringBuilder()
var count = 0
for (i in titleWords) {
if (count != 0 && count.rem(7) == 0) {
builder.append("%0A")
}
builder.append(i).append("+")
count++
}
builder.append("%0A%0A")
var charCount = 0
for (i in altTextWords) {
if (charCount > 25) {
builder.append("%0A")
charCount = 0
}
builder.append(i).append("+")
charCount += i.length + 1
}
return "https://fakeimg.ryd.tools/1500x2126/ffffff/000000/?text=$builder&font_size=42&font=museo"
}
// Used to throw custom error codes for http codes
private fun Call.asObservableSuccess(): Observable<Response> {
return asObservable().doOnNext { response ->

View File

@ -0,0 +1,8 @@
ext {
extName = 'Saturday Morning Breakfast Comics'
extClass = '.SaturdayMorningBreakfastComics'
extVersionCode = 1
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,134 @@
package eu.kanade.tachiyomi.extension.en.saturdaymorningbreakfastcomics
import android.net.Uri.encode
import eu.kanade.tachiyomi.network.asObservable
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 eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.tryParse
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
/**
* Split from Hiveworks extension
*/
class SaturdayMorningBreakfastComics : HttpSource() {
override val name = "Saturday Morning Breakfast Comics"
override val baseUrl = "https://smbc-comics.com"
override val lang = "en"
override val supportsLatest = false
private fun String.image() =
"https://fakeimg.ryd.tools/1500x2126/ffffff/000000/?font=museo&font_size=42&text=" + encode(
this,
)
// Taken from XKCD
private fun wordWrap(text: String) = buildString {
var charCount = 0
text.replace("\r\n", " ").split(' ').forEach { w ->
if (charCount > 25) {
append("\n")
charCount = 0
}
append(w).append(' ')
charCount += w.length + 1
}
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
val manga = SManga.create().apply {
title = "Saturday Morning Breakfast Comics"
artist = "Zach Weinersmith"
author = "Zach Weinersmith"
status = SManga.ONGOING
url = "/comic/archive"
description =
"SMBC is a daily comic strip about life, philosophy, science, mathematics, and dirty jokes."
thumbnail_url = "https://fakeimg.ryd.tools/550x780/ffffff/6e7b91/?font=museo&text=SMBC"
}
return Observable.just(MangasPage(listOf(manga), false))
}
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> = Observable.just(MangasPage(emptyList(), false))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.just(manga)
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga))
.asObservable()
.map { response ->
if (!response.isSuccessful && response.code != 500) {
response.close()
throw Exception("HTTP ${response.code}")
}
response.asJsoup().select("option[value*=\"comic/\"]")
.mapIndexed { index, element ->
val chapter = SChapter.create()
chapter.url = "/${element.attr("value")}"
val (date, title) = element.text().split(" - ")
chapter.name = title
chapter.date_upload = dateFormat.tryParse(date)
chapter.chapter_number = (index + 1).toFloat()
chapter
}
.reversed()
}
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val pages = mutableListOf<Page>()
val image = document.select("img#cc-comic")
pages.add(Page(0, "", image.attr("abs:src")))
if (image.hasAttr("title")) {
pages.add(Page(1, "", wordWrap(image.attr("title")).image()))
}
pages.add(Page(2, "", document.select("#aftercomic > img").attr("abs:src")))
return pages
}
private val dateFormat by lazy {
SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
}
override fun chapterListParse(response: Response): List<SChapter> =
throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response): MangasPage =
throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
throw UnsupportedOperationException()
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response): MangasPage =
throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
override fun mangaDetailsParse(response: Response): SManga =
throw UnsupportedOperationException()
override fun popularMangaParse(response: Response): MangasPage =
throw UnsupportedOperationException()
}