IkigaiMangas: Replace popular and latest endpoints + convention changes (#1689)
* Update * Follow exactly site order * Replace popular and latest endpoints
This commit is contained in:
parent
5344c62b6b
commit
cf4a208d08
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Ikigai Mangas'
|
extName = 'Ikigai Mangas'
|
||||||
extClass = '.IkigaiMangas'
|
extClass = '.IkigaiMangas'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class IkigaiMangas : HttpSource() {
|
class IkigaiMangas : HttpSource() {
|
||||||
|
|
||||||
@ -30,12 +31,13 @@ class IkigaiMangas : HttpSource() {
|
|||||||
|
|
||||||
override val supportsLatest: Boolean = true
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
override val client = super.client.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 1, 2)
|
.rateLimitHost(baseUrl.toHttpUrl(), 1, 2)
|
||||||
.rateLimitHost(apiBaseUrl.toHttpUrl(), 2, 1)
|
.rateLimitHost(apiBaseUrl.toHttpUrl(), 2, 1)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Origin", baseUrl)
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
@ -45,18 +47,26 @@ class IkigaiMangas : HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
val apiUrl = "$apiBaseUrl/api/swf/series?page=$page&column=view_count&direction=desc"
|
val apiUrl = "$apiBaseUrl/api/swf/series/ranking-list?type=total_ranking&series_type=comic"
|
||||||
return GET(apiUrl, headers)
|
return GET(apiUrl, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val result = json.decodeFromString<PayloadSeriesDto>(response.body.string())
|
||||||
|
val mangaList = result.data.map { it.toSManga() }
|
||||||
|
return MangasPage(mangaList, false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
val apiUrl = "$apiBaseUrl/api/swf/series?page=$page&column=last_chapter_date&direction=desc"
|
val apiUrl = "$apiBaseUrl/api/swf/new-chapters?page=$page"
|
||||||
return GET(apiUrl, headers)
|
return GET(apiUrl, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val result = json.decodeFromString<PayloadLatestDto>(response.body.string())
|
||||||
|
val mangaList = result.data.filter { it.type == "comic" }.map { it.toSManga() }
|
||||||
|
return MangasPage(mangaList, result.hasNextPage())
|
||||||
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val sortByFilter = filters.firstInstanceOrNull<SortByFilter>()
|
val sortByFilter = filters.firstInstanceOrNull<SortByFilter>()
|
||||||
@ -66,6 +76,7 @@ class IkigaiMangas : HttpSource() {
|
|||||||
if (query.isNotEmpty()) apiUrl.addQueryParameter("search", query)
|
if (query.isNotEmpty()) apiUrl.addQueryParameter("search", query)
|
||||||
|
|
||||||
apiUrl.addQueryParameter("page", page.toString())
|
apiUrl.addQueryParameter("page", page.toString())
|
||||||
|
apiUrl.addQueryParameter("type", "comic")
|
||||||
|
|
||||||
val genres = filters.firstInstanceOrNull<GenreFilter>()?.state.orEmpty()
|
val genres = filters.firstInstanceOrNull<GenreFilter>()?.state.orEmpty()
|
||||||
.filter(Genre::state)
|
.filter(Genre::state)
|
||||||
@ -82,23 +93,14 @@ class IkigaiMangas : HttpSource() {
|
|||||||
|
|
||||||
apiUrl.addQueryParameter("column", sortByFilter?.selected ?: "name")
|
apiUrl.addQueryParameter("column", sortByFilter?.selected ?: "name")
|
||||||
apiUrl.addQueryParameter("direction", if (sortByFilter?.state?.ascending == true) "asc" else "desc")
|
apiUrl.addQueryParameter("direction", if (sortByFilter?.state?.ascending == true) "asc" else "desc")
|
||||||
apiUrl.addQueryParameter("type", "comic")
|
|
||||||
|
|
||||||
return GET(apiUrl.build(), headers)
|
return GET(apiUrl.build(), headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
runCatching { fetchFilters() }
|
|
||||||
val result = json.decodeFromString<PayloadSeriesDto>(response.body.string())
|
val result = json.decodeFromString<PayloadSeriesDto>(response.body.string())
|
||||||
val mangaList = result.data.filter { it.type == "comic" }.map {
|
val mangaList = result.data.filter { it.type == "comic" }.map { it.toSManga() }
|
||||||
SManga.create().apply {
|
return MangasPage(mangaList, result.hasNextPage())
|
||||||
url = "/series/comic-${it.slug}#${it.id}"
|
|
||||||
title = it.name
|
|
||||||
thumbnail_url = it.cover
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val hasNextPage = result.currentPage < result.lastPage
|
|
||||||
return MangasPage(mangaList, hasNextPage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
@ -109,13 +111,7 @@ class IkigaiMangas : HttpSource() {
|
|||||||
val apiUrl = "$apiBaseUrl/api/swf/series/$slug".toHttpUrl()
|
val apiUrl = "$apiBaseUrl/api/swf/series/$slug".toHttpUrl()
|
||||||
val newResponse = client.newCall(GET(url = apiUrl, headers = headers)).execute()
|
val newResponse = client.newCall(GET(url = apiUrl, headers = headers)).execute()
|
||||||
val result = json.decodeFromString<PayloadSeriesDetailsDto>(newResponse.body.string())
|
val result = json.decodeFromString<PayloadSeriesDetailsDto>(newResponse.body.string())
|
||||||
return SManga.create().apply {
|
return result.series.toSMangaDetails()
|
||||||
title = result.series.name
|
|
||||||
thumbnail_url = result.series.cover
|
|
||||||
description = result.series.summary
|
|
||||||
status = parseStatus(result.series.status?.id)
|
|
||||||
genre = result.series.genres?.joinToString { it.name.trim() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String = pageViewerUrl + chapter.url
|
override fun getChapterUrl(chapter: SChapter): String = pageViewerUrl + chapter.url
|
||||||
@ -127,14 +123,7 @@ class IkigaiMangas : HttpSource() {
|
|||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val result = json.decodeFromString<PayloadChaptersDto>(response.body.string())
|
val result = json.decodeFromString<PayloadChaptersDto>(response.body.string())
|
||||||
return result.data.map {
|
return result.data.map { it.toSChapter(dateFormat) }.reversed()
|
||||||
SChapter.create().apply {
|
|
||||||
url = "/capitulo/${it.id}"
|
|
||||||
name = "Capítulo ${it.name}"
|
|
||||||
date_upload = runCatching { dateFormat.parse(it.date)?.time }
|
|
||||||
.getOrNull() ?: 0L
|
|
||||||
}
|
|
||||||
}.reversed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
@ -150,33 +139,14 @@ class IkigaiMangas : HttpSource() {
|
|||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
private fun parseStatus(statusId: Long?) = when (statusId) {
|
|
||||||
906397890812182531, 911437469204086787 -> SManga.ONGOING
|
|
||||||
906409397258190851 -> SManga.ON_HIATUS
|
|
||||||
906409532796731395, 911793517664960513 -> SManga.COMPLETED
|
|
||||||
906426661911756802, 906428048651190273, 911793767845265410, 911793856861798402 -> SManga.CANCELLED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SortProperty(val name: String, val value: String) {
|
|
||||||
override fun toString(): String = name
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSortProperties(): List<SortProperty> = listOf(
|
|
||||||
SortProperty("Nombre", "name"),
|
|
||||||
SortProperty("Creado en", "created_at"),
|
|
||||||
SortProperty("Actualización más reciente", "last_chapter_date"),
|
|
||||||
SortProperty("Número de favoritos", "bookmark_count"),
|
|
||||||
SortProperty("Número de valoración", "rating_count"),
|
|
||||||
SortProperty("Número de vistas", "view_count"),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
override fun getFilterList(): FilterList {
|
||||||
|
fetchFilters()
|
||||||
|
|
||||||
val filters = mutableListOf<Filter<*>>(
|
val filters = mutableListOf<Filter<*>>(
|
||||||
SortByFilter("Ordenar por", getSortProperties()),
|
SortByFilter("Ordenar por", getSortProperties()),
|
||||||
)
|
)
|
||||||
|
|
||||||
filters += if (genresList.isNotEmpty() || statusesList.isNotEmpty()) {
|
filters += if (filtersState == FiltersState.FETCHED) {
|
||||||
listOf(
|
listOf(
|
||||||
StatusFilter("Estados", getStatusFilters()),
|
StatusFilter("Estados", getStatusFilters()),
|
||||||
GenreFilter("Géneros", getGenreFilters()),
|
GenreFilter("Géneros", getGenreFilters()),
|
||||||
@ -190,27 +160,44 @@ class IkigaiMangas : HttpSource() {
|
|||||||
return FilterList(filters)
|
return FilterList(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getSortProperties(): List<SortProperty> = listOf(
|
||||||
|
SortProperty("Nombre", "name"),
|
||||||
|
SortProperty("Creado en", "created_at"),
|
||||||
|
SortProperty("Actualización más reciente", "last_chapter_date"),
|
||||||
|
SortProperty("Número de favoritos", "bookmark_count"),
|
||||||
|
SortProperty("Número de valoración", "rating_count"),
|
||||||
|
SortProperty("Número de vistas", "view_count"),
|
||||||
|
)
|
||||||
|
|
||||||
private fun getGenreFilters(): List<Genre> = genresList.map { Genre(it.first, it.second) }
|
private fun getGenreFilters(): List<Genre> = genresList.map { Genre(it.first, it.second) }
|
||||||
private fun getStatusFilters(): List<Status> = statusesList.map { Status(it.first, it.second) }
|
private fun getStatusFilters(): List<Status> = statusesList.map { Status(it.first, it.second) }
|
||||||
|
|
||||||
private var genresList: List<Pair<String, Long>> = emptyList()
|
private var genresList: List<Pair<String, Long>> = emptyList()
|
||||||
private var statusesList: List<Pair<String, Long>> = emptyList()
|
private var statusesList: List<Pair<String, Long>> = emptyList()
|
||||||
private var fetchFiltersAttempts = 0
|
private var fetchFiltersAttempts = 0
|
||||||
private var fetchFiltersFailed = false
|
private var filtersState = FiltersState.NOT_FETCHED
|
||||||
|
|
||||||
private fun fetchFilters() {
|
private fun fetchFilters() {
|
||||||
if (fetchFiltersAttempts <= 3 && ((genresList.isEmpty() && statusesList.isEmpty()) || fetchFiltersFailed)) {
|
if (filtersState != FiltersState.NOT_FETCHED || fetchFiltersAttempts >= 3) return
|
||||||
val filters = runCatching {
|
filtersState = FiltersState.FETCHING
|
||||||
|
fetchFiltersAttempts++
|
||||||
|
thread {
|
||||||
|
try {
|
||||||
val response = client.newCall(GET("$apiBaseUrl/api/swf/filter-options", headers)).execute()
|
val response = client.newCall(GET("$apiBaseUrl/api/swf/filter-options", headers)).execute()
|
||||||
json.decodeFromString<PayloadFiltersDto>(response.body.string())
|
val filters = json.decodeFromString<PayloadFiltersDto>(response.body.string())
|
||||||
}
|
|
||||||
|
|
||||||
fetchFiltersFailed = filters.isFailure
|
genresList = filters.data.genres.map { it.name.trim() to it.id }
|
||||||
genresList = filters.getOrNull()?.data?.genres?.map { it.name.trim() to it.id } ?: emptyList()
|
statusesList = filters.data.statuses.map { it.name.trim() to it.id }
|
||||||
statusesList = filters.getOrNull()?.data?.statuses?.map { it.name.trim() to it.id } ?: emptyList()
|
|
||||||
|
filtersState = FiltersState.FETCHED
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
filtersState = FiltersState.NOT_FETCHED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
||||||
filterIsInstance<R>().firstOrNull()
|
filterIsInstance<R>().firstOrNull()
|
||||||
|
|
||||||
|
private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED }
|
||||||
}
|
}
|
||||||
|
@ -1,73 +1,133 @@
|
|||||||
package eu.kanade.tachiyomi.extension.es.ikigaimangas
|
package eu.kanade.tachiyomi.extension.es.ikigaimangas
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PayloadSeriesDto(
|
class PayloadLatestDto(
|
||||||
val data: List<SeriesDto>,
|
val data: List<LatestDto>,
|
||||||
@SerialName("current_page")val currentPage: Int = 0,
|
@SerialName("current_page") private val currentPage: Int = 0,
|
||||||
@SerialName("last_page") val lastPage: Int = 0,
|
@SerialName("last_page") private val lastPage: Int = 0,
|
||||||
)
|
) {
|
||||||
|
fun hasNextPage() = currentPage < lastPage
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SeriesDto(
|
class LatestDto(
|
||||||
val id: Long,
|
@SerialName("series_id") private val id: Long,
|
||||||
val name: String,
|
@SerialName("series_name") private val name: String,
|
||||||
val slug: String,
|
@SerialName("series_slug") private val slug: String,
|
||||||
val cover: String? = null,
|
private val thumbnail: String? = null,
|
||||||
val type: String? = null,
|
val type: String? = null,
|
||||||
val summary: String? = null,
|
) {
|
||||||
val status: SeriesStatusDto? = null,
|
fun toSManga() = SManga.create().apply {
|
||||||
val genres: List<FilterDto>? = null,
|
url = "/series/comic-$slug#$id"
|
||||||
)
|
title = name
|
||||||
|
thumbnail_url = thumbnail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PayloadSeriesDetailsDto(
|
class PayloadSeriesDto(
|
||||||
|
val data: List<SeriesDto>,
|
||||||
|
@SerialName("current_page") private val currentPage: Int = 0,
|
||||||
|
@SerialName("last_page") private val lastPage: Int = 0,
|
||||||
|
) {
|
||||||
|
fun hasNextPage() = currentPage < lastPage
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SeriesDto(
|
||||||
|
private val id: Long,
|
||||||
|
private val name: String,
|
||||||
|
private val slug: String,
|
||||||
|
private val cover: String? = null,
|
||||||
|
val type: String? = null,
|
||||||
|
private val summary: String? = null,
|
||||||
|
private val status: SeriesStatusDto? = null,
|
||||||
|
private val genres: List<FilterDto>? = null,
|
||||||
|
) {
|
||||||
|
fun toSManga() = SManga.create().apply {
|
||||||
|
url = "/series/comic-$slug#$id"
|
||||||
|
title = name
|
||||||
|
thumbnail_url = cover
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toSMangaDetails() = SManga.create().apply {
|
||||||
|
title = name
|
||||||
|
thumbnail_url = cover
|
||||||
|
description = summary
|
||||||
|
status = parseStatus(this@SeriesDto.status?.id)
|
||||||
|
genre = genres?.joinToString { it.name.trim() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(statusId: Long?) = when (statusId) {
|
||||||
|
906397890812182531, 911437469204086787 -> SManga.ONGOING
|
||||||
|
906409397258190851 -> SManga.ON_HIATUS
|
||||||
|
906409532796731395, 911793517664960513 -> SManga.COMPLETED
|
||||||
|
906426661911756802, 906428048651190273, 911793767845265410, 911793856861798402 -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PayloadSeriesDetailsDto(
|
||||||
val series: SeriesDto,
|
val series: SeriesDto,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PayloadChaptersDto(
|
class PayloadChaptersDto(
|
||||||
var data: List<ChapterDto>,
|
var data: List<ChapterDto>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ChapterDto(
|
class ChapterDto(
|
||||||
val id: Long,
|
private val id: Long,
|
||||||
val name: String,
|
private val name: String,
|
||||||
@SerialName("published_at") val date: String,
|
@SerialName("published_at") val date: String,
|
||||||
)
|
) {
|
||||||
|
fun toSChapter(dateFormat: SimpleDateFormat) = SChapter.create().apply {
|
||||||
|
url = "/capitulo/$id"
|
||||||
|
name = "Capítulo ${this@ChapterDto.name}"
|
||||||
|
date_upload = try {
|
||||||
|
dateFormat.parse(date)?.time ?: 0L
|
||||||
|
} catch (e: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PayloadPagesDto(
|
class PayloadPagesDto(
|
||||||
val chapter: PageDto,
|
val chapter: PageDto,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PageDto(
|
class PageDto(
|
||||||
val pages: List<String>,
|
val pages: List<String>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SeriesStatusDto(
|
class SeriesStatusDto(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PayloadFiltersDto(
|
class PayloadFiltersDto(
|
||||||
val data: GenresStatusesDto,
|
val data: GenresStatusesDto,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GenresStatusesDto(
|
class GenresStatusesDto(
|
||||||
val genres: List<FilterDto>,
|
val genres: List<FilterDto>,
|
||||||
val statuses: List<FilterDto>,
|
val statuses: List<FilterDto>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class FilterDto(
|
class FilterDto(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,7 @@ class GenreFilter(title: String, genres: List<Genre>) : Filter.Group<Genre>(titl
|
|||||||
class Status(title: String, val id: Long) : Filter.CheckBox(title)
|
class Status(title: String, val id: Long) : Filter.CheckBox(title)
|
||||||
class StatusFilter(title: String, statuses: List<Status>) : Filter.Group<Status>(title, statuses)
|
class StatusFilter(title: String, statuses: List<Status>) : Filter.Group<Status>(title, statuses)
|
||||||
|
|
||||||
class SortByFilter(title: String, private val sortProperties: List<IkigaiMangas.SortProperty>) : Filter.Sort(
|
class SortByFilter(title: String, private val sortProperties: List<SortProperty>) : Filter.Sort(
|
||||||
title,
|
title,
|
||||||
sortProperties.map { it.name }.toTypedArray(),
|
sortProperties.map { it.name }.toTypedArray(),
|
||||||
Selection(0, ascending = true),
|
Selection(0, ascending = true),
|
||||||
@ -16,3 +16,7 @@ class SortByFilter(title: String, private val sortProperties: List<IkigaiMangas.
|
|||||||
val selected: String
|
val selected: String
|
||||||
get() = sortProperties[state!!.index].value
|
get() = sortProperties[state!!.index].value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SortProperty(val name: String, val value: String) {
|
||||||
|
override fun toString(): String = name
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user