diff --git a/src/ca/fansubscat/AndroidManifest.xml b/src/ca/fansubscat/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/ca/fansubscat/AndroidManifest.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest package="eu.kanade.tachiyomi.extension" /> diff --git a/src/ca/fansubscat/build.gradle b/src/ca/fansubscat/build.gradle new file mode 100644 index 000000000..6c723cb91 --- /dev/null +++ b/src/ca/fansubscat/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Fansubs.cat' + pkgNameSuffix = 'ca.fansubscat' + extClass = '.FansubsCat' + extVersionCode = 1 + libVersion = '1.2' + containsNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ca/fansubscat/res/mipmap-hdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..8e0468783 Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ca/fansubscat/res/mipmap-mdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e0702ecea Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ca/fansubscat/res/mipmap-xhdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..b0027ad57 Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ca/fansubscat/res/mipmap-xxhdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..2424e711f Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ca/fansubscat/res/mipmap-xxxhdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..ccc47ea5f Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ca/fansubscat/res/web_hi_res_512.png b/src/ca/fansubscat/res/web_hi_res_512.png new file mode 100644 index 000000000..8e8555a10 Binary files /dev/null and b/src/ca/fansubscat/res/web_hi_res_512.png differ diff --git a/src/ca/fansubscat/src/eu/kanade/tachiyomi/extension/ca/fansubscat/FansubsCat.kt b/src/ca/fansubscat/src/eu/kanade/tachiyomi/extension/ca/fansubscat/FansubsCat.kt new file mode 100644 index 000000000..2f9d34e86 --- /dev/null +++ b/src/ca/fansubscat/src/eu/kanade/tachiyomi/extension/ca/fansubscat/FansubsCat.kt @@ -0,0 +1,160 @@ +package eu.kanade.tachiyomi.extension.ca.fansubscat + +import com.github.salomonbrys.kotson.float +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.get +import com.github.salomonbrys.kotson.long +import com.github.salomonbrys.kotson.nullString +import com.github.salomonbrys.kotson.string +import com.google.gson.Gson +import com.google.gson.JsonObject +import eu.kanade.tachiyomi.extension.BuildConfig +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.HttpSource +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import rx.Observable + +class FansubsCat : HttpSource() { + + override val name = "Fansubs.cat" + + override val baseUrl = "https://manga.fansubs.cat" + + override val lang = "ca" + + override val supportsLatest = true + + override fun headersBuilder(): Headers.Builder = Headers.Builder() + .add("User-Agent", "Tachiyomi/FansubsCat/${BuildConfig.VERSION_NAME}") + + override val client: OkHttpClient = network.client + + private val gson = Gson() + + private val apiBaseUrl = "https://api.fansubs.cat" + + private fun parseMangaFromJson(response: Response): MangasPage { + val jsonObject = gson.fromJson<JsonObject>(response.body()!!.string()) + + val mangas = jsonObject["result"].asJsonArray.map { json -> + SManga.create().apply { + url = json["slug"].string + title = json["name"].string + thumbnail_url = json["thumbnail_url"].string + author = json["author"].nullString + description = json["synopsis"].nullString + status = json["status"].string.toStatus() + genre = json["genres"].nullString + } + } + + return MangasPage(mangas, mangas.size >= 20) + } + + private fun parseChapterListFromJson(response: Response): List<SChapter> { + val jsonObject = gson.fromJson<JsonObject>(response.body()!!.string()) + + return jsonObject["result"].asJsonArray.map { json -> + SChapter.create().apply { + url = json["id"].string + name = json["title"].string + chapter_number = json["number"].float + scanlator = json["fansub"].string + date_upload = json["created"].long + } + } + } + + private fun parsePageListFromJson(response: Response): List<Page> { + val jsonObject = gson.fromJson<JsonObject>(response.body()!!.string()) + + return jsonObject["result"].asJsonArray.mapIndexed { i, it -> + Page(i, it["url"].asString, it["url"].asString) + } + } + + // Popular + + override fun popularMangaRequest(page: Int): Request { + return GET("$apiBaseUrl/manga/popular/$page", headers) + } + + override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response) + + // Latest + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$apiBaseUrl/manga/recent/$page", headers) + } + + override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response) + + // Search + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = HttpUrl.parse("$apiBaseUrl/manga/search/$page")!!.newBuilder() + .addQueryParameter("query", query) + return GET(url.toString(), headers) + } + + override fun searchMangaParse(response: Response): MangasPage = parseMangaFromJson(response) + + // Details + + // Workaround to allow "Open in browser" to use the real URL + override fun fetchMangaDetails(manga: SManga): Observable<SManga> = + client.newCall(apiMangaDetailsRequest(manga)).asObservableSuccess() + .map { mangaDetailsParse(it).apply { initialized = true } } + + // Return the real URL for "Open in browser" + override fun mangaDetailsRequest(manga: SManga) = GET("$baseUrl/${manga.url}", headers) + + private fun apiMangaDetailsRequest(manga: SManga): Request { + return GET("$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}", headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + val jsonObject = gson.fromJson<JsonObject>(response.body()!!.string()) + + return SManga.create().apply { + url = jsonObject["result"]["slug"].string + title = jsonObject["result"]["name"].string + thumbnail_url = jsonObject["result"]["thumbnail_url"].string + author = jsonObject["result"]["author"].nullString + description = jsonObject["result"]["synopsis"].nullString + status = jsonObject["result"]["status"].string.toStatus() + genre = jsonObject["result"]["genres"].nullString + } + } + + private fun String?.toStatus() = when { + this == null -> SManga.UNKNOWN + this.contains("ongoing", ignoreCase = true) -> SManga.ONGOING + this.contains("finished", ignoreCase = true) -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + // Chapters + + override fun chapterListRequest(manga: SManga): Request = GET("$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}", headers) + + override fun chapterListParse(response: Response): List<SChapter> = parseChapterListFromJson(response) + + // Pages + + override fun pageListRequest(chapter: SChapter): Request = GET("$apiBaseUrl/manga/pages/${chapter.url}", headers) + + override fun pageListParse(response: Response): List<Page> = parsePageListFromJson(response) + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used") +}