added buttsmithy extension (#10106)
This commit is contained in:
parent
4f208803da
commit
26b1407331
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,12 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'buttsmithy'
|
||||||
|
pkgNameSuffix = 'en.buttsmithy'
|
||||||
|
extClass = '.Buttsmithy'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
After Width: | Height: | Size: 614 KiB |
|
@ -0,0 +1,322 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.buttsmithy
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
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 okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.select.Elements
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author THE_ORONCO <the_oronco@posteo.net>
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Buttsmithy : HttpSource() {
|
||||||
|
data class TitleUrlPair(val title: String, val url: String)
|
||||||
|
|
||||||
|
override val name = "Buttsmithy"
|
||||||
|
|
||||||
|
override val baseUrl = "https://incase.buttsmithy.com"
|
||||||
|
|
||||||
|
// the full version of alfie for some reason has a separate url and isn't accessed like the other comics
|
||||||
|
private val baseUrlAlfie = "https://buttsmithy.com"
|
||||||
|
private val chapterOverviewBaseUrl = "$baseUrlAlfie/archives/chapter"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
private final val inCase = "InCase"
|
||||||
|
private final val alfieTitle = "Alfie"
|
||||||
|
private final val alfieDateParser = SimpleDateFormat("HH:mm MMMM dd, yyyy", Locale.US)
|
||||||
|
|
||||||
|
override val supportsLatest: Boolean = false
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
val chapters: List<SChapter> =
|
||||||
|
if (manga.title.contains(alfieTitle)) {
|
||||||
|
// TODO misc-chapter is currently broken
|
||||||
|
fetchAlfiePagesAsChapters(manga.url).reversed()
|
||||||
|
} else {
|
||||||
|
fetchOtherPagesAsChapters(manga.title, baseUrl + manga.url).reversed()
|
||||||
|
}
|
||||||
|
return Observable.just(chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all the pages from a Comic and returns them as separate SChapters.
|
||||||
|
* Because there is no overview for comics that aren't Alfie this needs to visit each page of
|
||||||
|
* the chosen comic.
|
||||||
|
*
|
||||||
|
* @param comicTitle title of the comic that is fetched (is needed for setting the date only once)
|
||||||
|
* @param currentPageUrl url of the current page the function will start / continue the recursion on
|
||||||
|
* @param allChapters list of all chapters (should be initialized with an empty list)
|
||||||
|
* @return returns a list of all pages that were found as SChapters
|
||||||
|
* */
|
||||||
|
private tailrec fun fetchOtherPagesAsChapters(
|
||||||
|
comicTitle: String,
|
||||||
|
currentPageUrl: String,
|
||||||
|
pageNr: Float = 0f,
|
||||||
|
allChapters: MutableList<SChapter> = mutableListOf()
|
||||||
|
): MutableList<SChapter> {
|
||||||
|
|
||||||
|
val currentDoc = client.newCall(GET(currentPageUrl, headers)).execute().asJsoup()
|
||||||
|
val currentPageComicPage = currentDoc.select("#comic img").first()
|
||||||
|
val chapterTitle = currentPageComicPage.attr("alt")
|
||||||
|
|
||||||
|
val chapter = SChapter.create().apply {
|
||||||
|
/* the setUrlWithoutDomain method can't be used here because Alfie has another base
|
||||||
|
* namespace and when retrieving the pages it is impossible to clearly differentiate an
|
||||||
|
* Alfie Chapter from some other comic chapter. */
|
||||||
|
url = currentPageUrl
|
||||||
|
name = chapterTitle
|
||||||
|
chapter_number = pageNr
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the preferences for the current comic
|
||||||
|
val seriesPrefs = Injekt.get<Application>().getSharedPreferences("source_${id}_updateTime:${comicTitle.lowercase()}", 0)
|
||||||
|
val seriesPrefEditor = seriesPrefs.edit()
|
||||||
|
|
||||||
|
val currentTimeMillis = System.currentTimeMillis()
|
||||||
|
// only update the time if the current chapter is not downloaded yet
|
||||||
|
if (!seriesPrefs.contains(chapter.name)) {
|
||||||
|
seriesPrefEditor.putLong(chapter.name, currentTimeMillis)
|
||||||
|
}
|
||||||
|
chapter.date_upload = seriesPrefs.getLong(chapter.name, currentTimeMillis)
|
||||||
|
|
||||||
|
seriesPrefEditor.apply()
|
||||||
|
|
||||||
|
allChapters.add(chapter)
|
||||||
|
|
||||||
|
val potentialNextPageUrl = currentDoc.select(".comic-nav-next").attr("href")
|
||||||
|
|
||||||
|
return if (potentialNextPageUrl.isEmpty()) allChapters
|
||||||
|
else fetchOtherPagesAsChapters(comicTitle, potentialNextPageUrl, pageNr + 1, allChapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all the pages from one of Alfies chapters and returns them as separate SChapters.
|
||||||
|
*
|
||||||
|
* @param currentPageUrl url of the current page the function will start / continue the recursion on
|
||||||
|
* @param lastPageNr page Number the function start enumeration on if no page number can be extracted from the title
|
||||||
|
* @param allChapters list of all chapters (should be initialized with an empty list)
|
||||||
|
* @return returns a list of all pages that were found in the chapter overview as SChapters
|
||||||
|
* */
|
||||||
|
private tailrec fun fetchAlfiePagesAsChapters(
|
||||||
|
currentPageUrl: String,
|
||||||
|
lastPageNr: Float = 0f,
|
||||||
|
allChapters: MutableList<SChapter> = mutableListOf()
|
||||||
|
): MutableList<SChapter> {
|
||||||
|
val pageNrRegex = "p*[0-9]+".toRegex()
|
||||||
|
|
||||||
|
val currentDoc = client.newCall(GET(currentPageUrl, headers)).execute().asJsoup()
|
||||||
|
val pagesAsChapters = currentDoc.select("article.has-post-thumbnail .post-content")
|
||||||
|
.mapIndexed { index, postElement ->
|
||||||
|
val postTitleElement = postElement.select(".post-info .post-title a")
|
||||||
|
val chapUrl = postTitleElement.attr("href")
|
||||||
|
val title = postTitleElement.text()
|
||||||
|
// this is needed for the MISC chapter where the pages are not numbered
|
||||||
|
val pageNr =
|
||||||
|
if (pageNrRegex.matches(title)) {
|
||||||
|
title.substringAfter("p").trim().toFloat()
|
||||||
|
} else index + lastPageNr
|
||||||
|
|
||||||
|
val dateString = postElement.select(".post-info .post-date").text()
|
||||||
|
val timeString = postElement.select(".post-info .post-time").text()
|
||||||
|
val date = alfieDateParser.parse("$timeString $dateString")?.time ?: 0L
|
||||||
|
|
||||||
|
SChapter.create().apply {
|
||||||
|
/* Alfie has its own name space and thus can't be handled like other comics.
|
||||||
|
* This means the setUrlWithoutDomain method can't be used */
|
||||||
|
url = chapUrl
|
||||||
|
name = title
|
||||||
|
chapter_number = pageNr
|
||||||
|
date_upload = date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allChapters.addAll(pagesAsChapters)
|
||||||
|
|
||||||
|
val potentialNextPageUrl = currentDoc.select(".paginav-next a").attr("href")
|
||||||
|
return if (potentialNextPageUrl.isEmpty()) {
|
||||||
|
allChapters
|
||||||
|
} else {
|
||||||
|
fetchAlfiePagesAsChapters(potentialNextPageUrl, lastPageNr, allChapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all comics that are currently hosted on buttsmithy (including the first version of
|
||||||
|
* alfie currently)
|
||||||
|
*
|
||||||
|
* @return a list of all comics currently hosted on buttsmithy with alfies chapters separated into separate mangas
|
||||||
|
*/
|
||||||
|
private fun fetchAllComics(): List<SManga> {
|
||||||
|
val mainDoc = client.newCall(GET(baseUrl, headers)).execute().asJsoup()
|
||||||
|
// Incases choose your own adventure comics
|
||||||
|
val cyoaSelector = "#menu-item-331"
|
||||||
|
// Incase other comics (ignoring alfie because alfie has its own subdomain)
|
||||||
|
val otherComicsSelector = "#menu-item-38"
|
||||||
|
|
||||||
|
val alfieChapters = fetchAlfieSMangas()
|
||||||
|
val cyoaComics = convertMenuElementToSManga(mainDoc.select(cyoaSelector))
|
||||||
|
val otherComics = convertMenuElementToSManga(mainDoc.select(otherComicsSelector))
|
||||||
|
|
||||||
|
// concat all different comic lists
|
||||||
|
return listOf(alfieChapters, cyoaComics, otherComics).flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.lowercase(): String {
|
||||||
|
return this.toLowerCase(Locale.getDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all chapters of Alfie (one of InCases comics) as separate SManga because this comic
|
||||||
|
* is gigantic and only updates one page at a time. Putting the pages into SChapters would block
|
||||||
|
* the automatic update fetching.
|
||||||
|
*
|
||||||
|
* @return all of Alfies chapters as separate SManga
|
||||||
|
*/
|
||||||
|
private fun fetchAlfieSMangas(): List<SManga> {
|
||||||
|
val pageDoc = client.newCall(GET(baseUrlAlfie, headers)).execute().asJsoup()
|
||||||
|
val mostRecentChapTitle = extractChapterTitleFromPageDoc(pageDoc)
|
||||||
|
|
||||||
|
val chaptersAsSManga: List<SManga> =
|
||||||
|
pageDoc.select("#chapter").select("option.level-0")
|
||||||
|
.map { chapterElement ->
|
||||||
|
val chapTitle = chapterElement.text().lowercase()
|
||||||
|
val chapUrlName = chapterTitleToChapterUrlName(chapTitle)
|
||||||
|
|
||||||
|
SManga.create().apply {
|
||||||
|
url = "$chapterOverviewBaseUrl/$chapUrlName"
|
||||||
|
title = "$alfieTitle - $chapTitle"
|
||||||
|
author = inCase
|
||||||
|
artist = inCase
|
||||||
|
status = decideAlfieStatusFromTitle(chapTitle, mostRecentChapTitle)
|
||||||
|
genre = "fantasy, NSFW"
|
||||||
|
thumbnail_url = generateImageUrlWithText(alfieTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chaptersAsSManga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decideAlfieStatusFromTitle(chapTitle: String, mostRecentChapTitle: String): Int {
|
||||||
|
return if (chapTitle == mostRecentChapTitle) SManga.UNKNOWN
|
||||||
|
else SManga.COMPLETED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractChapterTitleFromPageDoc(doc: Document): String {
|
||||||
|
return doc.select(".comic-chapter a").first().text().lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chapterTitleToChapterUrlName(chapTitle: String): String {
|
||||||
|
return when (chapTitle.lowercase()) {
|
||||||
|
"chapter 1" -> "chapter-1v2"
|
||||||
|
else -> chapTitle.replace(" ", "-").replace(".", "-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertMenuElementToSManga(menuElement: Elements): List<SManga> {
|
||||||
|
val comicLinkSelector = ".menu-item-type-custom a[href]"
|
||||||
|
|
||||||
|
val linkElements = menuElement.select(comicLinkSelector)
|
||||||
|
return linkElements
|
||||||
|
// filter out the first Alfie chapter that is still hosted under "incase.buttsmithy.com"
|
||||||
|
// see "fetchAlfieSMangas()" for how Alfie should be retrieved
|
||||||
|
.filter { linkElement -> !linkElement.text().contains(alfieTitle) }
|
||||||
|
.map { linkElement ->
|
||||||
|
val comicTitle = linkElement.text()
|
||||||
|
val comicUrl = linkElement.attr("href")
|
||||||
|
|
||||||
|
SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(comicUrl)
|
||||||
|
title = comicTitle
|
||||||
|
author = inCase
|
||||||
|
artist = inCase
|
||||||
|
status = SManga.COMPLETED
|
||||||
|
genre = "NSFW"
|
||||||
|
thumbnail_url = generateImageUrlWithText(comicTitle)
|
||||||
|
status = SManga.COMPLETED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateImageUrlWithText(text: String): String {
|
||||||
|
return "https://fakeimg.pl/800x1236/?text=$text&font=lobster"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateMangasPage(): MangasPage {
|
||||||
|
return MangasPage(fetchAllComics(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> = throw Exception("Not used")
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
val pageDoc = response.asJsoup()
|
||||||
|
return pageDoc.select("#comic").select("img[src]").attr("href")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage = throw Exception("Not used")
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
|
return Observable.just(
|
||||||
|
if (manga.title.contains(alfieTitle)) {
|
||||||
|
|
||||||
|
val pageDoc = client.newCall(GET(baseUrlAlfie, headers)).execute().asJsoup()
|
||||||
|
val mostRecentChapTitle = extractChapterTitleFromPageDoc(pageDoc)
|
||||||
|
val chapTitle = manga.title.substringAfter("Alfie - ").trim()
|
||||||
|
|
||||||
|
SManga.create().apply {
|
||||||
|
url = "$chapterOverviewBaseUrl/${chapterTitleToChapterUrlName(chapTitle)}"
|
||||||
|
title = "$alfieTitle - $chapTitle"
|
||||||
|
author = inCase
|
||||||
|
artist = inCase
|
||||||
|
status = decideAlfieStatusFromTitle(chapTitle, mostRecentChapTitle)
|
||||||
|
genre = "fantasy, NSFW"
|
||||||
|
thumbnail_url = generateImageUrlWithText(alfieTitle)
|
||||||
|
}
|
||||||
|
} else manga
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request = GET(manga.url, headers)
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga = throw Exception("Not used")
|
||||||
|
|
||||||
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
|
val comicPageDoc = client.newCall(GET(chapter.url, headers)).execute().asJsoup()
|
||||||
|
val imageUrl = comicPageDoc.select("#comic img").attr("src")
|
||||||
|
val comicPage = Page(0, "", imageUrl)
|
||||||
|
|
||||||
|
return Observable.just(listOf(comicPage))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> = throw Exception("Not used")
|
||||||
|
|
||||||
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
|
return Observable.just(generateMangasPage())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage = throw Exception("Not used")
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request = throw Exception("Not used")
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage = throw Exception("Not used")
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||||
|
throw Exception("Not used")
|
||||||
|
}
|
Loading…
Reference in New Issue