Improve Hiveworks (#2458)
* Improve Hiveworks - Optimize imports - Reorganize code - Hide known incompatible comics - Add local search - Add proper Manga Details - Changed page list selector - Add site specific pages - Add addational filters - Add custom error codes - Other misc code edits * Update smbcTextHandler - Add title to smbc text
This commit is contained in:
parent
49b34c57f6
commit
0be01826cf
|
@ -4,8 +4,8 @@ apply plugin: 'kotlin-android'
|
|||
ext {
|
||||
appName = 'Tachiyomi: Hiveworks Comics'
|
||||
pkgNameSuffix = 'en.hiveworks'
|
||||
extClass = '.HiveWorks'
|
||||
extVersionCode = 1
|
||||
extClass = '.Hiveworks'
|
||||
extVersionCode = 2
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -2,23 +2,38 @@ package eu.kanade.tachiyomi.extension.en.hiveworks
|
|||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.*
|
||||
import okhttp3.Call
|
||||
import okhttp3.OkHttpClient
|
||||
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.*
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class Hiveworks : ParsedHttpSource() {
|
||||
|
||||
class HiveWorks : ParsedHttpSource() {
|
||||
//Info
|
||||
|
||||
override val name = "Hiveworks Comics"
|
||||
override val baseUrl = "https://hiveworkscomics.com"
|
||||
override val lang = "en"
|
||||
override val supportsLatest = false
|
||||
override val supportsLatest = true
|
||||
|
||||
//Client
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
|
@ -26,64 +41,156 @@ class HiveWorks : ParsedHttpSource() {
|
|||
.followRedirects(true)
|
||||
.build()!!
|
||||
|
||||
override fun popularMangaSelector() = "div.comicblock"
|
||||
override fun latestUpdatesSelector() = throw Exception ("Not Used")
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
override fun chapterListSelector() = "select[name=comic] option"
|
||||
|
||||
override fun popularMangaNextPageSelector() = "none"
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
|
||||
override fun latestUpdatesRequest(page: Int) = throw Exception ("Not Used")
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
override fun popularMangaSelector() = "div.comicblock"
|
||||
override fun popularMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).filterNot {
|
||||
val url = it.select("a.comiclink").first().attr("abs:href")
|
||||
url.contains("sparklermonthly.com") || url.contains("explosm.net") //Filter Unsupported Comics
|
||||
}.map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val day = SimpleDateFormat("EEEE", Locale.US).format(Date()).toLowerCase(Locale.US)
|
||||
return GET("$baseUrl/home/update-day/$day", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element)
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
|
||||
// Search
|
||||
// Source's website doesn't appear to have a search function; so searching locally
|
||||
|
||||
private lateinit var searchQuery: String
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val uri = Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("home")
|
||||
if (filters.isNotEmpty()) uri.appendPath("home")
|
||||
//Append uri filters
|
||||
filters.forEach {
|
||||
if (it is UriFilter)
|
||||
it.addToUri(uri)
|
||||
}
|
||||
if (query.isNotEmpty()) {
|
||||
searchQuery = query
|
||||
uri.fragment("localSearch")
|
||||
}
|
||||
return GET(uri.toString(), headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers)
|
||||
override fun pageListRequest(chapter: SChapter) = GET(chapter.url, headers)
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val url = response.request().url().toString()
|
||||
val document = response.asJsoup()
|
||||
|
||||
val selectManga = document.select(searchMangaSelector())
|
||||
val filterManga = if (url.endsWith("localSearch")) {
|
||||
selectManga.filter { it.text().contains(searchQuery, true) }
|
||||
} else {
|
||||
selectManga
|
||||
}
|
||||
val mangas = filterManga.map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
// Common
|
||||
|
||||
private fun mangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.url = element.select("a.comiclink").first().attr("abs:href")
|
||||
manga.title = element.select("h1").text().trim()
|
||||
manga.thumbnail_url = element.select("img").attr("abs:src")
|
||||
manga.artist = element.select("h2").text().removePrefix("by").trim()
|
||||
manga.author = manga.artist
|
||||
manga.description = element.select("div.description").text().trim()
|
||||
manga.genre = element.select("div.comicrating").text().trim()
|
||||
return manga
|
||||
}
|
||||
|
||||
// Details
|
||||
// Fetches details by calling home page again and using the existing url to find the correct comic
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
val url = manga.url
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response, url).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl, headers)
|
||||
override fun mangaDetailsParse(document: Document): SManga = throw Exception("Not Used")
|
||||
private fun mangaDetailsParse(response: Response, url: String): SManga {
|
||||
val document = response.asJsoup()
|
||||
return document.select(popularMangaSelector()).first {
|
||||
url == it.select("a.comiclink").first().attr("abs:href")
|
||||
}.let {
|
||||
mangaFromElement(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Chapters
|
||||
|
||||
//Included to call custom error codes
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return if (manga.status != SManga.LICENSED) {
|
||||
client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
}
|
||||
} else {
|
||||
Observable.error(Exception("Licensed - No chapters to show"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "select[name=comic] option"
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val uri = Uri.parse(manga.url).buildUpon()
|
||||
.appendPath("comic")
|
||||
.appendPath("archive")
|
||||
.build().toString()
|
||||
return GET(uri, headers)
|
||||
}
|
||||
//override fun chapterListRequest(manga: SManga) = GET(manga.url + "/comic/archive", headers)
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element)
|
||||
override fun searchMangaFromElement(element: Element)= mangaFromElement(element)
|
||||
|
||||
private fun mangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.url = element.select("a.comiclink").first().attr("abs:href")
|
||||
manga.title = element.select("h1").text().trim()
|
||||
manga.thumbnail_url = element.select("img").attr("abs:src")
|
||||
manga.artist = element.select("h2").text().removePrefix("by").trim()
|
||||
manga.author = manga.artist
|
||||
manga.description = element.select("div.description").text().trim() + "\n" + "\n" + "*Not all comics are supported*"
|
||||
manga.genre = element.select("div.comicrating").text().trim()
|
||||
return manga
|
||||
return GET(uri.toString(), headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val uri = Uri.parse(document.baseUri())
|
||||
val baseUrl = "${uri.scheme}://${uri.authority}"
|
||||
val baseUrl = document.select("div script").html().substringAfter("href='").substringBefore("'")
|
||||
val elements = document.select(chapterListSelector())
|
||||
if (elements.isNullOrEmpty()) throw Exception("This comic has a unsupported chapter list")
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
for (i in 1 until elements.size) {
|
||||
chapters.add(createChapter(elements[i] , baseUrl))
|
||||
chapters.add(createChapter(elements[i], baseUrl))
|
||||
}
|
||||
chapters.reverse()
|
||||
return chapters
|
||||
|
@ -91,47 +198,58 @@ class HiveWorks : ParsedHttpSource() {
|
|||
|
||||
private fun createChapter(element: Element, baseUrl: String?) = SChapter.create().apply {
|
||||
name = element.text().substringAfter("-").trim()
|
||||
url = "$baseUrl/" + element.attr("value")
|
||||
url = baseUrl + element.attr("value")
|
||||
date_upload = parseDate(element.text().substringBefore("-").trim())
|
||||
}
|
||||
|
||||
private fun parseDate(date: String): Long {
|
||||
return SimpleDateFormat("MMM dd, yyyy", Locale.US ).parse(date).time
|
||||
return SimpleDateFormat("MMM dd, yyyy", Locale.US).parse(date)?.time ?: 0
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element) = throw Exception("Not Used")
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
return manga
|
||||
}
|
||||
//Pages
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
override fun pageListRequest(chapter: SChapter) = GET(chapter.url, headers)
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val url = response.request().url().toString()
|
||||
val document = response.asJsoup()
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
document.select("img[id=cc-comic]")?.forEach {
|
||||
document.select("div#cc-comicbody img")?.forEach {
|
||||
pages.add(Page(pages.size, "", it.attr("src")))
|
||||
}
|
||||
|
||||
//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)))
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> = throw Exception("Not used, see pageListParse(response)")
|
||||
override fun imageUrlRequest(page: Page) = throw Exception("Not used")
|
||||
override fun imageUrlParse(document: Document) = throw Exception("Not used")
|
||||
|
||||
//Filter List Code
|
||||
//Filters
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("NOTE: Text search does not work."),
|
||||
Filter.Header("Only one filter can be used at a time"),
|
||||
Filter.Separator(),
|
||||
UpdateDay(),
|
||||
RatingFilter(),
|
||||
GenreFilter(),
|
||||
TitleFilter(),
|
||||
SortFilter()
|
||||
)
|
||||
|
||||
private open class UriSelectFilter(displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>,
|
||||
val firstIsUnspecified: Boolean = true,
|
||||
defaultValue: Int = 0) :
|
||||
val firstIsUnspecified: Boolean = true,
|
||||
defaultValue: Int = 0) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
if (state != 0 || !firstIsUnspecified)
|
||||
|
@ -144,44 +262,133 @@ class HiveWorks : ParsedHttpSource() {
|
|||
fun addToUri(uri: Uri.Builder)
|
||||
}
|
||||
|
||||
private class RatingFilter: UriSelectFilter("Rating","age", arrayOf(
|
||||
Pair("all","All"),
|
||||
Pair("everyone","Everyone"),
|
||||
Pair("teen","Teen"),
|
||||
Pair("young-adult","Young Adult"),
|
||||
Pair("mature","Mature")
|
||||
private class UpdateDay : UriSelectFilter("Update Day", "update-day", arrayOf(
|
||||
Pair("all", "All"),
|
||||
Pair("monday", "Monday"),
|
||||
Pair("tuesday", "Tuesday"),
|
||||
Pair("wednesday", "Wednesday"),
|
||||
Pair("thursday", "Thursday"),
|
||||
Pair("friday", "Friday"),
|
||||
Pair("saturday", "Saturday"),
|
||||
Pair("sunday", "Sunday")
|
||||
))
|
||||
|
||||
private class GenreFilter: UriSelectFilter("Genre","genre", arrayOf(
|
||||
Pair("all","All"),
|
||||
Pair("action/adventure","Action/Adventure"),
|
||||
Pair("animated","Animated"),
|
||||
Pair("autobio","Autobio"),
|
||||
Pair("comedy","Comedy"),
|
||||
Pair("drama","Drama"),
|
||||
Pair("dystopian","Dystopian"),
|
||||
Pair("fairytale","Fairytale"),
|
||||
Pair("fantasy","Fantasy"),
|
||||
Pair("finished","Finished"),
|
||||
Pair("historical-fiction","Historical Fiction"),
|
||||
Pair("horror","Horror"),
|
||||
Pair("lgbt","LGBT"),
|
||||
Pair("mystery","Mystery"),
|
||||
Pair("romance","Romance"),
|
||||
Pair("sci-fi","Science Fiction"),
|
||||
Pair("slice-of-life","Slice of Life"),
|
||||
Pair("steampunk","Steampunk"),
|
||||
Pair("superhero","Superhero"),
|
||||
Pair("urban-fantasy","Urban Fantasy")
|
||||
))
|
||||
|
||||
private class SortFilter: UriSelectFilter("Sort By","sortby", arrayOf(
|
||||
Pair("none","None"),
|
||||
Pair("a-z","A-Z"),
|
||||
Pair("z-a","Z-A")
|
||||
private class RatingFilter : UriSelectFilter("Rating", "age", arrayOf(
|
||||
Pair("all", "All"),
|
||||
Pair("everyone", "Everyone"),
|
||||
Pair("teen", "Teen"),
|
||||
Pair("young-adult", "Young Adult"),
|
||||
Pair("mature", "Mature")
|
||||
))
|
||||
|
||||
private class GenreFilter : UriSelectFilter("Genre", "genre", arrayOf(
|
||||
Pair("all", "All"),
|
||||
Pair("action/adventure", "Action/Adventure"),
|
||||
Pair("animated", "Animated"),
|
||||
Pair("autobio", "Autobio"),
|
||||
Pair("comedy", "Comedy"),
|
||||
Pair("drama", "Drama"),
|
||||
Pair("dystopian", "Dystopian"),
|
||||
Pair("fairytale", "Fairytale"),
|
||||
Pair("fantasy", "Fantasy"),
|
||||
Pair("finished", "Finished"),
|
||||
Pair("historical-fiction", "Historical Fiction"),
|
||||
Pair("horror", "Horror"),
|
||||
Pair("lgbt", "LGBT"),
|
||||
Pair("mystery", "Mystery"),
|
||||
Pair("romance", "Romance"),
|
||||
Pair("sci-fi", "Science Fiction"),
|
||||
Pair("slice-of-life", "Slice of Life"),
|
||||
Pair("steampunk", "Steampunk"),
|
||||
Pair("superhero", "Superhero"),
|
||||
Pair("urban-fantasy", "Urban Fantasy")
|
||||
))
|
||||
|
||||
private class TitleFilter : UriSelectFilter("Title", "alpha", arrayOf(
|
||||
Pair("all", "All"),
|
||||
Pair("a", "A"),
|
||||
Pair("b", "B"),
|
||||
Pair("c", "C"),
|
||||
Pair("d", "D"),
|
||||
Pair("e", "E"),
|
||||
Pair("f", "F"),
|
||||
Pair("g", "G"),
|
||||
Pair("h", "H"),
|
||||
Pair("i", "I"),
|
||||
Pair("j", "J"),
|
||||
Pair("k", "K"),
|
||||
Pair("l", "L"),
|
||||
Pair("m", "M"),
|
||||
Pair("n", "N"),
|
||||
Pair("o", "O"),
|
||||
Pair("p", "P"),
|
||||
Pair("q", "Q"),
|
||||
Pair("r", "R"),
|
||||
Pair("s", "S"),
|
||||
Pair("t", "T"),
|
||||
Pair("u", "U"),
|
||||
Pair("v", "V"),
|
||||
Pair("w", "W"),
|
||||
Pair("x", "X"),
|
||||
Pair("y", "Y"),
|
||||
Pair("z", "Z"),
|
||||
Pair("numbers-symbols", "Numbers / Symbols")
|
||||
))
|
||||
|
||||
private class SortFilter : UriSelectFilter("Sort By", "sortby", arrayOf(
|
||||
Pair("none", "None"),
|
||||
Pair("a-z", "A-Z"),
|
||||
Pair("z-a", "Z-A")
|
||||
))
|
||||
|
||||
//Other Code
|
||||
|
||||
//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.pl/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 ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
when (response.code()) {
|
||||
404 -> throw Exception("This comic has a unsupported chapter list")
|
||||
else -> throw Exception("HiveWorks Comics HTTP Error ${response.code()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue