[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>
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 73 KiB |
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 49 KiB |
|
@ -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) {}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|