Manta Comics: new extension (#9635)
This commit is contained in:
		
							parent
							
								
									7ec5b66189
								
							
						
					
					
						commit
						6a5bad1f42
					
				
							
								
								
									
										2
									
								
								src/en/manta/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/en/manta/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<manifest package="eu.kanade.tachiyomi.extension" />
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/en/manta/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/en/manta/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					apply plugin: 'com.android.application'
 | 
				
			||||||
 | 
					apply plugin: 'kotlin-android'
 | 
				
			||||||
 | 
					apply plugin: 'kotlinx-serialization'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ext {
 | 
				
			||||||
 | 
					    extName = 'Manta Comics'
 | 
				
			||||||
 | 
					    pkgNameSuffix = 'en.manta'
 | 
				
			||||||
 | 
					    extClass = '.MantaComics'
 | 
				
			||||||
 | 
					    extVersionCode = 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apply from: "$rootDir/common.gradle"
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 6.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 10 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/manta/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 17 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/manta/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/manta/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 108 KiB  | 
@ -0,0 +1,127 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.en.manta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat
 | 
				
			||||||
 | 
					import java.util.Locale
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit.MILLISECONDS as MS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private val isoDate by lazy {
 | 
				
			||||||
 | 
					    SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private inline val String?.timestamp: Long
 | 
				
			||||||
 | 
					    get() = this?.substringBefore('.')?.let(isoDate::parse)?.time ?: 0L
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Series<T : Any>(
 | 
				
			||||||
 | 
					    val data: T,
 | 
				
			||||||
 | 
					    val id: Int,
 | 
				
			||||||
 | 
					    val image: Cover,
 | 
				
			||||||
 | 
					    val episodes: List<Episode>? = null
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    override fun toString() = data.toString()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Title(private val title: Name) {
 | 
				
			||||||
 | 
					    override fun toString() = title.toString()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Details(
 | 
				
			||||||
 | 
					    val tags: List<Tag>,
 | 
				
			||||||
 | 
					    val isCompleted: Boolean? = null,
 | 
				
			||||||
 | 
					    private val description: Description,
 | 
				
			||||||
 | 
					    private val creators: List<Creator>
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    val artists by lazy {
 | 
				
			||||||
 | 
					        creators.filter { it.role == "Illustration" }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val authors by lazy {
 | 
				
			||||||
 | 
					        creators.filter { it.role != "Illustration" }.ifEmpty { creators }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun toString() = description.toString()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Episode(
 | 
				
			||||||
 | 
					    val id: Int,
 | 
				
			||||||
 | 
					    val ord: Int,
 | 
				
			||||||
 | 
					    val data: Data?,
 | 
				
			||||||
 | 
					    private val createdAt: String,
 | 
				
			||||||
 | 
					    val cutImages: List<Image>? = null
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    val timestamp: Long
 | 
				
			||||||
 | 
					        get() = createdAt.timestamp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val isLocked: Boolean
 | 
				
			||||||
 | 
					        get() = timeTillFree > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val waitingTime: String
 | 
				
			||||||
 | 
					        get() = when (val days = MS.toDays(timeTillFree)) {
 | 
				
			||||||
 | 
					            0L -> "later today"
 | 
				
			||||||
 | 
					            1L -> "tomorrow"
 | 
				
			||||||
 | 
					            else -> "in $days days"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val timeTillFree by lazy {
 | 
				
			||||||
 | 
					        data?.freeAt.timestamp - System.currentTimeMillis()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun toString() = buildString {
 | 
				
			||||||
 | 
					        append(data?.title ?: "Episode $ord")
 | 
				
			||||||
 | 
					        if (isLocked) append(" \uD83D\uDD12")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Data(
 | 
				
			||||||
 | 
					    val title: String? = null,
 | 
				
			||||||
 | 
					    val freeAt: String? = null
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Creator(
 | 
				
			||||||
 | 
					    private val name: String,
 | 
				
			||||||
 | 
					    val role: String
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    override fun toString() = name
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Description(
 | 
				
			||||||
 | 
					    private val long: String,
 | 
				
			||||||
 | 
					    private val short: String
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    override fun toString() = "$short\n\n$long"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Cover(private val `1280x1840_480`: Image) {
 | 
				
			||||||
 | 
					    override fun toString() = `1280x1840_480`.toString()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Image(private val downloadUrl: String) {
 | 
				
			||||||
 | 
					    override fun toString() = downloadUrl
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Tag(private val name: Name) {
 | 
				
			||||||
 | 
					    override fun toString() = name.toString()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Name(private val en: String) {
 | 
				
			||||||
 | 
					    override fun toString() = en
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class Status(
 | 
				
			||||||
 | 
					    private val description: String,
 | 
				
			||||||
 | 
					    private val message: String
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    override fun toString() = "$description: $message"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.en.manta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.asObservable
 | 
				
			||||||
 | 
					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.decodeFromJsonElement
 | 
				
			||||||
 | 
					import kotlinx.serialization.json.jsonObject
 | 
				
			||||||
 | 
					import okhttp3.Request
 | 
				
			||||||
 | 
					import okhttp3.Response
 | 
				
			||||||
 | 
					import uy.kohesive.injekt.injectLazy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MantaComics : HttpSource() {
 | 
				
			||||||
 | 
					    override val name = "Manta"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val lang = "en"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val baseUrl = "https://manta.net"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val supportsLatest = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val json by injectLazy<Json>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun headersBuilder() = super.headersBuilder()
 | 
				
			||||||
 | 
					        .set("User-Agent", "Manta/167").set("Origin", baseUrl)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun latestUpdatesRequest(page: Int) =
 | 
				
			||||||
 | 
					        GET("$baseUrl/manta/v1/search/series?cat=New", headers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun fetchPopularManga(page: Int) =
 | 
				
			||||||
 | 
					        latestUpdatesRequest(page).fetch(::searchMangaParse)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
 | 
				
			||||||
 | 
					        filters.category.ifEmpty { if (query.isEmpty()) "New" else "" }.let {
 | 
				
			||||||
 | 
					            GET("$baseUrl/manta/v1/search/series?cat=$it&q=$query", headers)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun searchMangaParse(response: Response) =
 | 
				
			||||||
 | 
					        response.parse<List<Series<Title>>>().map {
 | 
				
			||||||
 | 
					            SManga.create().apply {
 | 
				
			||||||
 | 
					                title = it.toString()
 | 
				
			||||||
 | 
					                url = it.id.toString()
 | 
				
			||||||
 | 
					                thumbnail_url = it.image.toString()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }.let { MangasPage(it, false) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
 | 
				
			||||||
 | 
					        searchMangaRequest(page, query, filters).fetch(::searchMangaParse)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Request the actual manga URL for the webview
 | 
				
			||||||
 | 
					    override fun mangaDetailsRequest(manga: SManga) =
 | 
				
			||||||
 | 
					        GET("$baseUrl/series/${manga.url}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun mangaDetailsParse(response: Response) =
 | 
				
			||||||
 | 
					        SManga.create().apply {
 | 
				
			||||||
 | 
					            val data = response.parse<Series<Details>>().data
 | 
				
			||||||
 | 
					            description = data.toString()
 | 
				
			||||||
 | 
					            genre = data.tags.joinToString()
 | 
				
			||||||
 | 
					            artist = data.artists.joinToString()
 | 
				
			||||||
 | 
					            author = data.authors.joinToString()
 | 
				
			||||||
 | 
					            status = when (data.isCompleted) {
 | 
				
			||||||
 | 
					                true -> SManga.COMPLETED
 | 
				
			||||||
 | 
					                else -> SManga.ONGOING
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            initialized = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun fetchMangaDetails(manga: SManga) =
 | 
				
			||||||
 | 
					        chapterListRequest(manga).fetch(::mangaDetailsParse)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chapterListRequest(manga: SManga) =
 | 
				
			||||||
 | 
					        GET("$baseUrl/front/v1/series/${manga.url}", headers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chapterListParse(response: Response) =
 | 
				
			||||||
 | 
					        response.parse<Series<Title>>().episodes!!.map {
 | 
				
			||||||
 | 
					            SChapter.create().apply {
 | 
				
			||||||
 | 
					                name = it.toString()
 | 
				
			||||||
 | 
					                url = it.id.toString()
 | 
				
			||||||
 | 
					                date_upload = it.timestamp
 | 
				
			||||||
 | 
					                chapter_number = it.ord.toFloat()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun fetchChapterList(manga: SManga) =
 | 
				
			||||||
 | 
					        chapterListRequest(manga).fetch(::chapterListParse)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun pageListRequest(chapter: SChapter) =
 | 
				
			||||||
 | 
					        GET("$baseUrl/front/v1/episodes/${chapter.url}", headers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun pageListParse(response: Response) =
 | 
				
			||||||
 | 
					        response.parse<Episode>().run {
 | 
				
			||||||
 | 
					            if (!isLocked) return@run cutImages!!
 | 
				
			||||||
 | 
					            error("This episode will be available $waitingTime.")
 | 
				
			||||||
 | 
					        }.mapIndexed { idx, img -> Page(idx, "", img.toString()) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun fetchPageList(chapter: SChapter) =
 | 
				
			||||||
 | 
					        pageListRequest(chapter).fetch(::pageListParse)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getFilterList() = FilterList(Category())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun latestUpdatesParse(response: Response) =
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException("Not used")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun popularMangaRequest(page: Int) =
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException("Not used")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun popularMangaParse(response: Response) =
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException("Not used")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun imageUrlParse(response: Response) =
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException("Not used")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun <R> Request.fetch(parse: (Response) -> R) =
 | 
				
			||||||
 | 
					        client.newCall(this).asObservable().map { res ->
 | 
				
			||||||
 | 
					            if (res.isSuccessful) return@map parse(res)
 | 
				
			||||||
 | 
					            error(res.parse<Status>("status").toString())
 | 
				
			||||||
 | 
					        }!!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private inline fun <reified T> Response.parse(key: String = "data") =
 | 
				
			||||||
 | 
					        json.decodeFromJsonElement<T>(
 | 
				
			||||||
 | 
					            json.parseToJsonElement(body!!.string()).jsonObject[key]!!
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.en.manta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.Filter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private val categories = arrayOf(
 | 
				
			||||||
 | 
					    "",
 | 
				
			||||||
 | 
					    "New",
 | 
				
			||||||
 | 
					    "Exclusive",
 | 
				
			||||||
 | 
					    "Completed",
 | 
				
			||||||
 | 
					    "Romance",
 | 
				
			||||||
 | 
					    "BL / GL",
 | 
				
			||||||
 | 
					    "Drama",
 | 
				
			||||||
 | 
					    "Fantasy",
 | 
				
			||||||
 | 
					    "Thriller",
 | 
				
			||||||
 | 
					    "Slice of life"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Category(
 | 
				
			||||||
 | 
					    values: Array<String> = categories
 | 
				
			||||||
 | 
					) : Filter.Select<String>("Category", values)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					inline val List<Filter<*>>.category: String
 | 
				
			||||||
 | 
					    get() = (firstOrNull() as? Category)?.run { values[state] } ?: ""
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user