diff --git a/src/en/guya/build.gradle b/src/en/guya/build.gradle new file mode 100644 index 000000000..fcdf10246 --- /dev/null +++ b/src/en/guya/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Guya' + pkgNameSuffix = "en.guya" + extClass = '.Guya' + extVersionCode = 1 + libVersion = '1.2' +} + +dependencies { + compileOnly project(':preference-stub') + compileOnly 'com.github.inorichi.injekt:injekt-core:65b0440' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/guya/res/mipmap-hdpi/ic_launcher.png b/src/en/guya/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 000000000..21c05b7af Binary files /dev/null and b/src/en/guya/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/guya/res/mipmap-mdpi/ic_launcher.png b/src/en/guya/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 000000000..bfbd6b513 Binary files /dev/null and b/src/en/guya/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/guya/res/mipmap-xhdpi/ic_launcher.png b/src/en/guya/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 000000000..578a69733 Binary files /dev/null and b/src/en/guya/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/guya/res/mipmap-xxhdpi/ic_launcher.png b/src/en/guya/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 000000000..ec660aec8 Binary files /dev/null and b/src/en/guya/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/guya/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/guya/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 000000000..c1a5a40aa Binary files /dev/null and b/src/en/guya/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/guya/res/web_hi_res_512.png b/src/en/guya/res/web_hi_res_512.png new file mode 100755 index 000000000..fc77eb56a Binary files /dev/null and b/src/en/guya/res/web_hi_res_512.png differ diff --git a/src/en/guya/src/eu/kanade/tachiyomi/extension/en/guya/Guya.kt b/src/en/guya/src/eu/kanade/tachiyomi/extension/en/guya/Guya.kt new file mode 100644 index 000000000..3e82bb46f --- /dev/null +++ b/src/en/guya/src/eu/kanade/tachiyomi/extension/en/guya/Guya.kt @@ -0,0 +1,336 @@ +package eu.kanade.tachiyomi.extension.en.guya + +import android.app.Application +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.ConfigurableSource +import android.content.SharedPreferences +import android.support.v7.preference.ListPreference +import android.support.v7.preference.PreferenceScreen +import okhttp3.* +import org.json.JSONObject +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.IOException +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.collections.ArrayList + +open class Guya() : ConfigurableSource, HttpSource() { + + override val name = "Guya" + override val baseUrl = "https://guya.moe" + override val supportsLatest = false + override val lang = "en" + + private val Scanlators: ScanlatorStore = ScanlatorStore() + + // Preferences confirguration + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + private val SCANLATOR_PREFERENCE = "SCANLATOR_PREFERENCE" + + // Request builder for the "browse" page of the manga + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/api/get_all_series") + } + + // Gets the response object from the request + override fun popularMangaParse(response: Response): MangasPage { + val res = response.body()!!.string() + return parseManga(JSONObject(res)) + } + + // Overridden to use our overload + override fun fetchMangaDetails(manga: SManga): Observable { + return clientBuilder().newCall(GET("$baseUrl/api/get_all_series/")) + .asObservableSuccess() + .map {response -> + mangaDetailsParse(response, manga) + } + } + + // Called when the series is loaded, or when opening in browser + override fun mangaDetailsRequest(manga: SManga): Request { + return GET("$baseUrl/reader/series/" + manga.url) + } + + // Stub + override fun mangaDetailsParse(response: Response): SManga { + throw Exception("Unused") + } + + private fun mangaDetailsParse(response: Response, manga: SManga): SManga { + val res = response.body()!!.string() + return parseMangaFromJson(JSONObject(res).getJSONObject(manga.title), manga.title) + } + + // Gets the chapter list based on the series being viewed + override fun chapterListRequest(manga: SManga): Request { + return GET("$baseUrl/api/series/" + manga.url) + } + + // Called after the request + override fun chapterListParse(response: Response): List { + val res = response.body()!!.string() + return parseChapterList(res) + } + + // Overridden fetch so that we use our overloaded method instead + override fun fetchPageList(chapter: SChapter): Observable> { + return clientBuilder().newCall(pageListRequest(chapter)) + .asObservableSuccess() + .map { response -> + pageListParse(response, chapter) + } + } + + // Stub + override fun pageListParse(response: Response): List { + throw Exception("Unused") + } + + private fun pageListParse(response: Response, chapter: SChapter): List { + val res = response.body()!!.string() + + val json = JSONObject(res) + val chapterNum = chapter.name.split(" - ")[0] + val pages = json.getJSONObject("chapters") + .getJSONObject(chapterNum) + .getJSONObject("groups") + val metadata = JSONObject() + + metadata.put("chapter", chapterNum) + metadata.put("scanlator", Scanlators.getKeyFromValue(chapter.scanlator.toString())) + metadata.put("slug", json.getString("slug")) + metadata.put("folder", json.getJSONObject("chapters") + .getJSONObject(chapterNum) + .getString("folder")) + + return parsePageFromJson(pages, metadata) + } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() + .map { response -> + searchMangaParse(response, query) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return GET("$baseUrl/api/get_all_series") + } + + override fun searchMangaParse(response: Response): MangasPage { + throw Exception("Unused.") + } + + private fun searchMangaParse(response: Response, query: String): MangasPage { + val res = response.body()!!.string() + var json = JSONObject(res) + val truncatedJSON = JSONObject() + + val iter = json.keys() + + while (iter.hasNext()) { + val candidate = iter.next() + if (candidate.contains(query.toRegex(RegexOption.IGNORE_CASE))) { + truncatedJSON.put(candidate, json.get(candidate)) + } + } + + return parseManga(truncatedJSON) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + val preference = ListPreference(screen.context).apply { + key = "preferred_scanlator" + title = "Preferred scanlator" + var scanlators = arrayOf() + var scanlatorsIndex = arrayOf() + for (key in Scanlators.keys()) { + scanlators += Scanlators.getValueFromKey(key) + scanlatorsIndex += key + } + entries = scanlators + entryValues = scanlatorsIndex + summary = "%s" + this.setDefaultValue(1) + + setOnPreferenceChangeListener{_, newValue -> + val selected = newValue.toString() + val index = (this.findIndexOfValue(selected) + 1).toString() + preferences.edit().putString(SCANLATOR_PREFERENCE, index).commit() + } + } + + screen.addPreference(preference) + } + + // ------------- Helpers and whatnot --------------- + + private fun parseChapterList(payload: String): List { + val response = JSONObject(payload) + val chapters = response.getJSONObject("chapters") + + val chapterList = ArrayList() + + val iter = chapters.keys() + + while (iter.hasNext()) { + val chapter = iter.next() + val chapterObj = chapters.getJSONObject(chapter) + chapterList.add(parseChapterFromJson(chapterObj, chapter, response.getString("slug"))) + } + + return chapterList.reversed() + } + + // Helper function to get all the listings + private fun parseManga(payload: JSONObject) : MangasPage { + val mangas = ArrayList() + + val iter = payload.keys() + + while (iter.hasNext()) { + val series = iter.next() + val json = payload.getJSONObject(series) + val manga = parseMangaFromJson(json, series) + mangas.add(manga) + } + + return MangasPage(mangas, false) + } + + // Takes a json of the manga to parse + private fun parseMangaFromJson(json: JSONObject, title: String): SManga { + val manga = SManga.create() + manga.title = title + manga.artist = json.getString("artist") + manga.author = json.getString("author") + manga.description = json.getString("description") + manga.url = json.getString("slug") + manga.thumbnail_url = "$baseUrl/" + json.getString("cover") + return manga + } + + private fun parseChapterFromJson(json: JSONObject, num: String, slug: String): SChapter { + val chapter = SChapter.create() + + // Get the scanlator info based on group ranking; do it first since we need it later + val firstGroupId = getBestScanlator(json.getJSONObject("groups")) + chapter.scanlator = Scanlators.getValueFromKey(firstGroupId) + chapter.name = num + " - " + json.getString("title") + chapter.chapter_number = num.toFloat() + chapter.url = "/api/series/$slug/$num/1" + + return chapter + } + + private fun parsePageFromJson(json: JSONObject, metadata: JSONObject): List { + val pages = json.getJSONArray(metadata.getString("scanlator")) + val pageArray = ArrayList() + + for (i in 0 until pages.length()) { + val page = Page(i + 1, "", pageBuilder(metadata.getString("slug"), + metadata.getString("folder"), + pages[i].toString(), + metadata.getString("scanlator"))) + pageArray.add(page) + } + + return pageArray + } + + private fun getBestScanlator(json: JSONObject): String { + val preferred = preferences.getString(SCANLATOR_PREFERENCE, null) + + return if (preferred != null && json.has(preferred)) { + preferred + } else { + json.keys().next() + } + } + + private fun pageBuilder(slug: String, folder: String, filename: String, groupId: String): String { + return "$baseUrl/media/manga/$slug/chapters/$folder/$groupId/$filename" + } + + private fun clientBuilder(): OkHttpClient = network.cloudflareClient.newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build()!! + + inner class ScanlatorStore { + private val scanlatorMap = HashMap() + private var polling = false + + init { + update() + } + + fun getKeyFromValue(value: String): String { + update() + for (key in scanlatorMap.keys) { + if (scanlatorMap[key].equals(value)) { + return key + } + } + + throw Exception("Cannot find scanlator group!") + } + + fun getValueFromKey(key: String): String { + update() + return if (!scanlatorMap[key].isNullOrEmpty()) + scanlatorMap[key].toString() else "No info" + } + + fun keys(): MutableSet { + update() + return scanlatorMap.keys + } + + private fun update() { + if (scanlatorMap.isEmpty() && !polling) { + polling = true + clientBuilder().newCall(GET("$baseUrl/api/get_all_groups")).enqueue( + object: Callback { + override fun onResponse(call: Call, response: Response) { + val json = JSONObject(response.body()!!.string()) + val iter = json.keys() + while (iter.hasNext()) { + val scanId = iter.next() + scanlatorMap[scanId] = json.getString(scanId) + } + polling = false + } + override fun onFailure(call: Call, e: IOException) { + polling = false + } + } + ) + } + } + } + + // ----------------- Things we aren't supporting ----------------- + + override fun imageUrlParse(response: Response): String { + throw Exception("imageUrlParse not supported.") + } + + override fun latestUpdatesRequest(page: Int): Request { + throw Exception("Latest updates not supported.") + } + + override fun latestUpdatesParse(response: Response): MangasPage { + throw Exception("Latest updates not supported.") + } + +}