diff --git a/src/all/hentaicafe/AndroidManifest.xml b/src/all/hentaicafe/AndroidManifest.xml new file mode 100644 index 000000000..e8b73b39e --- /dev/null +++ b/src/all/hentaicafe/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/all/hentaicafe/build.gradle b/src/all/hentaicafe/build.gradle new file mode 100644 index 000000000..2f4accb71 --- /dev/null +++ b/src/all/hentaicafe/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Hentai Cafe' + extClass = '.HentaiCafe' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/hentaicafe/res/mipmap-hdpi/ic_launcher.png b/src/all/hentaicafe/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..dc63ee26e Binary files /dev/null and b/src/all/hentaicafe/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/hentaicafe/res/mipmap-mdpi/ic_launcher.png b/src/all/hentaicafe/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..1935fce6c Binary files /dev/null and b/src/all/hentaicafe/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/hentaicafe/res/mipmap-xhdpi/ic_launcher.png b/src/all/hentaicafe/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..74ded80a8 Binary files /dev/null and b/src/all/hentaicafe/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/hentaicafe/res/mipmap-xxhdpi/ic_launcher.png b/src/all/hentaicafe/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d1735eea8 Binary files /dev/null and b/src/all/hentaicafe/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/hentaicafe/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/hentaicafe/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..bf4eae564 Binary files /dev/null and b/src/all/hentaicafe/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/hentaicafe/src/eu/kanade/tachiyomi/extension/all/hentaicafe/HentaiCafe.kt b/src/all/hentaicafe/src/eu/kanade/tachiyomi/extension/all/hentaicafe/HentaiCafe.kt new file mode 100644 index 000000000..7123e72e6 --- /dev/null +++ b/src/all/hentaicafe/src/eu/kanade/tachiyomi/extension/all/hentaicafe/HentaiCafe.kt @@ -0,0 +1,167 @@ +package eu.kanade.tachiyomi.extension.all.hentaicafe + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +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.model.UpdateStrategy +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable + +class HentaiCafe : ParsedHttpSource() { + + override val name = "Hentai Cafe" + + override val baseUrl = "https://hentaicafe.xxx" + + override val lang = "all" + + override val supportsLatest = true + + override val client by lazy { + network.client.newBuilder() + .rateLimitHost(baseUrl.toHttpUrl(), 2) + // Image CDN + .rateLimitHost("https://cdn.hentaibomb.com".toHttpUrl(), 2) + .build() + } + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + .add("Accept-Language", "en-US,en;q=0.5") + + // ============================== Popular =============================== + override fun popularMangaRequest(page: Int) = GET(baseUrl, headers) + + override fun popularMangaSelector() = "div.index-popular > div.gallery > a" + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.attr("href")) + thumbnail_url = element.selectFirst("img")?.getImageUrl() + title = element.selectFirst("div.caption")!!.text() + } + + override fun popularMangaNextPageSelector() = null + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers) + + override fun latestUpdatesSelector() = "div.index-container:contains(new uploads) > div.gallery > a" + + override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = "section.pagination > a.last:not(.disabled)" + + // =============================== Search =============================== + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler + val id = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/g/$id")) + .asObservableSuccess() + .map(::searchMangaByIdParse) + } else { + super.fetchSearchManga(page, query, filters) + } + } + + private fun searchMangaByIdParse(response: Response): MangasPage { + val details = mangaDetailsParse(response.use { it.asJsoup() }) + return MangasPage(listOf(details), false) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/search".toHttpUrl().newBuilder() + .addQueryParameter("q", query) + .addQueryParameter("page", page.toString()) + .build() + + return GET(url, headers) + } + + override fun searchMangaSelector() = "div.index-container > div.gallery > a" + + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() + + // =========================== Manga Details ============================ + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + thumbnail_url = document.selectFirst("#cover > a > img")?.getImageUrl() + + with(document.selectFirst("div#bigcontainer > div > div#info")!!) { + title = selectFirst("h1.title")!!.text() + artist = getInfo("Artists") + genre = getInfo("Tags") + + description = buildString { + select(".title > span").eachText().joinToString("\n").also { + append("Full titles:\n$it\n") + } + + getInfo("Groups")?.also { append("\nGroups: $it") } + getInfo("Languages")?.also { append("\nLanguages: $it") } + getInfo("Parodies")?.also { append("\nParodies: $it") } + getInfo("Pages")?.also { append("\nPages: $it") } + } + } + + status = SManga.COMPLETED + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + } + + private fun Element.getInfo(item: String) = + select("div.field-name:containsOwn($item) a.tag > span.name") + .eachText() + .takeUnless { it.isEmpty() } + ?.joinToString() + + // ============================== Chapters ============================== + override fun fetchChapterList(manga: SManga): Observable> { + val chapter = SChapter.create().apply { + url = manga.url + name = "Chapter" + chapter_number = 1F + } + + return Observable.just(listOf(chapter)) + } + + override fun chapterListSelector(): String { + throw UnsupportedOperationException() + } + + override fun chapterFromElement(element: Element): SChapter { + throw UnsupportedOperationException() + } + + // =============================== Pages ================================ + override fun pageListParse(document: Document): List { + return document.select("div.thumbs a.gallerythumb > img").mapIndexed { index, item -> + val url = item.getImageUrl() + // Show original images instead of previews + val imageUrl = url.substringBeforeLast('/') + "/" + url.substringAfterLast('/').replace("t.", ".") + Page(index, "", imageUrl) + } + } + + override fun imageUrlParse(document: Document): String { + throw UnsupportedOperationException() + } + + // ============================= Utilities ============================== + private fun Element.getImageUrl() = absUrl("data-src").ifEmpty { absUrl("src") } + + companion object { + const val PREFIX_SEARCH = "id:" + } +} diff --git a/src/all/hentaicafe/src/eu/kanade/tachiyomi/extension/all/hentaicafe/HentaiCafeUrlActivity.kt b/src/all/hentaicafe/src/eu/kanade/tachiyomi/extension/all/hentaicafe/HentaiCafeUrlActivity.kt new file mode 100644 index 000000000..84011a91e --- /dev/null +++ b/src/all/hentaicafe/src/eu/kanade/tachiyomi/extension/all/hentaicafe/HentaiCafeUrlActivity.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.extension.all.hentaicafe + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +/** + * Springboard that accepts https://hentaicafe.xxx/g/ intents + * and redirects them to the main Tachiyomi process. + */ +class HentaiCafeUrlActivity : Activity() { + + private val tag = javaClass.simpleName + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val item = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${HentaiCafe.PREFIX_SEARCH}$item") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e(tag, e.toString()) + } + } else { + Log.e(tag, "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}