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