diff --git a/src/en/tsumino/build.gradle b/src/en/tsumino/build.gradle new file mode 100644 index 000000000..db9fda667 --- /dev/null +++ b/src/en/tsumino/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Tsumino' + pkgNameSuffix = 'en.tsumino' + extClass = '.Tsumino' + extVersionCode = 1 + libVersion = '1.2' +} + +dependencies { + compileOnly 'com.google.code.gson:gson:2.8.5' + compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/tsumino/res/mipmap-hdpi/ic_launcher.png b/src/en/tsumino/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..fad9e2ce3 Binary files /dev/null and b/src/en/tsumino/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/tsumino/res/mipmap-mdpi/ic_launcher.png b/src/en/tsumino/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..7d5ebe101 Binary files /dev/null and b/src/en/tsumino/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/tsumino/res/mipmap-xhdpi/ic_launcher.png b/src/en/tsumino/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..790fda0a5 Binary files /dev/null and b/src/en/tsumino/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/tsumino/res/mipmap-xxhdpi/ic_launcher.png b/src/en/tsumino/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..c576238ae Binary files /dev/null and b/src/en/tsumino/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/tsumino/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/tsumino/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..99543019c Binary files /dev/null and b/src/en/tsumino/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/tsumino/res/web_hi_res_512.png b/src/en/tsumino/res/web_hi_res_512.png new file mode 100644 index 000000000..69069ff19 Binary files /dev/null and b/src/en/tsumino/res/web_hi_res_512.png differ diff --git a/src/en/tsumino/src/eu/kanade/tachiyomi/extension/en/tsumino/Tsumino.kt b/src/en/tsumino/src/eu/kanade/tachiyomi/extension/en/tsumino/Tsumino.kt new file mode 100644 index 000000000..e40a959e9 --- /dev/null +++ b/src/en/tsumino/src/eu/kanade/tachiyomi/extension/en/tsumino/Tsumino.kt @@ -0,0 +1,167 @@ +package eu.kanade.tachiyomi.extension.en.tsumino + +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.get +import com.google.gson.Gson +import com.google.gson.JsonObject +import eu.kanade.tachiyomi.extension.en.tsumino.TsuminoUtils.Companion.getArtists +import eu.kanade.tachiyomi.extension.en.tsumino.TsuminoUtils.Companion.getDate +import eu.kanade.tachiyomi.extension.en.tsumino.TsuminoUtils.Companion.getDesc +import eu.kanade.tachiyomi.extension.en.tsumino.TsuminoUtils.Companion.getGroups +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +class Tsumino: ParsedHttpSource() { + + override val name = "Tsumino" + + override val baseUrl = "https://tsumino.com" + + override val lang = "en" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + private val gson = Gson() + + override fun latestUpdatesSelector() = "Not needed" + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/Search/Operate/?PageNumber=$page&Sort=Newest") + + override fun latestUpdatesParse(response: Response): MangasPage { + val allManga = mutableListOf() + val jsonManga = gson.fromJson(response.body()!!.string())["data"].asJsonArray + + for (i in 0 until jsonManga.size()) { + val manga = SManga.create() + manga.url = "/entry/" + jsonManga[i]["entry"]["id"].asString + manga.title = jsonManga[i]["entry"]["title"].asString + manga.thumbnail_url = jsonManga[i]["entry"]["thumbnailUrl"].asString + allManga.add(manga) + } + + return MangasPage(allManga, true) + } + + override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesNextPageSelector() = "Not needed" + + override fun popularMangaRequest(page: Int) = GET("$baseUrl/Search/Operate/?PageNumber=$page&Sort=Popularity") + + override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response) + + override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element) + + override fun popularMangaSelector() = latestUpdatesSelector() + + override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector() + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = + throw UnsupportedOperationException("Not implemented yet") + + override fun searchMangaSelector() = latestUpdatesSelector() + + override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element) + + override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() + + override fun mangaDetailsRequest(manga: SManga): Request { + if (manga.url.startsWith("http")) { + return GET(manga.url, headers) + } + return super.mangaDetailsRequest(manga) + } + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div.book-page-container") + val manga = SManga.create() + val genres = mutableListOf() + + infoElement.select("#Tag a").forEach { element -> + val genre = element.text() + genres.add(genre) + } + + manga.title = infoElement.select("#Title").text() + manga.artist = getArtists(document) + manga.author = manga.artist + manga.status = SManga.COMPLETED + manga.genre = genres.joinToString(", ") + manga.thumbnail_url = infoElement.select("img").attr("src") + manga.description = getDesc(document) + + return manga + } + override fun chapterListRequest(manga: SManga): Request { + if (manga.url.startsWith("http")) { + return GET(manga.url, headers) + } + return super.chapterListRequest(manga) + } + + override fun chapterListParse(response: Response): List { + val chapterList = mutableListOf() + val document = response.asJsoup() + val collection = document.select(chapterListSelector()) + if (collection.isNotEmpty()) { + return collection.map { element -> + SChapter.create().apply { + name = element.text() + scanlator = getGroups(document) + setUrlWithoutDomain(element.attr("href"). + replace("entry", "Read/Index")) + } + }.reversed() + } else { + val chapter = SChapter.create().apply { + name = "Chapter" + scanlator = getGroups(document) + chapter_number = 1f + setUrlWithoutDomain(response.request().url().encodedPath(). + replace("entry", "Read/Index")) + } + chapterList.add(chapter) + return chapterList + } + } + + override fun chapterListSelector() = ".book-collection-table a" + + override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Not used") + + override fun pageListRequest(chapter: SChapter): Request { + if (chapter.url.startsWith("http")) { + return GET(chapter.url, headers) + } + return super.pageListRequest(chapter) + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + val numPages = document.select("h1").text().split(" ").last() + + if (numPages.isNotEmpty()) { + for (i in 1 until numPages.toInt() + 1) { + val data = document.select("#image-container").attr("data-cdn") + .replace("[PAGE]", i.toString()) + pages.add(Page(i, "", data)) + } + } else { + throw UnsupportedOperationException("Error: Open in WebView and solve the Captcha!") + } + return pages + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") + + override fun getFilterList() = FilterList() +} diff --git a/src/en/tsumino/src/eu/kanade/tachiyomi/extension/en/tsumino/TsuminoUtils.kt b/src/en/tsumino/src/eu/kanade/tachiyomi/extension/en/tsumino/TsuminoUtils.kt new file mode 100644 index 000000000..11bc1e0b2 --- /dev/null +++ b/src/en/tsumino/src/eu/kanade/tachiyomi/extension/en/tsumino/TsuminoUtils.kt @@ -0,0 +1,82 @@ +package eu.kanade.tachiyomi.extension.en.tsumino + +import org.jsoup.nodes.Document +import java.lang.StringBuilder +import java.text.SimpleDateFormat +import java.util.* + +class TsuminoUtils { + companion object { + fun getArtists(document: Document): String { + val stringBuilder = StringBuilder() + val artists = document.select("#Artist a") + + artists.forEach { + stringBuilder.append(it.text()) + + if (it != artists.last()) + stringBuilder.append(", ") + } + + return stringBuilder.toString() + } + + fun getGroups(document: Document): String? { + val stringBuilder = StringBuilder() + val groups = document.select("#Group a") + + groups.forEach { + stringBuilder.append(it.text()) + + if (it != groups.last()) + stringBuilder.append(", ") + } + + return if (stringBuilder.toString().isEmpty()) null else stringBuilder.toString() + } + + fun getDesc(document: Document): String { + val stringBuilder = StringBuilder() + val pages = document.select("#Pages").text() + val parodies = document.select("#Parody a") + val characters = document.select("#Character a") + + stringBuilder.append("Pages: $pages") + + if (parodies.size > 0) { + stringBuilder.append("\n\n") + stringBuilder.append("Parodies: ") + + parodies.forEach { + stringBuilder.append(it.text()) + + if (it != parodies.last()) + stringBuilder.append(", ") + } + } + + if (characters.size > 0) { + stringBuilder.append("\n\n") + stringBuilder.append("Characters: ") + + characters.forEach { + stringBuilder.append(it.text()) + + if (it != characters.last()) + stringBuilder.append(", ") + } + } + return stringBuilder.toString() + } + + fun getDate(document: Document): Long { + val timeString = document.select("#Uploaded").text() + return try { + SimpleDateFormat("yyyy MMMMM dd", Locale.ENGLISH).parse(timeString).time + }catch (e: Exception) { + 0 + } + } + + } +}