izneo: new extension (#9664)
This commit is contained in:
parent
cd300695b4
commit
d0e8f185ab
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'izneo (webtoons)'
|
||||
pkgNameSuffix = 'all.izneo'
|
||||
extClass = '.IzneoFactory'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
|
@ -0,0 +1,38 @@
|
|||
package eu.kanade.tachiyomi.extension.all.izneo
|
||||
|
||||
import android.util.Base64
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object ImageInterceptor : Interceptor {
|
||||
private val mediaType = "image/jpeg".toMediaType()
|
||||
|
||||
private inline val AES: Cipher
|
||||
get() = Cipher.getInstance("AES/CBC/PKCS7Padding")
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val url = chain.request().url
|
||||
val key = url.queryParameter("key")
|
||||
?: return chain.proceed(chain.request())
|
||||
return chain.proceed(
|
||||
chain.request().newBuilder().url(
|
||||
url.newBuilder()
|
||||
.removeAllQueryParameters("key")
|
||||
.removeAllQueryParameters("iv")
|
||||
.build()
|
||||
).build()
|
||||
).decode(key.atob(), url.queryParameter("iv")!!.atob())
|
||||
}
|
||||
|
||||
private fun Response.decode(key: ByteArray, iv: ByteArray) = AES.let {
|
||||
it.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||
newBuilder().body(it.doFinal(body!!.bytes()).toResponseBody(mediaType)).build()
|
||||
}
|
||||
|
||||
private fun String.atob() = Base64.decode(this, Base64.URL_SAFE)
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package eu.kanade.tachiyomi.extension.all.izneo
|
||||
|
||||
import android.app.Application
|
||||
import android.text.InputType.TYPE_CLASS_TEXT
|
||||
import android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
import android.util.Base64
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Izneo(override val lang: String) : ConfigurableSource, HttpSource() {
|
||||
override val name = "izneo"
|
||||
|
||||
override val baseUrl = "$ORIGIN/$lang/webtoon"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.addInterceptor(ImageInterceptor).build()
|
||||
|
||||
private val apiUrl = "$ORIGIN/$lang/api/catalog/detail/webtoon"
|
||||
|
||||
private val json by lazy { Injekt.get<Json>() }
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||
}
|
||||
|
||||
private inline val username: String
|
||||
get() = preferences.getString("username", "")!!
|
||||
|
||||
private inline val password: String
|
||||
get() = preferences.getString("password", "")!!
|
||||
|
||||
private val apiHeaders by lazy {
|
||||
headers.newBuilder().apply {
|
||||
set("X-Requested-With", "XMLHttpRequest")
|
||||
if (username.isNotEmpty() && password.isNotEmpty()) {
|
||||
set("Authorization", "Basic " + "$username:$password".btoa())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private var seriesCount = 0
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Cookie", "lang=$lang;").set("Referer", baseUrl)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$apiUrl/new?offset=${page - 1}&order=1&abo=0", apiHeaders)
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$apiUrl/topSales?offset=${page - 1}&order=0&abo=0", apiHeaders)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
GET("$apiUrl/free?offset=${page - 1}&order=3&abo=0", apiHeaders)
|
||||
|
||||
// Request the real URL for the webview
|
||||
override fun mangaDetailsRequest(manga: SManga) =
|
||||
GET(ORIGIN + manga.url, headers)
|
||||
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
GET(manga.apiUrl + "/volumes/old/0/500", apiHeaders)
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) =
|
||||
GET(ORIGIN + "/book/" + chapter.url, apiHeaders)
|
||||
|
||||
override fun imageRequest(page: Page) =
|
||||
GET(ORIGIN + "/book/" + page.imageUrl!!, apiHeaders)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
response.parse().run {
|
||||
val count = get("series_count")!!.jsonPrimitive.int
|
||||
val series = get("series")!!.jsonObject.values.flatMap {
|
||||
json.decodeFromJsonElement<List<Series>>(it)
|
||||
}.also { seriesCount += it.size }
|
||||
if (count == seriesCount) seriesCount = 0
|
||||
series.map {
|
||||
SManga.create().apply {
|
||||
url = it.url
|
||||
title = it.name
|
||||
genre = it.genres
|
||||
author = it.authors.joinToString()
|
||||
artist = it.authors.joinToString()
|
||||
thumbnail_url = "$ORIGIN/$lang${it.cover}"
|
||||
description = it.toString()
|
||||
}
|
||||
}.let { MangasPage(it, seriesCount != 0) }
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) =
|
||||
latestUpdatesParse(response)
|
||||
|
||||
override fun searchMangaParse(response: Response) =
|
||||
latestUpdatesParse(response)
|
||||
|
||||
override fun chapterListParse(response: Response) =
|
||||
response.parse()["albums"]!!.jsonArray.map {
|
||||
val album = json.decodeFromJsonElement<Album>(it)
|
||||
SChapter.create().apply {
|
||||
url = album.id
|
||||
name = album.toString()
|
||||
date_upload = album.timestamp
|
||||
chapter_number = album.number
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response) =
|
||||
response.parse()["data"]!!.jsonObject.run {
|
||||
val id = get("id")!!.jsonPrimitive.content
|
||||
get("pages")!!.jsonArray.map {
|
||||
val page = json.decodeFromJsonElement<AlbumPage>(it)
|
||||
Page(page.albumPageNumber, "", id + page.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
super.fetchSearchManga(page, query, filters).map { mp ->
|
||||
mp.copy(mp.mangas.filter { it.title.contains(query, true) })
|
||||
}!!
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga) =
|
||||
rx.Observable.just(manga.apply { initialized = true })!!
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = "username"
|
||||
title = "Username"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putString(key, newValue as String).commit()
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = "password"
|
||||
title = "Password"
|
||||
|
||||
setOnBindEditTextListener {
|
||||
it.inputType = TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putString(key, newValue as String).commit()
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
}
|
||||
|
||||
private inline val SManga.apiUrl: String
|
||||
get() = "$ORIGIN/$lang/api/web/serie/" + url.substringAfterLast('-')
|
||||
|
||||
private inline val Album.timestamp: Long
|
||||
get() = dateFormat.parse(publicationDate)?.time ?: 0L
|
||||
|
||||
private fun String.btoa() = Base64.encode(toByteArray(), Base64.DEFAULT)
|
||||
|
||||
private fun Response.parse() =
|
||||
json.parseToJsonElement(body!!.string()).apply {
|
||||
if (jsonObject["status"]?.jsonPrimitive?.content == "error") {
|
||||
when (jsonObject["code"]?.jsonPrimitive?.content) {
|
||||
"4" -> throw Error("You are not authorized to view this")
|
||||
else -> throw Error(jsonObject["data"]?.jsonPrimitive?.content)
|
||||
}
|
||||
}
|
||||
}.jsonObject
|
||||
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
companion object {
|
||||
private const val ORIGIN = "https://izneo.com"
|
||||
|
||||
private val dateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package eu.kanade.tachiyomi.extension.all.izneo
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Series(
|
||||
val name: String,
|
||||
val url: String,
|
||||
private val id: String,
|
||||
private val version: Int,
|
||||
private val synopsis: String,
|
||||
private val gender: String,
|
||||
private val target: Target,
|
||||
val authors: List<Author>
|
||||
) {
|
||||
val genres: String
|
||||
get() = "$gender, $target"
|
||||
|
||||
val cover: String
|
||||
get() = "/images/serie/$id.jpg?v=$version"
|
||||
|
||||
override fun toString() =
|
||||
synopsis.replace("\n ", " ").replace("<br />", "")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Target(private val name: String) {
|
||||
override fun toString() = name
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Author(private val nickname: String) {
|
||||
override fun toString() = nickname
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Album(
|
||||
val id: String,
|
||||
private val title: String,
|
||||
private val volume: String,
|
||||
val publicationDate: String,
|
||||
private val fullAvailable: Boolean,
|
||||
private val inUserLibrary: Boolean,
|
||||
private val inUserSubscription: Boolean
|
||||
) {
|
||||
val number: Float
|
||||
get() = volume.toFloat()
|
||||
|
||||
private inline val isLocked: Boolean
|
||||
get() = !fullAvailable && !(inUserLibrary || inUserSubscription)
|
||||
|
||||
override fun toString() =
|
||||
title + if (isLocked) " \uD83D\uDD12" else ""
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AlbumPage(
|
||||
val albumPageNumber: Int,
|
||||
private val key: String,
|
||||
private val iv: String
|
||||
) {
|
||||
override fun toString() =
|
||||
"/$albumPageNumber?type=full&key=${key.urlSafe}&iv=${iv.urlSafe}"
|
||||
|
||||
private inline val String.urlSafe: String
|
||||
get() = replace('+', '-').replace('/', '_')
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package eu.kanade.tachiyomi.extension.all.izneo
|
||||
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class IzneoFactory : SourceFactory {
|
||||
override fun createSources() = listOf(
|
||||
Izneo("en"),
|
||||
Izneo("fr"),
|
||||
// Izneo("de"),
|
||||
// Izneo("nl"),
|
||||
// Izneo("it"),
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue