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:
Aria Moradi 2021-08-03 18:43:00 +04:30 committed by GitHub
parent 958cc4457c
commit 94915eeb0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 269 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -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

View File

@ -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(),
)

View File

@ -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")
}