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 {
|
ext {
|
||||||
appName = 'Tachiyomi: Hiveworks Comics'
|
appName = 'Tachiyomi: Hiveworks Comics'
|
||||||
pkgNameSuffix = 'en.hiveworks'
|
pkgNameSuffix = 'en.hiveworks'
|
||||||
extClass = '.HiveWorks'
|
extClass = '.Hiveworks'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,23 +2,38 @@ package eu.kanade.tachiyomi.extension.en.hiveworks
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
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.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class Hiveworks : ParsedHttpSource() {
|
||||||
|
|
||||||
class HiveWorks : ParsedHttpSource() {
|
//Info
|
||||||
|
|
||||||
override val name = "Hiveworks Comics"
|
override val name = "Hiveworks Comics"
|
||||||
override val baseUrl = "https://hiveworkscomics.com"
|
override val baseUrl = "https://hiveworkscomics.com"
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
override val supportsLatest = false
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
//Client
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.connectTimeout(1, TimeUnit.MINUTES)
|
.connectTimeout(1, TimeUnit.MINUTES)
|
||||||
.readTimeout(1, TimeUnit.MINUTES)
|
.readTimeout(1, TimeUnit.MINUTES)
|
||||||
|
@ -26,44 +41,89 @@ class HiveWorks : ParsedHttpSource() {
|
||||||
.followRedirects(true)
|
.followRedirects(true)
|
||||||
.build()!!
|
.build()!!
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.comicblock"
|
// Popular
|
||||||
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()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
|
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 {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val uri = Uri.parse(baseUrl).buildUpon()
|
val uri = Uri.parse(baseUrl).buildUpon()
|
||||||
.appendPath("home")
|
if (filters.isNotEmpty()) uri.appendPath("home")
|
||||||
//Append uri filters
|
//Append uri filters
|
||||||
filters.forEach {
|
filters.forEach {
|
||||||
if (it is UriFilter)
|
if (it is UriFilter)
|
||||||
it.addToUri(uri)
|
it.addToUri(uri)
|
||||||
}
|
}
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
searchQuery = query
|
||||||
|
uri.fragment("localSearch")
|
||||||
|
}
|
||||||
return GET(uri.toString(), headers)
|
return GET(uri.toString(), headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers)
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
override fun pageListRequest(chapter: SChapter) = GET(chapter.url, headers)
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val uri = Uri.parse(manga.url).buildUpon()
|
val url = response.request().url().toString()
|
||||||
.appendPath("comic")
|
val document = response.asJsoup()
|
||||||
.appendPath("archive")
|
|
||||||
.build().toString()
|
val selectManga = document.select(searchMangaSelector())
|
||||||
return GET(uri, headers)
|
val filterManga = if (url.endsWith("localSearch")) {
|
||||||
}
|
selectManga.filter { it.text().contains(searchQuery, true) }
|
||||||
//override fun chapterListRequest(manga: SManga) = GET(manga.url + "/comic/archive", headers)
|
} 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 popularMangaFromElement(element: Element) = mangaFromElement(element)
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element)
|
|
||||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||||
|
|
||||||
|
// Common
|
||||||
|
|
||||||
private fun mangaFromElement(element: Element): SManga {
|
private fun mangaFromElement(element: Element): SManga {
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
manga.url = element.select("a.comiclink").first().attr("abs:href")
|
manga.url = element.select("a.comiclink").first().attr("abs:href")
|
||||||
|
@ -71,16 +131,63 @@ class HiveWorks : ParsedHttpSource() {
|
||||||
manga.thumbnail_url = element.select("img").attr("abs:src")
|
manga.thumbnail_url = element.select("img").attr("abs:src")
|
||||||
manga.artist = element.select("h2").text().removePrefix("by").trim()
|
manga.artist = element.select("h2").text().removePrefix("by").trim()
|
||||||
manga.author = manga.artist
|
manga.author = manga.artist
|
||||||
manga.description = element.select("div.description").text().trim() + "\n" + "\n" + "*Not all comics are supported*"
|
manga.description = element.select("div.description").text().trim()
|
||||||
manga.genre = element.select("div.comicrating").text().trim()
|
manga.genre = element.select("div.comicrating").text().trim()
|
||||||
return manga
|
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")
|
||||||
|
return GET(uri.toString(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val uri = Uri.parse(document.baseUri())
|
val baseUrl = document.select("div script").html().substringAfter("href='").substringBefore("'")
|
||||||
val baseUrl = "${uri.scheme}://${uri.authority}"
|
|
||||||
val elements = document.select(chapterListSelector())
|
val elements = document.select(chapterListSelector())
|
||||||
|
if (elements.isNullOrEmpty()) throw Exception("This comic has a unsupported chapter list")
|
||||||
val chapters = mutableListOf<SChapter>()
|
val chapters = mutableListOf<SChapter>()
|
||||||
for (i in 1 until elements.size) {
|
for (i in 1 until elements.size) {
|
||||||
chapters.add(createChapter(elements[i], baseUrl))
|
chapters.add(createChapter(elements[i], baseUrl))
|
||||||
|
@ -91,41 +198,52 @@ class HiveWorks : ParsedHttpSource() {
|
||||||
|
|
||||||
private fun createChapter(element: Element, baseUrl: String?) = SChapter.create().apply {
|
private fun createChapter(element: Element, baseUrl: String?) = SChapter.create().apply {
|
||||||
name = element.text().substringAfter("-").trim()
|
name = element.text().substringAfter("-").trim()
|
||||||
url = "$baseUrl/" + element.attr("value")
|
url = baseUrl + element.attr("value")
|
||||||
date_upload = parseDate(element.text().substringBefore("-").trim())
|
date_upload = parseDate(element.text().substringBefore("-").trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseDate(date: String): Long {
|
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 chapterFromElement(element: Element) = throw Exception("Not Used")
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
//Pages
|
||||||
val manga = SManga.create()
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
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>()
|
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")))
|
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
|
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 imageUrlRequest(page: Page) = throw Exception("Not used")
|
||||||
override fun imageUrlParse(document: Document) = throw Exception("Not used")
|
override fun imageUrlParse(document: Document) = throw Exception("Not used")
|
||||||
|
|
||||||
//Filter List Code
|
//Filters
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
override fun getFilterList() = FilterList(
|
||||||
Filter.Header("NOTE: Text search does not work."),
|
|
||||||
Filter.Header("Only one filter can be used at a time"),
|
Filter.Header("Only one filter can be used at a time"),
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
|
UpdateDay(),
|
||||||
RatingFilter(),
|
RatingFilter(),
|
||||||
GenreFilter(),
|
GenreFilter(),
|
||||||
|
TitleFilter(),
|
||||||
SortFilter()
|
SortFilter()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -144,6 +262,17 @@ class HiveWorks : ParsedHttpSource() {
|
||||||
fun addToUri(uri: Uri.Builder)
|
fun addToUri(uri: Uri.Builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 RatingFilter : UriSelectFilter("Rating", "age", arrayOf(
|
private class RatingFilter : UriSelectFilter("Rating", "age", arrayOf(
|
||||||
Pair("all", "All"),
|
Pair("all", "All"),
|
||||||
Pair("everyone", "Everyone"),
|
Pair("everyone", "Everyone"),
|
||||||
|
@ -175,13 +304,91 @@ class HiveWorks : ParsedHttpSource() {
|
||||||
Pair("urban-fantasy", "Urban Fantasy")
|
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(
|
private class SortFilter : UriSelectFilter("Sort By", "sortby", arrayOf(
|
||||||
Pair("none", "None"),
|
Pair("none", "None"),
|
||||||
Pair("a-z", "A-Z"),
|
Pair("a-z", "A-Z"),
|
||||||
Pair("z-a", "Z-A")
|
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