diff --git a/src/pt/mangalivre/build.gradle b/src/pt/mangalivre/build.gradle new file mode 100644 index 000000000..e2f939df3 --- /dev/null +++ b/src/pt/mangalivre/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Manga Livre' + extClass = '.MangaLivre' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/mangalivre/res/mipmap-hdpi/ic_launcher.png b/src/pt/mangalivre/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..7f7a2fd61 Binary files /dev/null and b/src/pt/mangalivre/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/mangalivre/res/mipmap-mdpi/ic_launcher.png b/src/pt/mangalivre/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..ec492d976 Binary files /dev/null and b/src/pt/mangalivre/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/mangalivre/res/mipmap-xhdpi/ic_launcher.png b/src/pt/mangalivre/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..fa44977fe Binary files /dev/null and b/src/pt/mangalivre/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/mangalivre/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/mangalivre/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..22da04c97 Binary files /dev/null and b/src/pt/mangalivre/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/mangalivre/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/mangalivre/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..5b88deff5 Binary files /dev/null and b/src/pt/mangalivre/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivre.kt b/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivre.kt new file mode 100644 index 000000000..d9241dd6d --- /dev/null +++ b/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivre.kt @@ -0,0 +1,150 @@ +package eu.kanade.tachiyomi.extension.pt.mangalivre + +import android.annotation.SuppressLint +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.interceptor.rateLimit +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 kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat + +class MangaLivre : HttpSource() { + + override val name = "Manga Livre" + + override val baseUrl = "https://mangalivre.one" + + override val lang = "pt-BR" + + override val supportsLatest = false + + private val json: Json by injectLazy() + + override val client = network.cloudflareClient.newBuilder() + .rateLimit(2) + .build() + + // ============================== Popular =============================== + + override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList()) + + override fun popularMangaParse(response: Response) = searchMangaParse(response) + + // ============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int) = + throw UnsupportedOperationException() + + override fun latestUpdatesParse(response: Response) = + throw UnsupportedOperationException() + + // ============================== Search =============================== + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$API_URL/manga".toHttpUrl().newBuilder() + .addQueryParameter("per_page", "50") + .addQueryParameter("page", "$page") + .addQueryParameter("name", query) + .build() + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + val page = response.parseAs() + val mangas = page.mangas.map { + SManga.create().apply { + title = it.name + description = it.synopsis + thumbnail_url = "$CDN_URL/${it.photo}" + url = "/manga/slug/${it.slug}#${it.id}" + } + } + return MangasPage(mangas, page.hasNextPage()) + } + + // ============================== Details =============================== + + override fun getMangaUrl(manga: SManga): String { + val slug = manga.url + .substringAfterLast("/") + .removeComment() + return "$baseUrl/manga/$slug" + } + + override fun mangaDetailsRequest(manga: SManga) = + GET("$API_URL${manga.url.removeComment()}", headers) + + override fun mangaDetailsParse(response: Response): SManga { + val mangaDto = response.parseAs() + return SManga.create().apply { + title = mangaDto.name + description = mangaDto.synopsis + thumbnail_url = "$CDN_URL/${mangaDto.photo}" + genre = mangaDto.genre?.joinToString { it.value } + mangaDto.status?.let { + status = when (it.value) { + "Ativo" -> SManga.ONGOING + else -> SManga.UNKNOWN + } + } + url = "/manga/slug/${mangaDto.slug}#${mangaDto.id}" + } + } + + // ============================== Chapters =============================== + + override fun chapterListRequest(manga: SManga): Request { + val id = manga.url.substringAfterLast("#") + return GET("$API_URL/chapter/manga/all/$id", headers) + } + + override fun chapterListParse(response: Response): List { + return response.parseAs>().map { + SChapter.create().apply { + name = "${it.chapter} - ${it.title}" + date_upload = it.createdAt.toDate() + chapter_number = it.chapter.toFloat() + url = "/chapter/${it.id}" + } + } + } + + private fun String.toDate(): Long = dateFormat.parse(this)?.time ?: 0L + + // ============================== Pages =============================== + + override fun pageListRequest(chapter: SChapter) = GET("$API_URL${chapter.url}", headers) + + override fun pageListParse(response: Response): List { + return response.parseAs().pages.mapIndexed { index, page -> + Page(index, imageUrl = "$CDN_URL/${page.url}") + } + } + + override fun imageUrlParse(response: Response) = "" + + // ============================= Utilities ============================== + + private inline fun Response.parseAs(): T = use { + json.decodeFromStream(it.body.byteStream()) + } + + private fun String.removeComment() = this.substringBeforeLast("#") + + companion object { + const val API_URL = "https://api.mangalivre.one" + const val CDN_URL = "https://cdn.mangalivre.one" + + @SuppressLint("SimpleDateFormat") + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'") + } +} diff --git a/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivreDto.kt b/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivreDto.kt new file mode 100644 index 000000000..6783d8612 --- /dev/null +++ b/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivreDto.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.extension.pt.mangalivre + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MangaLivreDto( + @SerialName("data") + val mangas: List, + val path: String, + @SerialName("current_page") + val currentPage: Int, + @SerialName("last_page") + val lastPage: Int, +) { + fun hasNextPage(): Boolean = currentPage < lastPage +} + +@Serializable +data class MangaDto( + val id: Int, + val name: String, + val photo: String, + val slug: String, + val synopsis: String, + val status: Name?, + @SerialName("categories") + val genre: List?, +) + +@Serializable +data class Name( + @SerialName("name") + val value: String, +) + +@Serializable +data class ChapterDto( + val id: Int, + val title: String, + val chapter: String, + @SerialName("created_at") + val createdAt: String, +) + +@Serializable +data class MangaPageDto( + @SerialName("manga_pages") + val pages: List, +) + +@Serializable +data class PageDto( + @SerialName("page") + val url: String, +)