[RU]Senkuro and Senkognito new source (#18552)

* [RU]Senkuro and Senkognito  new source

* Senkognito isNsfw

* pageListParse

* offset pages search

* more details

* scanlator and urlS

* dynamic filters list

* filters

* tags

* altName

* Senkuro eternal built-in exclude 18+ filter

* unrealizable manifest

* autoclear android studio

* clear spaces

* author and artist

* hard getFilterList comments

* not null description

* API_URL

* more senkuroExclude

* no load senkuroExcludeGenres

* clear spaces 2

* autoclear android studio 2

* autoclear android studio 2,5

* fix getFilterList format

* correctly reset selected filters

* autoclear android studio 3

* fix indentation QUERY

* hide only dynamic filters

* typo

* minimizing the query space

* icon "pattern"

* zoom icon

* fix reset selected filters j2k

* import optim

* personalized domain optional

* Not yet implemented

* autoclean space

* multi scanlator

* summary domainRedirect

* WebView

* typo

* Update multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/senkuro/SenkuroQueries.kt

* Update multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/senkuro/SenkuroQueries.kt

* Update multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/senkuro/SenkuroQueries.kt

* Update multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/senkuro/SenkuroQueries.kt

* Update multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/senkuro/SenkuroQueries.kt

---------

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>
This commit is contained in:
Eshlender 2023-10-21 19:40:44 +05:00 committed by GitHub
parent 4cd74249b7
commit ce11d6f168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 881 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 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: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.extension.ru.senkognito
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import eu.kanade.tachiyomi.multisrc.senkuro.Senkuro
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class Senkognito : Senkuro("Senkognito", "https://senkognito.com", "ru") {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private var domain: String? = if (preferences.getBoolean(redirect_PREF, true)) "https://senkognito.com" else "https://senkuro.com"
override val baseUrl: String = domain.toString()
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
val domainRedirect = androidx.preference.CheckBoxPreference(screen.context).apply {
key = redirect_PREF
title = "Домен Senkognito"
summary = "Отключите если домен Senkognito недоступен в браузере/WebView."
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
val warning = "Для смены домена необходимо перезапустить приложение с полной остановкой."
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
true
}
}
screen.addPreference(domainRedirect)
}
companion object {
private const val redirect_PREF = "domainRedirect"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.extension.ru.senkuro
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.multisrc.senkuro.Senkuro
class Senkuro : Senkuro("Senkuro", "https://senkuro.com", "ru") {
override fun setupPreferenceScreen(screen: PreferenceScreen) {}
}

View File

@ -0,0 +1,128 @@
package eu.kanade.tachiyomi.multisrc.senkuro
import kotlinx.serialization.Serializable
@Serializable
data class PageWrapperDto<T>(
val data: T,
)
// Library Container
@Serializable
data class MangaTachiyomiSearchDto<T>(
val mangaTachiyomiSearch: MangasDto<T>,
) {
@Serializable
data class MangasDto<T>(
val mangas: List<T>,
)
}
// Manga Details
@Serializable
data class SubInfoDto(
val mangaTachiyomiInfo: MangaTachiyomiInfoDto,
)
@Serializable
data class MangaTachiyomiInfoDto(
val id: String,
val slug: String,
val cover: SubImgDto? = null,
val status: String? = null,
val type: String? = null,
val rating: String? = null,
val formats: List<String>? = null,
val genres: List<TagsDto>? = null,
val tags: List<TagsDto>? = null,
val titles: List<TitleDto>,
val alternativeNames: List<TitleDto>? = null,
val localizations: List<LocalizationsDto>? = null,
val mainStaff: List<MainStaffDto>? = null,
) {
@Serializable
data class SubImgDto(
val original: ImgDto,
) {
@Serializable
data class ImgDto(
val url: String? = null,
)
}
@Serializable
data class TagsDto(
val slug: String,
val titles: List<TitleDto>,
)
@Serializable
data class TitleDto(
val lang: String,
val content: String,
)
@Serializable
data class LocalizationsDto(
val lang: String,
val description: String,
)
@Serializable
data class MainStaffDto(
val roles: List<String>,
val person: PersonDto,
) {
@Serializable
data class PersonDto(
val name: String,
)
}
}
// Chapters
@Serializable
data class MangaTachiyomiChaptersDto(
val mangaTachiyomiChapters: ChaptersMessage,
) {
@Serializable
data class ChaptersMessage(
val message: String? = null,
val chapters: List<BookDto>,
val teams: List<TeamsDto>,
) {
@Serializable
data class BookDto(
val id: String,
val slug: String,
val branchId: String,
val name: String? = null,
val teamIds: List<String>,
val number: String,
val volume: String,
val updatedAt: String,
)
@Serializable
data class TeamsDto(
val id: String,
val slug: String,
val name: String,
)
}
}
// Chapter Pages
@Serializable
data class MangaTachiyomiChapterPages(
val mangaTachiyomiChapterPages: ChaptersPages,
) {
@Serializable
data class ChaptersPages(
val pages: List<UrlDto>,
) {
@Serializable
data class UrlDto(
val url: String,
)
}
}

View File

@ -0,0 +1,440 @@
package eu.kanade.tachiyomi.multisrc.senkuro
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
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.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
abstract class Senkuro(
override val name: String,
override val baseUrl: String,
final override val lang: String,
) : ConfigurableSource, HttpSource() {
override val supportsLatest = false
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Tachiyomi (+https://github.com/tachiyomiorg/tachiyomi)")
.add("Content-Type", "application/json")
override val client: OkHttpClient =
network.client.newBuilder()
.rateLimit(5)
.build()
private inline fun <reified T : Any> T.toJsonRequestBody(): RequestBody =
json.encodeToString(this)
.toRequestBody(JSON_MEDIA_TYPE)
// Popular
override fun popularMangaRequest(page: Int): Request {
val requestBody = GraphQL(
SEARCH_QUERY,
SearchVariables(
offset = offsetCount * (page - 1),
genre = SearchVariables.FiltersDto(
// Senkuro eternal built-in exclude 18+ filter
exclude = if (name == "Senkuro") { senkuroExcludeGenres } else { listOf() },
),
),
).toJsonRequestBody()
fetchTachiyomiSearchFilters(page)
return POST(API_URL, headers, requestBody)
}
override fun popularMangaParse(response: Response) = searchMangaParse(response)
// Latest
override fun latestUpdatesRequest(page: Int): Request = throw NotImplementedError("Unused")
override fun latestUpdatesParse(response: Response): MangasPage = throw NotImplementedError("Unused")
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
fetchTachiyomiSearchFilters(page) // reset filters before sending searchMangaRequest
return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val includeGenres = mutableListOf<String>()
val excludeGenres = mutableListOf<String>()
val includeTags = mutableListOf<String>()
val excludeTags = mutableListOf<String>()
val includeTypes = mutableListOf<String>()
val excludeTypes = mutableListOf<String>()
val includeFormats = mutableListOf<String>()
val excludeFormats = mutableListOf<String>()
val includeStatus = mutableListOf<String>()
val excludeStatus = mutableListOf<String>()
val includeTStatus = mutableListOf<String>()
val excludeTStatus = mutableListOf<String>()
val includeAges = mutableListOf<String>()
val excludeAges = mutableListOf<String>()
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is GenreList -> filter.state.forEach { genre ->
if (genre.state != Filter.TriState.STATE_IGNORE) {
if (genre.isIncluded()) includeGenres.add(genre.slug) else excludeGenres.add(genre.slug)
}
}
is TagList -> filter.state.forEach { tag ->
if (tag.state != Filter.TriState.STATE_IGNORE) {
if (tag.isIncluded()) includeTags.add(tag.slug) else excludeTags.add(tag.slug)
}
}
is TypeList -> filter.state.forEach { type ->
if (type.state != Filter.TriState.STATE_IGNORE) {
if (type.isIncluded()) includeTypes.add(type.slug) else excludeTypes.add(type.slug)
}
}
is FormatList -> filter.state.forEach { format ->
if (format.state != Filter.TriState.STATE_IGNORE) {
if (format.isIncluded()) includeFormats.add(format.slug) else excludeFormats.add(format.slug)
}
}
is StatList -> filter.state.forEach { stat ->
if (stat.state != Filter.TriState.STATE_IGNORE) {
if (stat.isIncluded()) includeStatus.add(stat.slug) else excludeStatus.add(stat.slug)
}
}
is StatTranslateList -> filter.state.forEach { tstat ->
if (tstat.state != Filter.TriState.STATE_IGNORE) {
if (tstat.isIncluded()) includeTStatus.add(tstat.slug) else excludeTStatus.add(tstat.slug)
}
}
is AgeList -> filter.state.forEach { age ->
if (age.state != Filter.TriState.STATE_IGNORE) {
if (age.isIncluded()) includeAges.add(age.slug) else excludeAges.add(age.slug)
}
}
else -> {}
}
}
// Senkuro eternal built-in exclude 18+ filter
if (name == "Senkuro") {
excludeGenres.addAll(senkuroExcludeGenres)
}
val requestBody = GraphQL(
SEARCH_QUERY,
SearchVariables(
query = query, offset = offsetCount * (page - 1),
genre = SearchVariables.FiltersDto(
includeGenres,
excludeGenres,
),
tag = SearchVariables.FiltersDto(
includeTags,
excludeTags,
),
type = SearchVariables.FiltersDto(
includeTypes,
excludeTypes,
),
format = SearchVariables.FiltersDto(
includeFormats,
excludeFormats,
),
status = SearchVariables.FiltersDto(
includeStatus,
excludeStatus,
),
translationStatus = SearchVariables.FiltersDto(
includeTStatus,
excludeTStatus,
),
rating = SearchVariables.FiltersDto(
includeAges,
excludeAges,
),
),
).toJsonRequestBody()
return POST(API_URL, headers, requestBody)
}
override fun searchMangaParse(response: Response): MangasPage {
val page = json.decodeFromString<PageWrapperDto<MangaTachiyomiSearchDto<MangaTachiyomiInfoDto>>>(response.body.string())
val mangasList = page.data.mangaTachiyomiSearch.mangas.map {
it.toSManga()
}
return MangasPage(mangasList, mangasList.isNotEmpty())
}
// Details
private fun parseStatus(status: String?): Int {
return when (status) {
"FINISHED" -> SManga.COMPLETED
"ONGOING" -> SManga.ONGOING
"HIATUS" -> SManga.ON_HIATUS
"ANNOUNCE" -> SManga.ONGOING
"CANCELLED" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
private fun MangaTachiyomiInfoDto.toSManga(): SManga {
val o = this
return SManga.create().apply {
title = titles.find { it.lang == "RU" }?.content ?: titles.find { it.lang == "EN" }?.content ?: titles[0].content
url = "$id,,$slug" // mangaId[0],,mangaSlug[1]
thumbnail_url = cover?.original?.url
var altName = alternativeNames?.joinToString(" / ") { it.content }
if (!altName.isNullOrEmpty()) {
altName = "Альтернативные названия:\n$altName\n\n"
}
author = mainStaff?.filter { it.roles.contains("STORY") }?.joinToString(", ") { it.person.name }
artist = mainStaff?.filter { it.roles.contains("ART") }?.joinToString(", ") { it.person.name }
description = altName + localizations?.find { it.lang == "RU" }?.description.orEmpty()
status = parseStatus(o.status)
genre = (
getTypeList().find { it.slug == type }?.name + ", " +
getAgeList().find { it.slug == rating }?.name + ", " +
getFormatList().filter { formats.orEmpty().contains(it.slug) }.joinToString { it.name } + ", " +
genres?.joinToString { git -> git.titles.find { it.lang == "RU" }!!.content } + ", " +
tags?.joinToString { tit -> tit.titles.find { it.lang == "RU" }!!.content }
).split(", ").filter { it.isNotEmpty() }.joinToString { it.trim().capitalize() }
}
}
override fun mangaDetailsRequest(manga: SManga): Request {
val requestBody = GraphQL(
DETAILS_QUERY,
FetchDetailsVariables(mangaId = manga.url.split(",,")[0]),
).toJsonRequestBody()
return POST(API_URL, headers, requestBody)
}
override fun mangaDetailsParse(response: Response): SManga {
val series = json.decodeFromString<PageWrapperDto<SubInfoDto>>(response.body.string())
return series.data.mangaTachiyomiInfo.toSManga()
}
override fun getMangaUrl(manga: SManga) = baseUrl + "/manga/" + manga.url.split(",,")[1]
// Chapters
private val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S", Locale.ROOT) }
private fun parseDate(date: String?): Long {
date ?: return 0L
return try {
simpleDateFormat.parse(date)!!.time
} catch (_: Exception) {
Date().time
}
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response ->
chapterListParse(response, manga)
}
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("chapterListParse(response: Response, manga: SManga)")
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
val chaptersList = json.decodeFromString<PageWrapperDto<MangaTachiyomiChaptersDto>>(response.body.string())
val teamsList = chaptersList.data.mangaTachiyomiChapters.teams
return chaptersList.data.mangaTachiyomiChapters.chapters.map { chapter ->
SChapter.create().apply {
chapter_number = chapter.number.toFloatOrNull() ?: -2F
name = "${chapter.volume}. Глава ${chapter.number} " + (chapter.name ?: "")
url = "${manga.url},,${chapter.id},,${chapter.slug}" // mangaId[0],,mangaSlug[1],,chapterId[2],,chapterSlug[3]
date_upload = parseDate(chapter.updatedAt)
scanlator = teamsList.filter { chapter.teamIds.contains(it.id) }.joinToString { it.name }
}
}
}
override fun chapterListRequest(manga: SManga): Request {
val requestBody = GraphQL(
CHAPTERS_QUERY,
FetchDetailsVariables(mangaId = manga.url.split(",,")[0]),
).toJsonRequestBody()
return POST(API_URL, headers, requestBody)
}
// Pages
override fun pageListRequest(chapter: SChapter): Request {
val mangaChapterId = chapter.url.split(",,")
val requestBody = GraphQL(
CHAPTERS_PAGES_QUERY,
FetchChapterPagesVariables(mangaId = mangaChapterId[0], chapterId = mangaChapterId[2]),
).toJsonRequestBody()
return POST(API_URL, headers, requestBody)
}
override fun getChapterUrl(chapter: SChapter): String {
val mangaChapterSlug = chapter.url.split(",,")
return baseUrl + "/manga/" + mangaChapterSlug[1] + "/chapters/" + mangaChapterSlug[3]
}
override fun pageListParse(response: Response): List<Page> {
val imageList = json.decodeFromString<PageWrapperDto<MangaTachiyomiChapterPages>>(response.body.string())
return imageList.data.mangaTachiyomiChapterPages.pages.mapIndexed { index, page ->
Page(index, "", page.url)
}
}
override fun imageUrlRequest(page: Page): Request = throw NotImplementedError("Unused")
override fun imageUrlParse(response: Response): String = throw NotImplementedError("Unused")
override fun fetchImageUrl(page: Page): Observable<String> {
return Observable.just(page.url)
}
// Filters
// Filters are fetched immediately once an extension loads
// We're only able to get filters after a loading the manga directory, and resetting
// the filters is the only thing that seems to reinflate the view
private fun fetchTachiyomiSearchFilters(pageRequest: Int) {
// The function must be used in PopularMangaRequest and fetchSearchManga to correctly/guaranteed reset the selected filters!
if (pageRequest == 1) {
val responseBody = client.newCall(
POST(
API_URL,
headers,
GraphQL(
FILTERS_QUERY,
SearchVariables(),
).toJsonRequestBody(),
),
).execute().body.string()
val filterDto =
json.decodeFromString<PageWrapperDto<MangaTachiyomiSearchFilters>>(responseBody).data.mangaTachiyomiSearchFilters
genresList =
filterDto.genres.filterNot { name == "Senkuro" && senkuroExcludeGenres.contains(it.slug) }
.map { genre ->
FilterersTri(
genre.titles.find { it.lang == "RU" }!!.content.capitalize(),
genre.slug,
)
}
tagsList = filterDto.tags.map { tag ->
FilterersTri(
tag.titles.find { it.lang == "RU" }!!.content.capitalize(),
tag.slug,
)
}
}
}
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>()
filters += if (genresList.isEmpty() or tagsList.isEmpty()) {
listOf(
Filter.Separator(),
Filter.Header("Нажмите «Сбросить», чтобы загрузить все фильтры"),
Filter.Separator(),
)
} else {
listOf(
GenreList(genresList),
TagList(tagsList),
)
}
filters += listOf(
TypeList(getTypeList()),
FormatList(getFormatList()),
StatList(getStatList()),
StatTranslateList(getStatTranslateList()),
AgeList(getAgeList()),
)
return FilterList(filters)
}
private class FilterersTri(name: String, val slug: String) : Filter.TriState(name)
private class GenreList(genres: List<FilterersTri>) : Filter.Group<FilterersTri>("Жанры", genres)
private class TagList(tags: List<FilterersTri>) : Filter.Group<FilterersTri>("Тэги", tags)
private class TypeList(types: List<FilterersTri>) : Filter.Group<FilterersTri>("Тип", types)
private class FormatList(formats: List<FilterersTri>) : Filter.Group<FilterersTri>("Формат", formats)
private class StatList(status: List<FilterersTri>) : Filter.Group<FilterersTri>("Статус", status)
private class StatTranslateList(tstatus: List<FilterersTri>) : Filter.Group<FilterersTri>("Статус перевода", tstatus)
private class AgeList(ages: List<FilterersTri>) : Filter.Group<FilterersTri>("Возрастное ограничение", ages)
private var genresList: List<FilterersTri> = listOf()
private var tagsList: List<FilterersTri> = listOf()
private fun getTypeList() = listOf(
FilterersTri("Манга", "MANGA"),
FilterersTri("Манхва", "MANHWA"),
FilterersTri("Маньхуа", "MANHUA"),
FilterersTri("Комикс", "COMICS"),
FilterersTri("OEL Манга", "OEL_MANGA"),
FilterersTri("РуМанга", "RU_MANGA"),
)
private fun getStatList() = listOf(
FilterersTri("Анонс", "ANNOUNCE"),
FilterersTri("Онгоинг", "ONGOING"),
FilterersTri("Выпущено", "FINISHED"),
FilterersTri("Приостановлено", "HIATUS"),
FilterersTri("Отменено", "CANCELLED"),
)
private fun getStatTranslateList() = listOf(
FilterersTri("Переводится", "IN_PROGRESS"),
FilterersTri("Завершён", "FINISHED"),
FilterersTri("Заморожен", "FROZEN"),
FilterersTri("Заброшен", "ABANDONED"),
)
private fun getAgeList() = listOf(
FilterersTri("0+", "GENERAL"),
FilterersTri("12+", "SENSITIVE"),
FilterersTri("16+", "QUESTIONABLE"),
FilterersTri("18+", "EXPLICIT"),
)
private fun getFormatList() = listOf(
FilterersTri("Сборник", "DIGEST"),
FilterersTri("Додзинси", "DOUJINSHI"),
FilterersTri("В цвете", "IN_COLOR"),
FilterersTri("Сингл", "SINGLE"),
FilterersTri("Веб", "WEB"),
FilterersTri("Вебтун", "WEBTOON"),
FilterersTri("Ёнкома", "YONKOMA"),
FilterersTri("Short", "SHORT"),
)
companion object {
private const val offsetCount = 20
private const val API_URL = "https://api.senkuro.com/graphql"
private val senkuroExcludeGenres = listOf("hentai", "yaoi", "yuri", "shoujo_ai", "shounen_ai")
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
}
private val json: Json by injectLazy()
}

