diff --git a/src/en/hiveworks/build.gradle b/src/en/hiveworks/build.gradle index c9e50a6ff..4142396bb 100644 --- a/src/en/hiveworks/build.gradle +++ b/src/en/hiveworks/build.gradle @@ -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' } diff --git a/src/en/hiveworks/src/eu/kanade/tachiyomi/extension/en/hiveworks/Hiveworks.kt b/src/en/hiveworks/src/eu/kanade/tachiyomi/extension/en/hiveworks/Hiveworks.kt index 8d7eb1610..1f2893b98 100644 --- a/src/en/hiveworks/src/eu/kanade/tachiyomi/extension/en/hiveworks/Hiveworks.kt +++ b/src/en/hiveworks/src/eu/kanade/tachiyomi/extension/en/hiveworks/Hiveworks.kt @@ -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 { + 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> { + 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 { 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() 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 { + override fun pageListRequest(chapter: SChapter) = GET(chapter.url, headers) + override fun pageListParse(response: Response): List { + val url = response.request().url().toString() + val document = response.asJsoup() val pages = mutableListOf() - 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 = 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>, - val firstIsUnspecified: Boolean = true, - defaultValue: Int = 0) : + val firstIsUnspecified: Boolean = true, + defaultValue: Int = 0) : Filter.Select(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 = title.splitToSequence(" ") + val altTextWords: Sequence = 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 { + 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()}") + } + } + } + } }