diff --git a/src/all/genkanio/AndroidManifest.xml b/src/all/genkanio/AndroidManifest.xml new file mode 100644 index 000000000..be2ea6bbd --- /dev/null +++ b/src/all/genkanio/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="eu.kanade.tachiyomi.extension"> + + <application> + <activity + android:name=".all.genkanio.GenkanIOUrlActivity" + android:excludeFromRecents="true" + android:theme="@android:style/Theme.NoDisplay"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data + android:host="genkan.io" + android:pathPattern="/manga/..*" + android:scheme="https" /> + </intent-filter> + + </activity> + </application> +</manifest> \ No newline at end of file diff --git a/src/all/genkanio/build.gradle b/src/all/genkanio/build.gradle new file mode 100644 index 000000000..eb0a37920 --- /dev/null +++ b/src/all/genkanio/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Genkan.io' + pkgNameSuffix = "all.genkanio" + extClass = '.GenkanIO' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/genkanio/res/mipmap-hdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..0a2c55aa7 Binary files /dev/null and b/src/all/genkanio/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/genkanio/res/mipmap-mdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e6f0260ff Binary files /dev/null and b/src/all/genkanio/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/genkanio/res/mipmap-xhdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..ca210a848 Binary files /dev/null and b/src/all/genkanio/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/genkanio/res/mipmap-xxhdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..872736ea2 Binary files /dev/null and b/src/all/genkanio/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/genkanio/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..e3e31f8c0 Binary files /dev/null and b/src/all/genkanio/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/genkanio/res/web_hi_res_512.png b/src/all/genkanio/res/web_hi_res_512.png new file mode 100644 index 000000000..3c424c17c Binary files /dev/null and b/src/all/genkanio/res/web_hi_res_512.png differ diff --git a/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIO.kt b/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIO.kt new file mode 100644 index 000000000..704f4d331 --- /dev/null +++ b/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIO.kt @@ -0,0 +1,182 @@ +package eu.kanade.tachiyomi.extension.all.genkanio + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +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.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import java.util.Calendar + +open class GenkanIO() : ParsedHttpSource() { + override val lang = "all" + final override val name = "Genkan.io" + final override val baseUrl = "https://genkan.io" + final override val supportsLatest = false + + // genkan.io defaults to listing manga alphabetically, and provides no configuration + fun alphabeticalMangaRequest(page: Int): Request = GET("$baseUrl/manga?page=$page", headers) + + fun alphabeticalMangaFromElement(element: Element): SManga { + val manga = SManga.create() + + element.select("a").let { + manga.url = it.attr("href").substringAfter(baseUrl) + manga.title = it.text() + } + manga.thumbnail_url = element.select("img").attr("src") + return manga + } + + fun alphabeticalMangaSelector() = "ul[role=list] > li" + fun alphabeticalMangaNextPageSelector() = "a[rel=next]" + + // popular + override fun popularMangaRequest(page: Int) = alphabeticalMangaRequest(page) + override fun popularMangaFromElement(element: Element) = alphabeticalMangaFromElement(element) + override fun popularMangaSelector() = alphabeticalMangaSelector() + override fun popularMangaNextPageSelector() = alphabeticalMangaNextPageSelector() + + // latest + + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("Not used") + override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException("Not used") + override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used") + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used") + + // search + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { + return client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() + .map { response -> + // Genkan.io redirects if the search query contains characters it deems "illegal" (e.g: '-') + // Return no responses if any redirects occurred + if (response.priorResponse != null) + MangasPage(emptyList(), false) + else + searchMangaParse(response) + } + } + + override fun searchMangaRequest(page: Int, query: String, @Suppress("UNUSED_PARAMETER") filters: FilterList): Request { + val url = "$baseUrl/manga".toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("page", "$page") + .addQueryParameter("search", query) + return GET("$url") + } + + override fun searchMangaFromElement(element: Element) = alphabeticalMangaFromElement(element) + override fun searchMangaSelector() = alphabeticalMangaSelector() + override fun searchMangaNextPageSelector() = alphabeticalMangaNextPageSelector() + + // chapter list (is paginated), + override fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException("Not used") + override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException("Not used") + data class ChapterPage(val chapters: List<SChapter>, val hasnext: Boolean) + + override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { + return if (manga.status != SManga.LICENSED) { + // Returns an observable which emits the list of chapters found on a page, + // for every page starting from specified page + fun getAllPagesFrom(page: Int): Observable<List<SChapter>> = + client.newCall(chapterListRequest(manga, page)) + .asObservableSuccess() + .concatMap { response -> + val cp = chapterPageParse(response) + if (cp.hasnext) + Observable.just(cp.chapters).concatWith(getAllPagesFrom(page + 1)) + else + Observable.just(cp.chapters) + } + getAllPagesFrom(1).reduce(List<SChapter>::plus) + } else { + Observable.error(Exception("Licensed - No chapters to show")) + } + } + + private fun chapterPageParse(response: Response): ChapterPage { + val document = response.asJsoup() + + val mangas = document.select(chapterListSelector()).map { element -> + chapterFromElement(element) + } + + val hasNextPage = chapterListNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return ChapterPage(mangas, hasNextPage) + } + + private fun chapterListRequest(manga: SManga, page: Int): Request { + val url = "$baseUrl${manga.url}".toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("page", "$page") + return GET("$url", headers) + } + + override fun chapterFromElement(element: Element): SChapter = element.children().let { tablerow -> + val isTitleBlank: (String) -> Boolean = { s: String -> s == "-" || s.isBlank() } + val (numElem, nameElem, languageElem, groupElem, viewsElem) = tablerow + val (releasedElem, urlElem) = Pair(tablerow[5], tablerow[6]) + SChapter.create().apply { + name = if (isTitleBlank(nameElem.text())) "Chapter ${numElem.text()}" else "Ch. ${numElem.text()}: ${nameElem.text()}" + url = urlElem.select("a").attr("href").substringAfter(baseUrl) + date_upload = parseRelativeDate(releasedElem.text()) ?: 0 + scanlator = "${groupElem.text()} - ${languageElem.text()}" + chapter_number = numElem.text().toFloat() + } + } + + override fun chapterListSelector() = "tbody > tr" + fun chapterListNextPageSelector() = "a[rel=next]" + + // manga + + override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + thumbnail_url = document.selectFirst("section > div > img").attr("src") + status = SManga.UNKNOWN // unreported + artist = null // unreported + author = null // unreported + description = document.selectFirst("h2").nextElementSibling().text() + .plus("\n\n\n") + // Add additional details from info table + .plus( + document.select("ul.mt-1").joinToString("\n") { + "${it.previousElementSibling().text()}: ${it.text()}" + } + ) + } + + private fun parseRelativeDate(date: String): Long? { + val trimmedDate = date.substringBefore(" ago").removeSuffix("s").split(" ") + + val calendar = Calendar.getInstance() + when (trimmedDate[1]) { + "year" -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) } + "month" -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) } + "week" -> calendar.apply { add(Calendar.WEEK_OF_MONTH, -trimmedDate[0].toInt()) } + "day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) } + "hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) } + "minute" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) } + "second" -> calendar.apply { add(Calendar.SECOND, 0) } + } + + return calendar.timeInMillis + } + + // Pages + override fun pageListParse(document: Document): List<Page> = document.select("main > div > img").mapIndexed { index, img -> + Page(index, "", img.attr("src")) + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") +} diff --git a/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIOUrlActivity.kt b/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIOUrlActivity.kt new file mode 100644 index 000000000..4f8705fb7 --- /dev/null +++ b/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIOUrlActivity.kt @@ -0,0 +1,44 @@ +package eu.kanade.tachiyomi.extension.all.genkanio + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class GenkanIOUrlActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size >= 2) { + // url scheme is of the form "/manga/ID-MANGANAME" + val (_, titleComponent) = pathSegments + + // This is essentially substringBefore(titleComponent, '-'), don't have access to stdlib + var titleId = "" + for (i in 0 until titleComponent.length) { + if (titleComponent[i] == '-') break + titleId = titleId.plus(titleComponent[i]) + } + + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", titleId) + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("GenkanIOUrlActivity", e.toString()) + } + } else { + Log.e("GenkanIOUrlActivity", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}