add Tachidesk extension (#8361)
* add Tachidesk extension * rewrite with kotlinx-serialization * apply suggestions by @ObserverOfTime * linter must lint * map Tachidesk MangaStatus to SManga constants * raise exception when baseUrl is empty * defensive programming * print Exception to Log.e instead * cesco asked for this...
This commit is contained in:
parent
958cc4457c
commit
94915eeb0f
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,14 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Tachidesk'
|
||||
pkgNameSuffix = 'all.tachidesk'
|
||||
extClass = '.Tachidesk'
|
||||
extVersionCode = 1
|
||||
libVersion = '1.2'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -0,0 +1,73 @@
|
|||
package eu.kanade.tachiyomi.extension.all.tachidesk
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SourceDataClass(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val lang: String?,
|
||||
val iconUrl: String?,
|
||||
val supportsLatest: Boolean?,
|
||||
val isConfigurable: Boolean?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaDataClass(
|
||||
val id: Int,
|
||||
val sourceId: String,
|
||||
|
||||
val url: String,
|
||||
val title: String,
|
||||
val thumbnailUrl: String? = null,
|
||||
|
||||
val initialized: Boolean = false,
|
||||
|
||||
val artist: String? = null,
|
||||
val author: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: String? = null,
|
||||
val status: String = "UNKNOWN",
|
||||
val inLibrary: Boolean = false,
|
||||
val source: SourceDataClass? = null,
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
|
||||
val freshData: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterDataClass(
|
||||
val url: String,
|
||||
val name: String,
|
||||
val uploadDate: Long,
|
||||
val chapterNumber: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
|
||||
/** chapter is read */
|
||||
val read: Boolean,
|
||||
|
||||
/** chapter is bookmarked */
|
||||
val bookmarked: Boolean,
|
||||
|
||||
/** last read page, zero means not read/no data */
|
||||
val lastPageRead: Int,
|
||||
|
||||
/** last read page, zero means not read/no data */
|
||||
val lastReadAt: Long,
|
||||
|
||||
/** this chapter's index, starts with 1 */
|
||||
val index: Int,
|
||||
|
||||
/** is chapter downloaded */
|
||||
val downloaded: Boolean,
|
||||
|
||||
/** used to construct pages in the front-end */
|
||||
val pageCount: Int = -1,
|
||||
|
||||
/** total chapter count, used to calculate if there's a next and prev chapter */
|
||||
val chapterCount: Int? = null,
|
||||
|
||||
/** used to store client specific values */
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
)
|
|
@ -0,0 +1,180 @@
|
|||
package eu.kanade.tachiyomi.extension.all.tachidesk
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.lang.RuntimeException
|
||||
|
||||
class Tachidesk : ConfigurableSource, HttpSource() {
|
||||
override val name = "Tachidesk"
|
||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||
override val lang = "en"
|
||||
override val supportsLatest = false
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// ------------- Popular Manga -------------
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$checkedBaseUrl/api/v1/library")
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage =
|
||||
MangasPage(
|
||||
json.decodeFromString<List<MangaDataClass>>(response.body!!.string()).map {
|
||||
it.toSManga()
|
||||
},
|
||||
false
|
||||
)
|
||||
// ------------- Manga Details -------------
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) =
|
||||
GET("$checkedBaseUrl/api/v1/manga/${manga.url}/?onlineFetch=true")
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga =
|
||||
json.decodeFromString<MangaDataClass>(response.body!!.string()).let { it.toSManga() }
|
||||
|
||||
// ------------- Chapter -------------
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request =
|
||||
GET("$checkedBaseUrl/api/v1/manga/${manga.url}/chapters?onlineFetch=true", headers)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> =
|
||||
json.decodeFromString<List<ChapterDataClass>>(response.body!!.string()).map {
|
||||
it.toSChapter()
|
||||
}
|
||||
|
||||
// ------------- Page List -------------
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
pageListParse(response, chapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val mangaId = chapter.url.split(" ").first()
|
||||
val chapterIndex = chapter.url.split(" ").last()
|
||||
|
||||
return GET("$checkedBaseUrl/api/v1/manga/$mangaId/chapter/$chapterIndex/?onlineFetch=True", headers)
|
||||
}
|
||||
|
||||
fun pageListParse(response: Response, sChapter: SChapter): List<Page> {
|
||||
val mangaId = sChapter.url.split(" ").first()
|
||||
val chapterIndex = sChapter.url.split(" ").last()
|
||||
|
||||
val chapter = json.decodeFromString<ChapterDataClass>(response.body!!.string())
|
||||
|
||||
return List(chapter.pageCount) {
|
||||
Page(it + 1, "", "$checkedBaseUrl/api/v1/manga/$mangaId/chapter/$chapterIndex/page/$it/")
|
||||
}
|
||||
}
|
||||
|
||||
// ------------- Preferences -------------
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
screen.addPreference(screen.editTextPreference(ADDRESS_TITLE, ADDRESS_DEFAULT, baseUrl))
|
||||
}
|
||||
|
||||
/** boilerplate for [EditTextPreference] */
|
||||
private fun PreferenceScreen.editTextPreference(title: String, default: String, value: String, isPassword: Boolean = false): EditTextPreference {
|
||||
return EditTextPreference(context).apply {
|
||||
key = title
|
||||
this.title = title
|
||||
summary = value
|
||||
this.setDefaultValue(default)
|
||||
dialogTitle = title
|
||||
|
||||
if (isPassword) {
|
||||
setOnBindEditTextListener {
|
||||
it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val res = preferences.edit().putString(title, newValue as String).commit()
|
||||
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
res
|
||||
} catch (e: Exception) {
|
||||
Log.e("Tachidesk", "Exception while setting text preference", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
|
||||
|
||||
companion object {
|
||||
private const val ADDRESS_TITLE = "Address"
|
||||
private const val ADDRESS_DEFAULT = ""
|
||||
}
|
||||
|
||||
// ------------- Not Used -------------
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = throw Exception("Not used")
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw Exception("Not used")
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw Exception("Not used")
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> = throw Exception("Not used")
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw Exception("Not used")
|
||||
|
||||
// ------------- Util -------------
|
||||
|
||||
private fun MangaDataClass.toSManga() = SManga.create().also {
|
||||
it.title = title
|
||||
it.url = id.toString()
|
||||
it.thumbnail_url = "$baseUrl$thumbnailUrl"
|
||||
it.artist = artist
|
||||
it.author = author
|
||||
it.description = description
|
||||
it.status = when (status) {
|
||||
"ONGOING" -> SManga.ONGOING
|
||||
"COMPLETED" -> SManga.COMPLETED
|
||||
"LICENSED" -> SManga.LICENSED
|
||||
else -> SManga.UNKNOWN // covers "UNKNOWN" and other Impossible cases
|
||||
}
|
||||
}
|
||||
|
||||
private fun ChapterDataClass.toSChapter() = SChapter.create().also {
|
||||
it.url = "$mangaId $index"
|
||||
it.name = name
|
||||
it.date_upload = uploadDate
|
||||
it.scanlator = scanlator
|
||||
}
|
||||
|
||||
private val checkedBaseUrl: String
|
||||
get(): String = if (baseUrl.isNotEmpty()) baseUrl
|
||||
else throw RuntimeException("Set Tachidesk server url in extension settings")
|
||||
}
|
Loading…
Reference in New Issue