From 008a7040d530d91252ef1179235cce5801cf3a25 Mon Sep 17 00:00:00 2001 From: contusionglory <102427829+contusionglory@users.noreply.github.com> Date: Fri, 23 Dec 2022 23:36:28 +0000 Subject: [PATCH] Added SkillShare.com --- SkillShareProvider/build.gradle.kts | 25 +++ .../src/main/AndroidManifest.xml | 2 + .../com/lagradost/SkillShareProvider.kt | 204 ++++++++++++++++++ .../com/lagradost/SkillShareProviderPlugin.kt | 14 ++ 4 files changed, 245 insertions(+) create mode 100644 SkillShareProvider/build.gradle.kts create mode 100644 SkillShareProvider/src/main/AndroidManifest.xml create mode 100644 SkillShareProvider/src/main/kotlin/com/lagradost/SkillShareProvider.kt create mode 100644 SkillShareProvider/src/main/kotlin/com/lagradost/SkillShareProviderPlugin.kt diff --git a/SkillShareProvider/build.gradle.kts b/SkillShareProvider/build.gradle.kts new file mode 100644 index 0000000..6105bdd --- /dev/null +++ b/SkillShareProvider/build.gradle.kts @@ -0,0 +1,25 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + language= "it" + authors = listOf("Forthe") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "TvSeries", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=skillshare.com&sz=%size%" +} diff --git a/SkillShareProvider/src/main/AndroidManifest.xml b/SkillShareProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/SkillShareProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/SkillShareProvider/src/main/kotlin/com/lagradost/SkillShareProvider.kt b/SkillShareProvider/src/main/kotlin/com/lagradost/SkillShareProvider.kt new file mode 100644 index 0000000..97c0a02 --- /dev/null +++ b/SkillShareProvider/src/main/kotlin/com/lagradost/SkillShareProvider.kt @@ -0,0 +1,204 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +class SkillShareProvider : MainAPI() { // all providers must be an instance of MainAPI + override var mainUrl = "https://www.skillshare.com" + override var name = "SkillShare" + + val apiUrl = "https://www.skillshare.com/api/graphql" + val bypassApiUrl = "https://skillshare-api.heckernohecking.repl.co" + + override val supportedTypes = setOf(TvType.TvSeries) + override val hasChromecastSupport = true + override var lang = "en" + override val hasMainPage = true + val client = OkHttpClient() + var cursor = "" + + override val mainPage = + mainPageOf( + "SIX_MONTHS_ENGAGEMENT" to "Popular Classes", + "ML_TRENDINESS" to "Trending Classes", + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val sortAttribute = request.data + val payload = + """{ "query": "query GetClassesByType(${'$'}filter: ClassFilters!, ${'$'}pageSize: Int, ${'$'}cursor: String, ${'$'}type: ClassListType!, ${'$'}sortAttribute: ClassListByTypeSortAttribute) { classListByType(type: ${'$'}type, where: ${'$'}filter, first: ${'$'}pageSize, after: ${'$'}cursor, sortAttribute: ${'$'}sortAttribute) { nodes { id title url sku smallCoverUrl largeCoverUrl studentCount durationInSeconds __typename } } }", "variables": { "type": "TRENDING_CLASSES", "filter": { "subCategory": "", "classLength": [] }, "pageSize": 30, "cursor": "$cursor", "sortAttribute": "$sortAttribute" }, "operationName": "GetClassesByType" }""" + val body = payload.toRequestBody("application/json".toMediaType()) + val r = Request.Builder() + .url(apiUrl) + .post(body) + .build() + + val response = client.newCall(r).execute() + val responseBody = response.body.string() + val parsedJson = parseJson(responseBody).data!!.classListByType!!.nodes + val home = parsedJson.map { + it.toSearchResult() + } + home.let { + cursor = + parsedJson.last().id.toString() + } + return newHomePageResponse( + arrayListOf(HomePageList(request.name, home, isHorizontalImages = true)), + hasNext = home.isNotEmpty(), + ) + } + + override suspend fun search(query: String): List { + val payload = + """{"query":"fragment ClassFields on Class {\n id\n\tsmallCoverUrl\n largeCoverUrl\n sku\n title\n url\n}\n\nquery GetClassesQuery(${"$"}query: String!, ${"$"}where: SearchFilters!, ${"$"}after: String!, ${"$"}first: Int!) {\n search(query: ${"$"}query, where: ${"$"}where, analyticsTags: [\"src:browser\", \"src:browser:search\"], after: ${"$"}after, first: ${"$"}first) {\n edges {\n node {\n ...ClassFields\n }\n }\n }\n}\n","variables":{"query":"$query","where":{"level":["ALL_LEVELS","BEGINNER","INTERMEDIATE","ADVANCED"]},"after":"-1","first":30},"operationName":"GetClassesQuery"}""" + val body = payload.toRequestBody("application/json".toMediaType()) + val r = Request.Builder() + .url(apiUrl) + .post(body) + .build() + val response = client.newCall(r).execute() + val responseBody = response.body.string() + val home = parseJson(responseBody).data!!.search!!.edges.map { + it.node!!.toSearchResult() + } + return home + } + + private fun ApiNode.toSearchResult(): SearchResponse { + val title = this.title ?: "" + val posterUrl = this.smallCoverUrl + return newTvSeriesSearchResponse( + title, + Data( + title = this.title, + courseId = this.courseId, + largeCoverUrl = this.largeCoverUrl + ).toJson(), + TvType.TvSeries + ) { + addPoster(posterUrl) + } + } + + + override suspend fun load(url: String): LoadResponse { + val data = parseJson(url) + val document = app.get(bypassApiUrl + "/${data.courseId}/0") + .parsedSafe() ?: throw ErrorLoadingException("Invalid Json Response") + val title = data.title ?: "" + val poster = data.largeCoverUrl + val episodeList = document.lessons.mapIndexed() { index, episode -> + Episode(episode.url ?: "", episode.title, 1, index) + } + + return newTvSeriesLoadResponse(title, mainUrl, TvType.TvSeries, episodeList) { + addPoster(poster) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + callback.invoke( + ExtractorLink( + name, + name, + data, + isM3u8 = false, + referer = mainUrl, + quality = Qualities.Unknown.value + ) + ) + return true + } + + + data class ApiNode( + //mainpage and search page + @JsonProperty("id") var id: String? = null, + @JsonProperty("title") var title: String? = null, + @JsonProperty("url") var url: String? = null, + @JsonProperty("sku") var courseId: String? = null, + @JsonProperty("smallCoverUrl") var smallCoverUrl: String? = null, + @JsonProperty("largeCoverUrl") var largeCoverUrl: String? = null, + ) + + data class ApiNodes( //mainpage + + @JsonProperty("nodes") var nodes: ArrayList = arrayListOf() + + ) + + data class ApiClassListByType( //mainpage + + @JsonProperty("classListByType") var classListByType: ApiNodes? = ApiNodes() + + ) + + data class ApiData( //mainpage + + @JsonProperty("data") var data: ApiClassListByType? = ApiClassListByType() + + ) + + data class SearchApiNodes( //search + + @JsonProperty("node") var node: ApiNode? = ApiNode() + + ) + + data class SearchApiEdges( //search + + @JsonProperty("edges") var edges: ArrayList = arrayListOf() + + ) + + data class SearchApiSearch( //search + + @JsonProperty("search") var search: SearchApiEdges? = SearchApiEdges() + + ) + + data class SearchApiData( //search + + @JsonProperty("data") var data: SearchApiSearch? = SearchApiSearch() + + ) + + data class BypassApiLesson( //bypass + + @JsonProperty("title") var title: String? = null, + @JsonProperty("url") var url: String? = null + + ) + + data class BypassApiData( //bypass + + @JsonProperty("class") var title: String? = null, + @JsonProperty("class_thumbnail") var largeCoverUrl: String? = null, + @JsonProperty("lessons") var lessons: ArrayList = arrayListOf() + + ) + + data class Data( + //for loading + val title: String? = null, + val courseId: String? = null, + val largeCoverUrl: String? = null, + ) +} \ No newline at end of file diff --git a/SkillShareProvider/src/main/kotlin/com/lagradost/SkillShareProviderPlugin.kt b/SkillShareProvider/src/main/kotlin/com/lagradost/SkillShareProviderPlugin.kt new file mode 100644 index 0000000..2bc844e --- /dev/null +++ b/SkillShareProvider/src/main/kotlin/com/lagradost/SkillShareProviderPlugin.kt @@ -0,0 +1,14 @@ +package com.lagradost + +import android.content.Context +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin + +@CloudstreamPlugin +class SkillShareProviderPlugin : Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list + // directly. + registerMainAPI(SkillShareProvider()) + } +}