View File

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.multisrc.senkuro
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class SenkuroGenerator : ThemeSourceGenerator {
override val themePkg = "senkuro"
override val themeClass = "Senkuro"
override val baseVersionCode = 1
override val sources = listOf(
SingleLang("Senkuro", "https://senkuro.com", "ru", overrideVersionCode = 0),
SingleLang("Senkognito", "https://senkognito.com", "ru", isNsfw = true, overrideVersionCode = 0),
)
companion object {
@JvmStatic
fun main(args: Array<String>) {
SenkuroGenerator().createAll()
}
}
}

View File

@ -0,0 +1,244 @@
package eu.kanade.tachiyomi.multisrc.senkuro
import kotlinx.serialization.Serializable
@Serializable
data class GraphQL<T>(
val query: String,
val variables: T,
)
private fun buildQuery(queryAction: () -> String): String {
return queryAction()
.trimIndent()
.replace("%", "$")
}
@Serializable
data class SearchVariables(
val query: String? = null,
val type: FiltersDto? = null,
val status: FiltersDto? = null,
val translationStatus: FiltersDto? = null,
val genre: FiltersDto? = null,
val tag: FiltersDto? = null,
val format: FiltersDto? = null,
val rating: FiltersDto? = null,
val offset: Int? = null,
) {
@Serializable
data class FiltersDto(
val include: List<String>? = null,
val exclude: List<String>? = null,
)
}
val SEARCH_QUERY: String = buildQuery {
"""
query searchTachiyomiManga(
%query: String,
%type: MangaTachiyomiSearchTypeFilter,
%status: MangaTachiyomiSearchStatusFilter,
%translationStatus: MangaTachiyomiSearchTranslationStatusFilter,
%genre: MangaTachiyomiSearchGenreFilter,
%tag: MangaTachiyomiSearchTagFilter,
%format: MangaTachiyomiSearchGenreFilter,
%rating: MangaTachiyomiSearchTagFilter,
%offset: Int,
) {
mangaTachiyomiSearch(
query:%query,
type: %type,
status: %status,
translationStatus: %translationStatus,
genre: %genre,
tag: %tag,
format: %format,
rating: %rating,
offset: %offset,
) {
mangas {
id
slug
originalName {
lang
content
}
titles {
lang
content
}
alternativeNames {
lang
content
}
cover {
original {
url
}
}
}
}
}
"""
}
@Serializable
data class FetchDetailsVariables(
val mangaId: String? = null,
)
val DETAILS_QUERY: String = buildQuery {
"""
query fetchTachiyomiManga(%mangaId: ID!) {
mangaTachiyomiInfo(mangaId: %mangaId) {
id
slug
originalName {
lang
content
}
titles {
lang
content
}
alternativeNames {
lang
content
}
localizations {
lang
description
}
type
rating
status
formats
genres {
slug
titles {
lang
content
}
}
tags {
slug
titles {
lang
content
}
}
translationStatus
cover {
original {
url
}
}
mainStaff {
roles
person {
name
}
}
}
}
"""
}
val CHAPTERS_QUERY: String = buildQuery {
"""
query fetchTachiyomiChapters(%mangaId: ID!) {
mangaTachiyomiChapters(mangaId: %mangaId) {
message
chapters {
id
slug
branchId
name
teamIds
number
volume
updatedAt
}
teams {
id
slug
name
}
}
}
"""
}
@Serializable
data class FetchChapterPagesVariables(
val mangaId: String? = null,
val chapterId: String? = null,
)
val CHAPTERS_PAGES_QUERY: String = buildQuery {
"""
query fetchTachiyomiChapterPages(
%mangaId: ID!,
%chapterId: ID!
) {
mangaTachiyomiChapterPages(
mangaId: %mangaId,
chapterId: %chapterId
) {
pages {
url
}
}
}
"""
}
@Serializable
data class MangaTachiyomiSearchFilters(
val mangaTachiyomiSearchFilters: FilterDto,
) {
@Serializable
data class FilterDto(
val genres: List<FilterDataDto>,
val tags: List<FilterDataDto>,
) {
@Serializable
data class FilterDataDto(
val slug: String,
val titles: List<TitleDto>,
) {
@Serializable
data class TitleDto(
val lang: String,
val content: String,
)
}
}
}
val FILTERS_QUERY: String = buildQuery {
"""
query fetchTachiyomiSearchFilters {
mangaTachiyomiSearchFilters {
genres {
id
slug
titles {
lang
content
}
}
tags {
id
slug
titles {
lang
content
}
}
}
}
"""
}