mirror of
https://github.com/recloudstream/cloudstream-extensions.git
synced 2024-08-15 03:03:54 +00:00
Fixed AnimePaheProvider
This commit is contained in:
parent
17ac5f1736
commit
2b174a3f57
4 changed files with 130 additions and 132 deletions
|
@ -16,7 +16,8 @@ cloudstream {
|
||||||
* 2: Slow
|
* 2: Slow
|
||||||
* 3: Beta only
|
* 3: Beta only
|
||||||
* */
|
* */
|
||||||
status = 1 // will be 3 if unspecified
|
// Status 0 because it it just a gogo scraper
|
||||||
|
status = 0 // will be 3 if unspecified
|
||||||
tvTypes = listOf(
|
tvTypes = listOf(
|
||||||
"AnimeMovie",
|
"AnimeMovie",
|
||||||
"Anime",
|
"Anime",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 1
|
version = 2
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
@ -16,11 +16,10 @@ cloudstream {
|
||||||
* 2: Slow
|
* 2: Slow
|
||||||
* 3: Beta only
|
* 3: Beta only
|
||||||
* */
|
* */
|
||||||
status = 0 // will be 3 if unspecified
|
status = 1 // will be 3 if unspecified
|
||||||
tvTypes = listOf(
|
tvTypes = listOf(
|
||||||
"AnimeMovie",
|
"AnimeMovie",
|
||||||
"Anime",
|
"Anime",
|
||||||
"OVA",
|
"OVA",
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,16 +1,15 @@
|
||||||
package com.lagradost
|
package com.lagradost
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
import com.lagradost.cloudstream3.utils.JsUnpacker
|
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
|
||||||
import com.lagradost.nicehttp.NiceResponse
|
import com.lagradost.nicehttp.NiceResponse
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
@ -18,7 +17,7 @@ import kotlin.math.pow
|
||||||
class AnimePaheProvider : MainAPI() {
|
class AnimePaheProvider : MainAPI() {
|
||||||
// credit to https://github.com/justfoolingaround/animdl/tree/master/animdl/core/codebase/providers/animepahe
|
// credit to https://github.com/justfoolingaround/animdl/tree/master/animdl/core/codebase/providers/animepahe
|
||||||
companion object {
|
companion object {
|
||||||
const val MAIN_URL = "https://animepahe.com"
|
const val MAIN_URL = "https://animepahe.ru"
|
||||||
|
|
||||||
var cookies: Map<String, String> = mapOf()
|
var cookies: Map<String, String> = mapOf()
|
||||||
private fun getType(t: String): TvType {
|
private fun getType(t: String): TvType {
|
||||||
|
@ -58,15 +57,18 @@ class AnimePaheProvider : MainAPI() {
|
||||||
TvType.OVA
|
TvType.OVA
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
override val mainPage =
|
||||||
|
listOf(MainPageData("Latest Releases", "$mainUrl/api?m=airing&page=", true))
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||||
data class Data(
|
data class Data(
|
||||||
@JsonProperty("id") val id: Int,
|
// @JsonProperty("id") val id: Int,
|
||||||
@JsonProperty("anime_id") val animeId: Int,
|
// @JsonProperty("anime_id") val animeId: Int,
|
||||||
@JsonProperty("anime_title") val animeTitle: String,
|
@JsonProperty("anime_title") val animeTitle: String,
|
||||||
@JsonProperty("anime_slug") val animeSlug: String,
|
// @JsonProperty("anime_slug") val animeSlug: String,
|
||||||
@JsonProperty("episode") val episode: Int,
|
@JsonProperty("episode") val episode: Int?,
|
||||||
@JsonProperty("snapshot") val snapshot: String,
|
@JsonProperty("snapshot") val snapshot: String?,
|
||||||
@JsonProperty("created_at") val createdAt: String,
|
@JsonProperty("created_at") val createdAt: String?,
|
||||||
@JsonProperty("anime_session") val animeSession: String,
|
@JsonProperty("anime_session") val animeSession: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -75,47 +77,34 @@ class AnimePaheProvider : MainAPI() {
|
||||||
@JsonProperty("data") val data: List<Data>
|
@JsonProperty("data") val data: List<Data>
|
||||||
)
|
)
|
||||||
|
|
||||||
val urls = listOf(
|
val response = app.get(request.data + page).text
|
||||||
Pair("$mainUrl/api?m=airing&page=1", "Latest Releases"),
|
val episodes = parseJson<AnimePaheLatestReleases>(response).data.map {
|
||||||
)
|
newAnimeSearchResponse(
|
||||||
|
it.animeTitle,
|
||||||
val items = ArrayList<HomePageList>()
|
LoadData(it.animeSession, unixTime, it.animeTitle).toJson(),
|
||||||
for (i in urls) {
|
fix = false
|
||||||
try {
|
) {
|
||||||
val response = app.get(i.first).text
|
this.posterUrl = it.snapshot
|
||||||
val episodes = parseJson<AnimePaheLatestReleases>(response).data.map {
|
addDubStatus(DubStatus.Subbed, it.episode)
|
||||||
newAnimeSearchResponse(
|
|
||||||
it.animeTitle,
|
|
||||||
"https://pahe.win/a/${it.animeId}?slug=${it.animeTitle}",
|
|
||||||
fix = false
|
|
||||||
) {
|
|
||||||
this.posterUrl = it.snapshot
|
|
||||||
addDubStatus(DubStatus.Subbed, it.episode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.add(HomePageList(i.second, episodes))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (items.size <= 0) throw ErrorLoadingException()
|
|
||||||
return HomePageResponse(items)
|
return HomePageResponse(listOf(HomePageList(request.name, episodes, request.horizontalImages)), episodes.isNotEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AnimePaheSearchData(
|
data class AnimePaheSearchData(
|
||||||
@JsonProperty("id") val id: Int,
|
@JsonProperty("id") val id: Int?,
|
||||||
@JsonProperty("slug") val slug: String,
|
@JsonProperty("slug") val slug: String?,
|
||||||
@JsonProperty("title") val title: String,
|
@JsonProperty("title") val title: String,
|
||||||
@JsonProperty("type") val type: String,
|
@JsonProperty("type") val type: String?,
|
||||||
@JsonProperty("episodes") val episodes: Int,
|
@JsonProperty("episodes") val episodes: Int?,
|
||||||
@JsonProperty("status") val status: String,
|
@JsonProperty("status") val status: String?,
|
||||||
@JsonProperty("season") val season: String,
|
@JsonProperty("season") val season: String?,
|
||||||
@JsonProperty("year") val year: Int,
|
@JsonProperty("year") val year: Int?,
|
||||||
@JsonProperty("score") val score: Double,
|
@JsonProperty("score") val score: Double?,
|
||||||
@JsonProperty("poster") val poster: String,
|
@JsonProperty("poster") val poster: String?,
|
||||||
@JsonProperty("session") val session: String,
|
@JsonProperty("session") val session: String,
|
||||||
@JsonProperty("relevance") val relevance: String
|
@JsonProperty("relevance") val relevance: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class AnimePaheSearch(
|
data class AnimePaheSearch(
|
||||||
|
@ -123,20 +112,6 @@ class AnimePaheProvider : MainAPI() {
|
||||||
@JsonProperty("data") val data: List<AnimePaheSearchData>
|
@JsonProperty("data") val data: List<AnimePaheSearchData>
|
||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun getAnimeByIdAndTitle(title: String, animeId: Int): String? {
|
|
||||||
val url = "$mainUrl/api?m=search&l=8&q=$title"
|
|
||||||
val headers = mapOf("referer" to "$mainUrl/")
|
|
||||||
|
|
||||||
val req = app.get(url, headers = headers).text
|
|
||||||
val data = parseJson<AnimePaheSearch>(req)
|
|
||||||
for (anime in data.data) {
|
|
||||||
if (anime.id == animeId) {
|
|
||||||
return "https://animepahe.com/anime/${anime.session}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun search(query: String): List<SearchResponse> {
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
val url = "$mainUrl/api?m=search&l=8&q=$query"
|
val url = "$mainUrl/api?m=search&l=8&q=$query"
|
||||||
|
@ -148,7 +123,7 @@ class AnimePaheProvider : MainAPI() {
|
||||||
return data.data.map {
|
return data.data.map {
|
||||||
newAnimeSearchResponse(
|
newAnimeSearchResponse(
|
||||||
it.title,
|
it.title,
|
||||||
"https://pahe.win/a/${it.id}?slug=${it.title}",
|
LoadData(it.session, unixTime, it.title).toJson(),
|
||||||
fix = false
|
fix = false
|
||||||
) {
|
) {
|
||||||
this.posterUrl = it.poster
|
this.posterUrl = it.poster
|
||||||
|
@ -180,12 +155,30 @@ class AnimePaheProvider : MainAPI() {
|
||||||
@JsonProperty("data") val data: List<AnimeData>
|
@JsonProperty("data") val data: List<AnimeData>
|
||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun generateListOfEpisodes(link: String): ArrayList<Episode> {
|
data class LinkLoadData(
|
||||||
try {
|
@JsonProperty("mainUrl") val mainUrl: String,
|
||||||
val attrs = link.split('/')
|
@JsonProperty("is_play_page") val is_play_page: Boolean,
|
||||||
val id = attrs[attrs.size - 1].split("?")[0]
|
@JsonProperty("episode_num") val episode_num: Int,
|
||||||
|
@JsonProperty("page") val page: Int,
|
||||||
|
@JsonProperty("session") val session: String,
|
||||||
|
@JsonProperty("episode_session") val episode_session: String,
|
||||||
|
) {
|
||||||
|
suspend fun getUrl(): String? {
|
||||||
|
return if (is_play_page) {
|
||||||
|
"$mainUrl/play/${session}/${episode_session}"
|
||||||
|
} else {
|
||||||
|
val url = "$mainUrl/api?m=release&id=${session}&sort=episode_asc&page=${page + 1}"
|
||||||
|
val jsonResponse = app.get(url).parsedSafe<AnimePaheAnimeData>() ?: return null
|
||||||
|
val episode = jsonResponse.data.firstOrNull { it.episode == episode_num }?.session
|
||||||
|
?: return null
|
||||||
|
"$mainUrl/play/${session}/${episode}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val uri = "$mainUrl/api?m=release&id=$id&sort=episode_asc&page=1"
|
private suspend fun generateListOfEpisodes(session: String): ArrayList<Episode> {
|
||||||
|
try {
|
||||||
|
val uri = "$mainUrl/api?m=release&id=$session&sort=episode_asc&page=1"
|
||||||
val headers = mapOf("referer" to "$mainUrl/")
|
val headers = mapOf("referer" to "$mainUrl/")
|
||||||
|
|
||||||
val req = app.get(uri, headers = headers).text
|
val req = app.get(uri, headers = headers).text
|
||||||
|
@ -206,7 +199,16 @@ class AnimePaheProvider : MainAPI() {
|
||||||
if (lastPage == 1 && perPage > total) {
|
if (lastPage == 1 && perPage > total) {
|
||||||
data.data.forEach {
|
data.data.forEach {
|
||||||
episodes.add(
|
episodes.add(
|
||||||
newEpisode("$mainUrl/api?m=links&id=${it.animeId}&session=${it.session}&p=kwik!!TRUE!!") {
|
newEpisode(
|
||||||
|
LinkLoadData(
|
||||||
|
mainUrl,
|
||||||
|
true,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
session,
|
||||||
|
it.session
|
||||||
|
).toJson()
|
||||||
|
) {
|
||||||
addDate(it.createdAt)
|
addDate(it.createdAt)
|
||||||
this.name = getEpisodeTitle(it)
|
this.name = getEpisodeTitle(it)
|
||||||
this.posterUrl = it.snapshot
|
this.posterUrl = it.snapshot
|
||||||
|
@ -219,7 +221,14 @@ class AnimePaheProvider : MainAPI() {
|
||||||
if (ep <= total) {
|
if (ep <= total) {
|
||||||
episodes.add(
|
episodes.add(
|
||||||
Episode(
|
Episode(
|
||||||
"$mainUrl/api?m=release&id=${id}&sort=episode_asc&page=${page + 1}&ep=${ep}!!FALSE!!"
|
LinkLoadData(
|
||||||
|
mainUrl,
|
||||||
|
false,
|
||||||
|
ep,
|
||||||
|
page + 1,
|
||||||
|
session,
|
||||||
|
""
|
||||||
|
).toJson()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
++ep
|
++ep
|
||||||
|
@ -233,16 +242,29 @@ class AnimePaheProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required to make bookmarks work with a session system
|
||||||
|
**/
|
||||||
|
data class LoadData(val session: String, val sessionDate: Long, val name: String)
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
return suspendSafeApiCall {
|
return suspendSafeApiCall {
|
||||||
val regex = Regex("""a/(\d+)\?slug=(.+)""")
|
val session = parseJson<LoadData>(url).let { data ->
|
||||||
val (animeId, animeTitle) = regex.find(url)!!.destructured
|
// Outdated
|
||||||
val link = getAnimeByIdAndTitle(animeTitle, animeId.toInt())!!
|
if (data.sessionDate + 60 * 10 < unixTime) {
|
||||||
|
parseJson<LoadData>(
|
||||||
|
search(data.name).firstOrNull()?.url ?: return@let null
|
||||||
|
).session
|
||||||
|
} else {
|
||||||
|
data.session
|
||||||
|
}
|
||||||
|
} ?: return@suspendSafeApiCall null
|
||||||
|
|
||||||
val html = app.get(link).text
|
val html = app.get("https://animepahe.ru/anime/$session").text
|
||||||
val doc = Jsoup.parse(html)
|
val doc = Jsoup.parse(html)
|
||||||
|
|
||||||
val japTitle = doc.selectFirst("h2.japanese")?.text()
|
val japTitle = doc.selectFirst("h2.japanese")?.text()
|
||||||
|
val animeTitle = doc.selectFirst("span.sr-only.unselectable")?.text()
|
||||||
val poster = doc.selectFirst(".anime-poster a")?.attr("href")
|
val poster = doc.selectFirst(".anime-poster a")?.attr("href")
|
||||||
|
|
||||||
val tvType = doc.selectFirst("""a[href*="/anime/type/"]""")?.text()
|
val tvType = doc.selectFirst("""a[href*="/anime/type/"]""")?.text()
|
||||||
|
@ -253,17 +275,18 @@ class AnimePaheProvider : MainAPI() {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val episodes = generateListOfEpisodes(url)
|
val episodes = generateListOfEpisodes(session)
|
||||||
val year = Regex("""<strong>Aired:</strong>[^,]*, (\d+)""")
|
val year = Regex("""<strong>Aired:</strong>[^,]*, (\d+)""")
|
||||||
.find(html)!!.destructured.component1()
|
.find(html)?.destructured?.component1()
|
||||||
.toIntOrNull()
|
?.toIntOrNull()
|
||||||
|
|
||||||
val status =
|
val status =
|
||||||
when (Regex("""<strong>Status:</strong>[^a]*a href=["']/anime/(.*?)["']""")
|
if (doc.selectFirst("a[href='/anime/airing']") != null)
|
||||||
.find(html)!!.destructured.component1()) {
|
ShowStatus.Ongoing
|
||||||
"airing" -> ShowStatus.Ongoing
|
else if (doc.selectFirst("a[href='/anime/completed']") != null)
|
||||||
"completed" -> ShowStatus.Completed
|
ShowStatus.Completed
|
||||||
else -> null
|
else null
|
||||||
}
|
|
||||||
val synopsis = doc.selectFirst(".anime-synopsis")?.text()
|
val synopsis = doc.selectFirst(".anime-synopsis")?.text()
|
||||||
|
|
||||||
var anilistId: Int? = null
|
var anilistId: Int? = null
|
||||||
|
@ -279,7 +302,7 @@ class AnimePaheProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newAnimeLoadResponse(animeTitle, url, getType(tvType.toString())) {
|
newAnimeLoadResponse(animeTitle ?: japTitle ?: "", url, getType(tvType.toString())) {
|
||||||
engName = animeTitle
|
engName = animeTitle
|
||||||
japName = japTitle
|
japName = japTitle
|
||||||
|
|
||||||
|
@ -459,14 +482,8 @@ class AnimePaheProvider : MainAPI() {
|
||||||
headers = mapOf("referer" to mainUrl),
|
headers = mapOf("referer" to mainUrl),
|
||||||
cookies = cookies
|
cookies = cookies
|
||||||
).text
|
).text
|
||||||
Regex("eval((.|\\n)*?)</script>").find(response)?.groupValues?.get(1)?.let { jsEval ->
|
val unpacked = getAndUnpack(response)
|
||||||
JsUnpacker("eval$jsEval").unpack()?.let { unPacked ->
|
return Regex("source=\'(.*?)\'").find(unpacked)?.groupValues?.get(1)
|
||||||
Regex("source=\'(.*?)\'").find(unPacked)?.groupValues?.get(1)?.let { link ->
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getStreamUrlFromKwikAdfly(adflyUri: String): String {
|
private suspend fun getStreamUrlFromKwikAdfly(adflyUri: String): String {
|
||||||
|
@ -505,48 +522,28 @@ class AnimePaheProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun extractVideoLinks(
|
private suspend fun extractVideoLinks(
|
||||||
episodeLink: String,
|
data: String,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
var link = episodeLink
|
val parsed = parseJson<LinkLoadData>(data)
|
||||||
val headers = mapOf("referer" to "$mainUrl/")
|
val headers = mapOf("referer" to "$mainUrl/")
|
||||||
|
val episodeUrl = parsed.getUrl() ?: return
|
||||||
|
|
||||||
if (link.contains("!!TRUE!!")) {
|
val text = app.get(episodeUrl, headers = headers).text
|
||||||
link = link.replace("!!TRUE!!", "")
|
val urlRegex = Regex("""let url = "(.*?)";""")
|
||||||
} else {
|
val embed = urlRegex.find(text)?.groupValues?.getOrNull(1) ?: return
|
||||||
val regex = """&ep=(\d+)!!FALSE!!""".toRegex()
|
|
||||||
val episodeNum = regex.find(link)?.destructured?.component1()?.toIntOrNull()
|
|
||||||
link = link.replace(regex, "")
|
|
||||||
|
|
||||||
val req = app.get(link, headers = headers).text
|
getStreamUrlFromKwik(embed)?.let { link ->
|
||||||
val jsonResponse = parseJson<AnimePaheAnimeData>(req)
|
callback(
|
||||||
val ep = ((jsonResponse.data.map {
|
ExtractorLink(
|
||||||
if (it.episode == episodeNum) {
|
this.name,
|
||||||
it
|
"Kwik",
|
||||||
} else {
|
link,
|
||||||
null
|
"https://kwik.cx/",
|
||||||
}
|
Qualities.Unknown.value,
|
||||||
}).filterNotNull())[0]
|
link.contains(".m3u8")
|
||||||
link = "$mainUrl/api?m=links&id=${ep.animeId}&session=${ep.session}&p=kwik"
|
)
|
||||||
}
|
)
|
||||||
val req = app.get(link, headers = headers).text
|
|
||||||
val data = mapper.readValue<AnimePaheEpisodeLoadLinks>(req)
|
|
||||||
|
|
||||||
data.data.forEach {
|
|
||||||
it.entries.toList().apmap { quality ->
|
|
||||||
getStreamUrlFromKwik(quality.value.kwik)?.let { link ->
|
|
||||||
callback(
|
|
||||||
ExtractorLink(
|
|
||||||
"KWIK",
|
|
||||||
"KWIK - ${quality.key} [${quality.value.audio ?: "jpn"}]",
|
|
||||||
link,
|
|
||||||
"https://kwik.cx/",
|
|
||||||
getQualityFromName(quality.key),
|
|
||||||
link.contains(".m3u8")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@ cloudstream {
|
||||||
* 2: Slow
|
* 2: Slow
|
||||||
* 3: Beta only
|
* 3: Beta only
|
||||||
* */
|
* */
|
||||||
status = 1 // will be 3 if unspecified
|
// Removed as it is an AsianLoad scraper
|
||||||
|
status = 0 // will be 3 if unspecified
|
||||||
tvTypes = listOf(
|
tvTypes = listOf(
|
||||||
"AsianDrama",
|
"AsianDrama",
|
||||||
"OVA",
|
"OVA",
|
||||||
|
|
Loading…
Reference in a new issue