From 759ada5f411b7755a692e6ff4d1689a62294152e Mon Sep 17 00:00:00 2001
From: contusionglory <102427829+contusionglory@users.noreply.github.com>
Date: Sat, 24 Dec 2022 18:10:29 +0000
Subject: [PATCH] Added SkillShare.com (#71)
* Added SkillShare.com
* Fixes and improvement
* Fixed cursor and increased timeout
* Update build.gradle.kts
* Removed some ? and make payload more clear to read
---
SkillShareProvider/build.gradle.kts | 25 ++
.../src/main/AndroidManifest.xml | 2 +
.../com/lagradost/SkillShareProvider.kt | 252 ++++++++++++++++++
.../com/lagradost/SkillShareProviderPlugin.kt | 14 +
4 files changed, 293 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..c808ff8
--- /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= "en"
+ 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..89f5550
--- /dev/null
+++ b/SkillShareProvider/src/main/kotlin/com/lagradost/SkillShareProvider.kt
@@ -0,0 +1,252 @@
+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 com.lagradost.nicehttp.RequestBodyTypes
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+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"
+
+ private val apiUrl = "https://www.skillshare.com/api/graphql"
+ private 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
+ private var cursor = mutableMapOf("SIX_MONTHS_ENGAGEMENT" to "", "ML_TRENDINESS" to "")
+
+ override val mainPage =
+ mainPageOf(
+ "SIX_MONTHS_ENGAGEMENT" to "Popular Classes",
+ "ML_TRENDINESS" to "Trending Classes",
+ )
+
+ private suspend fun queryMovieApi(payload: String): String {
+ val req = payload.toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
+ return app.post(apiUrl, requestBody = req, referer = "$mainUrl/", timeout = 30).text
+ }
+
+ override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
+ val sortAttribute = request.data
+ if (page == 1) //reset the cursor to "" if the first page is requested
+ cursor[sortAttribute] = ""
+ 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
+ }
+ }
+ }",
+ "variables":{
+ "type":"TRENDING_CLASSES",
+ "filter":{
+ "subCategory":"",
+ "classLength":[]
+ },
+ "pageSize":30,
+ "cursor":"${cursor[sortAttribute]}",
+ "sortAttribute":"$sortAttribute"
+ },
+ "operationName":"GetClassesByType"
+ }
+ """.replace(Regex("\n")," ")
+
+ val responseBody = queryMovieApi(payload)
+ val parsedJson = parseJson(responseBody).data.classListByType.nodes
+ val home = parsedJson.map {
+ it.toSearchResult()
+ }
+ cursor[sortAttribute] = parsedJson.lastOrNull()?.id ?: "" //set the right cursor for the nextPage to work
+ 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 {
+ id
+ smallCoverUrl
+ largeCoverUrl
+ sku
+ title
+ url
+ }
+
+ query GetClassesQuery(${"$"}query: String!, ${"$"}where: SearchFilters!, ${"$"}after: String!, ${"$"}first: Int!) {
+ search(query: ${"$"}query, where: ${"$"}where, analyticsTags: [\"src:browser\", \"src:browser:search\"], after: ${"$"}after, first: ${"$"}first) {
+ edges {
+ node {
+ ...ClassFields
+ }
+ }
+ }
+ }",
+ "variables":{
+ "query":"$query",
+ "where":{
+ "level":
+ ["ALL_LEVELS","BEGINNER","INTERMEDIATE","ADVANCED"]
+ },
+ "after":"-1",
+ "first":30
+ },
+ "operationName":"GetClassesQuery"
+ }
+ """.replace(Regex("\n")," ")
+
+ val responseBody = queryMovieApi(payload)
+ 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,
+ )
+}
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())
+ }
+}