├── .github └── workflows │ └── build.yml ├── .gitignore ├── DailymotionProvider ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── recloudstream │ ├── DailymotionPlugin.kt │ └── DailymotionProvider.kt ├── InvidiousProvider ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── recloudstream │ ├── InvidiousPlugin.kt │ └── InvidiousProvider.kt ├── README.md ├── TwitchProvider ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── recloudstream │ ├── TwitchPlugin.kt │ └── TwitchProvider.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── repo.json └── settings.gradle.kts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | # https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency 4 | concurrency: 5 | group: "build" 6 | cancel-in-progress: true 7 | 8 | on: 9 | push: 10 | branches: 11 | # choose your default branch 12 | - master 13 | - main 14 | paths-ignore: 15 | - '*.md' 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@master 23 | with: 24 | path: "src" 25 | 26 | - name: Checkout builds 27 | uses: actions/checkout@master 28 | with: 29 | ref: "builds" 30 | path: "builds" 31 | 32 | - name: Clean old builds 33 | run: | 34 | rm $GITHUB_WORKSPACE/builds/*.cs3 || true 35 | rm $GITHUB_WORKSPACE/builds/*.jar || true 36 | 37 | - name: Setup JDK 17 38 | uses: actions/setup-java@v1 39 | with: 40 | java-version: 17 41 | 42 | - name: Setup Android SDK 43 | uses: android-actions/setup-android@v2 44 | 45 | - name: Build Plugins 46 | run: | 47 | cd $GITHUB_WORKSPACE/src 48 | chmod +x gradlew 49 | ./gradlew make makePluginsJson 50 | ./gradlew ensureJarCompatibility 51 | cp **/build/*.cs3 $GITHUB_WORKSPACE/builds 52 | cp **/build/*.jar $GITHUB_WORKSPACE/builds 53 | cp build/plugins.json $GITHUB_WORKSPACE/builds 54 | 55 | - name: Push builds 56 | run: | 57 | cd $GITHUB_WORKSPACE/builds 58 | git config --local user.email "actions@github.com" 59 | git config --local user.name "GitHub Actions" 60 | git add . 61 | git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit 62 | git push --force 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | **/build 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | .vscode -------------------------------------------------------------------------------- /DailymotionProvider/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // use an integer for version numbers 2 | version = 3 3 | 4 | cloudstream { 5 | // All of these properties are optional, you can safely remove any of them. 6 | 7 | description = "Watch content from Dailymotion" 8 | authors = listOf("Luna712") 9 | 10 | /** 11 | * Status int as one of the following: 12 | * 0: Down 13 | * 1: Ok 14 | * 2: Slow 15 | * 3: Beta-only 16 | **/ 17 | status = 1 // Will be 3 if unspecified 18 | 19 | tvTypes = listOf("Others") 20 | iconUrl = "https://www.google.com/s2/favicons?domain=www.dailymotion.com&sz=%size%" 21 | 22 | isCrossPlatform = true 23 | } -------------------------------------------------------------------------------- /DailymotionProvider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /DailymotionProvider/src/main/kotlin/recloudstream/DailymotionPlugin.kt: -------------------------------------------------------------------------------- 1 | package recloudstream 2 | 3 | import com.lagradost.cloudstream3.plugins.BasePlugin 4 | import com.lagradost.cloudstream3.plugins.CloudstreamPlugin 5 | 6 | @CloudstreamPlugin 7 | class DailymotionPlugin: BasePlugin() { 8 | override fun load() { 9 | // All providers should be added in this manner. Please don't edit the providers list directly. 10 | registerMainAPI(DailymotionProvider()) 11 | } 12 | } -------------------------------------------------------------------------------- /DailymotionProvider/src/main/kotlin/recloudstream/DailymotionProvider.kt: -------------------------------------------------------------------------------- 1 | package recloudstream 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | import com.lagradost.cloudstream3.HomePageList 5 | import com.lagradost.cloudstream3.HomePageResponse 6 | import com.lagradost.cloudstream3.LoadResponse 7 | import com.lagradost.cloudstream3.MainAPI 8 | import com.lagradost.cloudstream3.MainPageRequest 9 | import com.lagradost.cloudstream3.SearchResponse 10 | import com.lagradost.cloudstream3.SubtitleFile 11 | import com.lagradost.cloudstream3.TvType 12 | import com.lagradost.cloudstream3.app 13 | import com.lagradost.cloudstream3.newHomePageResponse 14 | import com.lagradost.cloudstream3.newMovieLoadResponse 15 | import com.lagradost.cloudstream3.newMovieSearchResponse 16 | import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson 17 | import com.lagradost.cloudstream3.utils.ExtractorLink 18 | import com.lagradost.cloudstream3.utils.StringUtils.encodeUri 19 | import com.lagradost.cloudstream3.utils.loadExtractor 20 | 21 | class DailymotionProvider : MainAPI() { 22 | 23 | data class VideoSearchResponse( 24 | @JsonProperty("list") val list: List 25 | ) 26 | 27 | data class VideoItem( 28 | @JsonProperty("id") val id: String, 29 | @JsonProperty("title") val title: String, 30 | @JsonProperty("thumbnail_360_url") val thumbnail360Url: String 31 | ) 32 | 33 | data class VideoDetailResponse( 34 | @JsonProperty("id") val id: String, 35 | @JsonProperty("title") val title: String, 36 | @JsonProperty("description") val description: String, 37 | @JsonProperty("thumbnail_720_url") val thumbnail720Url: String 38 | ) 39 | 40 | override var mainUrl = "https://api.dailymotion.com" 41 | override var name = "Dailymotion" 42 | override val supportedTypes = setOf(TvType.Others) 43 | 44 | override var lang = "en" 45 | 46 | override val hasMainPage = true 47 | 48 | override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { 49 | val response = app.get("$mainUrl/videos?fields=id,title,thumbnail_360_url&limit=26").text 50 | val popular = tryParseJson(response)?.list ?: emptyList() 51 | 52 | return newHomePageResponse( 53 | listOf( 54 | HomePageList( 55 | "Popular", 56 | popular.map { it.toSearchResponse(this) }, 57 | true 58 | ), 59 | ), 60 | false 61 | ) 62 | } 63 | 64 | override suspend fun search(query: String): List { 65 | val response = app.get("$mainUrl/videos?fields=id,title,thumbnail_360_url&limit=10&search=${query.encodeUri()}").text 66 | val searchResults = tryParseJson(response)?.list ?: return emptyList() 67 | return searchResults.map { it.toSearchResponse(this) } 68 | } 69 | 70 | override suspend fun load(url: String): LoadResponse? { 71 | val videoId = Regex("dailymotion.com/video/([a-zA-Z0-9]+)").find(url)?.groups?.get(1)?.value 72 | val response = app.get("$mainUrl/video/$videoId?fields=id,title,description,thumbnail_720_url").text 73 | val videoDetail = tryParseJson(response) ?: return null 74 | return videoDetail.toLoadResponse(this) 75 | } 76 | 77 | private fun VideoItem.toSearchResponse(provider: DailymotionProvider): SearchResponse { 78 | return provider.newMovieSearchResponse( 79 | this.title, 80 | "https://www.dailymotion.com/video/${this.id}", 81 | TvType.Movie 82 | ) { 83 | this.posterUrl = thumbnail360Url 84 | } 85 | } 86 | 87 | private suspend fun VideoDetailResponse.toLoadResponse(provider: DailymotionProvider): LoadResponse { 88 | return provider.newMovieLoadResponse( 89 | this.title, 90 | "https://www.dailymotion.com/video/${this.id}", 91 | TvType.Movie, 92 | this.id 93 | ) { 94 | plot = description 95 | posterUrl = thumbnail720Url 96 | } 97 | } 98 | 99 | override suspend fun loadLinks( 100 | data: String, 101 | isCasting: Boolean, 102 | subtitleCallback: (SubtitleFile) -> Unit, 103 | callback: (ExtractorLink) -> Unit 104 | ): Boolean { 105 | loadExtractor( 106 | "https://www.dailymotion.com/embed/video/$data", 107 | subtitleCallback, 108 | callback 109 | ) 110 | return true 111 | } 112 | } -------------------------------------------------------------------------------- /InvidiousProvider/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // use an integer for version numbers 2 | version = 8 3 | 4 | cloudstream { 5 | // All of these properties are optional, you can safely remove any of them. 6 | 7 | description = "Watch content from any invidious instance" 8 | authors = listOf("Cloudburst") 9 | 10 | /** 11 | * Status int as one of the following: 12 | * 0: Down 13 | * 1: Ok 14 | * 2: Slow 15 | * 3: Beta-only 16 | **/ 17 | status = 1 // Will be 3 if unspecified 18 | 19 | tvTypes = listOf("Others") 20 | iconUrl = "https://www.google.com/s2/favicons?domain=invidious.io&sz=%size%" 21 | 22 | isCrossPlatform = true 23 | } -------------------------------------------------------------------------------- /InvidiousProvider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /InvidiousProvider/src/main/kotlin/recloudstream/InvidiousPlugin.kt: -------------------------------------------------------------------------------- 1 | package recloudstream 2 | 3 | import com.lagradost.cloudstream3.plugins.BasePlugin 4 | import com.lagradost.cloudstream3.plugins.CloudstreamPlugin 5 | 6 | @CloudstreamPlugin 7 | class InvidiousPlugin: BasePlugin() { 8 | override fun load() { 9 | // All providers should be added in this manner. Please don't edit the providers list directly. 10 | registerMainAPI(InvidiousProvider()) 11 | } 12 | } -------------------------------------------------------------------------------- /InvidiousProvider/src/main/kotlin/recloudstream/InvidiousProvider.kt: -------------------------------------------------------------------------------- 1 | package recloudstream 2 | 3 | import com.lagradost.cloudstream3.Actor 4 | import com.lagradost.cloudstream3.ActorData 5 | import com.lagradost.cloudstream3.HomePageList 6 | import com.lagradost.cloudstream3.HomePageResponse 7 | import com.lagradost.cloudstream3.LoadResponse 8 | import com.lagradost.cloudstream3.MainAPI 9 | import com.lagradost.cloudstream3.MainPageRequest 10 | import com.lagradost.cloudstream3.SearchResponse 11 | import com.lagradost.cloudstream3.SubtitleFile 12 | import com.lagradost.cloudstream3.TvType 13 | import com.lagradost.cloudstream3.app 14 | import com.lagradost.cloudstream3.newHomePageResponse 15 | import com.lagradost.cloudstream3.newMovieLoadResponse 16 | import com.lagradost.cloudstream3.newMovieSearchResponse 17 | import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson 18 | import com.lagradost.cloudstream3.utils.ExtractorLink 19 | import com.lagradost.cloudstream3.utils.ExtractorLinkType 20 | import com.lagradost.cloudstream3.utils.Qualities 21 | import com.lagradost.cloudstream3.utils.StringUtils.encodeUri 22 | import com.lagradost.cloudstream3.utils.loadExtractor 23 | import com.lagradost.cloudstream3.utils.newExtractorLink 24 | 25 | class InvidiousProvider : MainAPI() { // all providers must be an instance of MainAPI 26 | override var mainUrl = "https://iv.ggtyler.dev" 27 | override var name = "Invidious" // name of provider 28 | override val supportedTypes = setOf(TvType.Others) 29 | 30 | override var lang = "en" 31 | 32 | // enable this when your provider has a main page 33 | override val hasMainPage = true 34 | 35 | override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { 36 | val popular = tryParseJson>( 37 | app.get("$mainUrl/api/v1/popular?fields=videoId,title").text 38 | ) 39 | val trending = tryParseJson>( 40 | app.get("$mainUrl/api/v1/trending?fields=videoId,title").text 41 | ) 42 | return newHomePageResponse( 43 | listOf( 44 | HomePageList( 45 | "Popular", 46 | popular?.map { it.toSearchResponse(this) } ?: emptyList(), 47 | true 48 | ), 49 | HomePageList( 50 | "Trending", 51 | trending?.map { it.toSearchResponse(this) } ?: emptyList(), 52 | true 53 | ) 54 | ), 55 | false 56 | ) 57 | } 58 | 59 | // this function gets called when you search for something 60 | override suspend fun search(query: String): List { 61 | val res = tryParseJson>( 62 | app.get("$mainUrl/api/v1/search?q=${query.encodeUri()}&page=1&type=video&fields=videoId,title").text 63 | ) 64 | return res?.map { it.toSearchResponse(this) } ?: emptyList() 65 | } 66 | 67 | override suspend fun load(url: String): LoadResponse? { 68 | val videoId = Regex("watch\\?v=([a-zA-Z0-9_-]+)").find(url)?.groups?.get(1)?.value 69 | val res = tryParseJson( 70 | app.get("$mainUrl/api/v1/videos/$videoId?fields=videoId,title,description,recommendedVideos,author,authorThumbnails,formatStreams").text 71 | ) 72 | return res?.toLoadResponse(this) 73 | } 74 | 75 | private data class SearchEntry( 76 | val title: String, 77 | val videoId: String 78 | ) { 79 | fun toSearchResponse(provider: InvidiousProvider): SearchResponse { 80 | return provider.newMovieSearchResponse( 81 | title, 82 | "${provider.mainUrl}/watch?v=$videoId", 83 | TvType.Movie 84 | ) { 85 | this.posterUrl = "${provider.mainUrl}/vi/$videoId/mqdefault.jpg" 86 | } 87 | } 88 | } 89 | 90 | private data class VideoEntry( 91 | val title: String, 92 | val description: String, 93 | val videoId: String, 94 | val recommendedVideos: List, 95 | val author: String, 96 | val authorThumbnails: List 97 | ) { 98 | suspend fun toLoadResponse(provider: InvidiousProvider): LoadResponse { 99 | return provider.newMovieLoadResponse( 100 | title, 101 | "${provider.mainUrl}/watch?v=$videoId", 102 | TvType.Movie, 103 | videoId 104 | ) { 105 | plot = description 106 | posterUrl = "${provider.mainUrl}/vi/$videoId/hqdefault.jpg" 107 | recommendations = recommendedVideos.map { it.toSearchResponse(provider) } 108 | actors = listOf( 109 | ActorData( 110 | Actor(author, authorThumbnails.getOrNull(authorThumbnails.size - 1)?.url ?: ""), 111 | roleString = "Author" 112 | ) 113 | ) 114 | } 115 | } 116 | } 117 | 118 | private data class Thumbnail( 119 | val url: String 120 | ) 121 | 122 | override suspend fun loadLinks( 123 | data: String, 124 | isCasting: Boolean, 125 | subtitleCallback: (SubtitleFile) -> Unit, 126 | callback: (ExtractorLink) -> Unit 127 | ): Boolean { 128 | loadExtractor( 129 | "https://youtube.com/watch?v=$data", 130 | subtitleCallback, 131 | callback 132 | ) 133 | callback( 134 | newExtractorLink(this.name, this.name, "$mainUrl/api/manifest/dash/id/$data") { 135 | quality = Qualities.Unknown.value 136 | type = ExtractorLinkType.DASH 137 | referer = "" 138 | } 139 | ) 140 | return true 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudstream extensions 2 | 3 | This repository contains a collection of extensions for [Cloudstream3](https://github.com/recloudstream/cloudstream) 4 | 5 | ## Attribution 6 | 7 | The gradle plugin and the whole plugin system is **heavily** based on [Aliucord](https://github.com/Aliucord). 8 | *Go use it, it's a great mobile discord client mod!* 9 | -------------------------------------------------------------------------------- /TwitchProvider/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Use an integer for version numbers 2 | version = 2 3 | 4 | cloudstream { 5 | // All of these properties are optional, you can safely remove any of them. 6 | 7 | description = "Watch livestreams from Twitch" 8 | authors = listOf("CranberrySoup") 9 | 10 | /** 11 | * Status int as one of the following: 12 | * 0: Down 13 | * 1: Ok 14 | * 2: Slow 15 | * 3: Beta-only 16 | **/ 17 | status = 1 // Will be 3 if unspecified 18 | 19 | tvTypes = listOf("Live") 20 | iconUrl = "https://www.google.com/s2/favicons?domain=twitch.tv&sz=%size%" 21 | 22 | isCrossPlatform = true 23 | } -------------------------------------------------------------------------------- /TwitchProvider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /TwitchProvider/src/main/kotlin/recloudstream/TwitchPlugin.kt: -------------------------------------------------------------------------------- 1 | package recloudstream 2 | 3 | import com.lagradost.cloudstream3.plugins.BasePlugin 4 | import com.lagradost.cloudstream3.plugins.CloudstreamPlugin 5 | 6 | @CloudstreamPlugin 7 | class TwitchPlugin: BasePlugin() { 8 | override fun load() { 9 | // All providers should be added in this manner. Please don't edit the providers list directly. 10 | registerMainAPI(TwitchProvider()) 11 | registerExtractorAPI(TwitchProvider.TwitchExtractor()) 12 | } 13 | } -------------------------------------------------------------------------------- /TwitchProvider/src/main/kotlin/recloudstream/TwitchProvider.kt: -------------------------------------------------------------------------------- 1 | package recloudstream 2 | 3 | import com.lagradost.cloudstream3.HomePageList 4 | import com.lagradost.cloudstream3.HomePageResponse 5 | import com.lagradost.cloudstream3.LiveSearchResponse 6 | import com.lagradost.cloudstream3.LoadResponse 7 | import com.lagradost.cloudstream3.MainAPI 8 | import com.lagradost.cloudstream3.MainPageRequest 9 | import com.lagradost.cloudstream3.SearchResponse 10 | import com.lagradost.cloudstream3.SubtitleFile 11 | import com.lagradost.cloudstream3.TvType 12 | import com.lagradost.cloudstream3.app 13 | import com.lagradost.cloudstream3.fixUrl 14 | import com.lagradost.cloudstream3.mainPageOf 15 | import com.lagradost.cloudstream3.newHomePageResponse 16 | import com.lagradost.cloudstream3.newLiveSearchResponse 17 | import com.lagradost.cloudstream3.newLiveStreamLoadResponse 18 | import com.lagradost.cloudstream3.utils.ExtractorApi 19 | import com.lagradost.cloudstream3.utils.ExtractorLink 20 | import com.lagradost.cloudstream3.utils.ExtractorLinkType 21 | import com.lagradost.cloudstream3.utils.getQualityFromName 22 | import com.lagradost.cloudstream3.utils.loadExtractor 23 | import com.lagradost.cloudstream3.utils.newExtractorLink 24 | import org.jsoup.nodes.Element 25 | import java.lang.RuntimeException 26 | 27 | class TwitchProvider : MainAPI() { 28 | override var mainUrl = "https://twitchtracker.com" // Easiest to scrape 29 | override var name = "Twitch" 30 | override val supportedTypes = setOf(TvType.Live) 31 | 32 | override var lang = "uni" 33 | 34 | override val hasMainPage = true 35 | private val gamesName = "games" 36 | 37 | override val mainPage = mainPageOf( 38 | "$mainUrl/channels/live" to "Top global live streams", 39 | "$mainUrl/games" to gamesName 40 | ) 41 | private val isHorizontal = true 42 | 43 | override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { 44 | return when (request.name) { 45 | gamesName -> newHomePageResponse(parseGames(), hasNext = false) // Get top games 46 | else -> { 47 | val doc = app.get(request.data, params = mapOf("page" to page.toString())).document 48 | val channels = doc.select("table#channels tr").map { element -> 49 | element.toLiveSearchResponse() 50 | } 51 | newHomePageResponse( 52 | listOf( 53 | HomePageList( 54 | request.name, 55 | channels, 56 | isHorizontalImages = isHorizontal 57 | ) 58 | ), 59 | hasNext = true 60 | ) 61 | } 62 | } 63 | } 64 | 65 | private fun Element.toLiveSearchResponse(): LiveSearchResponse { 66 | val anchor = this.select("a") 67 | val linkName = anchor.attr("href").substringAfterLast("/") 68 | val name = anchor.firstOrNull { it.text().isNotBlank() }?.text() 69 | val image = this.select("img").attr("src") 70 | return newLiveSearchResponse( 71 | name ?: "", 72 | linkName, 73 | TvType.Live, 74 | fix = false 75 | ) { posterUrl = image } 76 | } 77 | 78 | private suspend fun parseGames(): List { 79 | val doc = app.get("$mainUrl/games").document 80 | return doc.select("div.ranked-item") 81 | .take(5) 82 | .mapNotNull { element -> // No apmap to prevent getting 503 by cloudflare 83 | val game = element.select("div.ri-name > a") 84 | val url = fixUrl(game.attr("href")) 85 | val name = game.text() 86 | val searchResponses = parseGame(url).ifEmpty { return@mapNotNull null } 87 | HomePageList(name, searchResponses, isHorizontalImages = isHorizontal) 88 | } 89 | } 90 | 91 | private suspend fun parseGame(url: String): List { 92 | val doc = app.get(url).document 93 | return doc.select("td.cell-slot.sm").map { element -> 94 | element.toLiveSearchResponse() 95 | } 96 | } 97 | 98 | override suspend fun load(url: String): LoadResponse { 99 | val realUrl = url.substringAfterLast("/") 100 | val doc = app.get("$mainUrl/$realUrl", referer = mainUrl).document 101 | val name = doc.select("div#app-title").text() 102 | if (name.isBlank()) { 103 | throw RuntimeException("Could not load page, please try again.\n") 104 | } 105 | val rank = doc.select("div.rank-badge > span").last()?.text()?.toIntOrNull() 106 | val image = doc.select("div#app-logo > img").attr("src") 107 | val poster = doc.select("div.embed-responsive > img").attr("src").ifEmpty { image } 108 | val description = doc.select("div[style='word-wrap:break-word;font-size:12px;']").text() 109 | val language = doc.select("a.label.label-soft").text().ifEmpty { null } 110 | val isLive = doc.select("div.live-indicator-container").isNotEmpty() 111 | 112 | val tags = listOfNotNull( 113 | isLive.let { if (it) "Live" else "Offline" }, 114 | language, 115 | rank?.let { "Rank: $it" }, 116 | ) 117 | 118 | val twitchUrl = "https://twitch.tv/$realUrl" 119 | 120 | return newLiveStreamLoadResponse( 121 | name, twitchUrl, twitchUrl 122 | ) { 123 | plot = description 124 | posterUrl = image 125 | backgroundPosterUrl = poster 126 | this@newLiveStreamLoadResponse.tags = tags 127 | } 128 | } 129 | 130 | override suspend fun search(query: String): List? { 131 | val document = 132 | app.get("$mainUrl/search", params = mapOf("q" to query), referer = mainUrl).document 133 | return document.select("table.tops tr").map { it.toLiveSearchResponse() } 134 | } 135 | 136 | override suspend fun loadLinks( 137 | data: String, 138 | isCasting: Boolean, 139 | subtitleCallback: (SubtitleFile) -> Unit, 140 | callback: (ExtractorLink) -> Unit 141 | ): Boolean { 142 | return loadExtractor(data, subtitleCallback, callback) 143 | } 144 | 145 | class TwitchExtractor : ExtractorApi() { 146 | override val mainUrl = "https://twitch.tv/" 147 | override val name = "Twitch" 148 | override val requiresReferer = false 149 | 150 | data class ApiResponse( 151 | val success: Boolean, 152 | val urls: Map? 153 | ) 154 | 155 | override suspend fun getUrl( 156 | url: String, 157 | referer: String?, 158 | subtitleCallback: (SubtitleFile) -> Unit, 159 | callback: (ExtractorLink) -> Unit 160 | ) { 161 | val response = 162 | app.get("https://pwn.sh/tools/streamapi.py?url=$url").parsed() 163 | response.urls?.forEach { (name, url) -> 164 | val quality = getQualityFromName(name.substringBefore("p")) 165 | callback.invoke( 166 | newExtractorLink( 167 | this.name, 168 | "${this.name} ${name.replace("${quality}p", "")}", 169 | url 170 | ) { 171 | this.type = ExtractorLinkType.M3U8 172 | this.quality = quality 173 | this.referer = "" 174 | } 175 | ) 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.BaseExtension 2 | import com.lagradost.cloudstream3.gradle.CloudstreamExtension 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 5 | 6 | buildscript { 7 | repositories { 8 | google() 9 | mavenCentral() 10 | // Shitpack repo which contains our tools and dependencies 11 | maven("https://jitpack.io") 12 | } 13 | 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:8.7.3") 16 | // Cloudstream gradle plugin which makes everything work and builds plugins 17 | classpath("com.github.recloudstream:gradle:-SNAPSHOT") 18 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | google() 25 | mavenCentral() 26 | maven("https://jitpack.io") 27 | } 28 | } 29 | 30 | fun Project.cloudstream(configuration: CloudstreamExtension.() -> Unit) = extensions.getByName("cloudstream").configuration() 31 | 32 | fun Project.android(configuration: BaseExtension.() -> Unit) = extensions.getByName("android").configuration() 33 | 34 | subprojects { 35 | apply(plugin = "com.android.library") 36 | apply(plugin = "kotlin-android") 37 | apply(plugin = "com.lagradost.cloudstream3.gradle") 38 | 39 | cloudstream { 40 | // when running through github workflow, GITHUB_REPOSITORY should contain current repository name 41 | // you can modify it to use other git hosting services, like gitlab 42 | setRepo(System.getenv("GITHUB_REPOSITORY") ?: "https://github.com/user/repo") 43 | } 44 | 45 | android { 46 | namespace = "recloudstream" 47 | 48 | defaultConfig { 49 | minSdk = 21 50 | compileSdkVersion(35) 51 | targetSdk = 35 52 | } 53 | 54 | compileOptions { 55 | sourceCompatibility = JavaVersion.VERSION_1_8 56 | targetCompatibility = JavaVersion.VERSION_1_8 57 | } 58 | 59 | tasks.withType { 60 | compilerOptions { 61 | jvmTarget.set(JvmTarget.JVM_1_8) // Required 62 | freeCompilerArgs.addAll( 63 | "-Xno-call-assertions", 64 | "-Xno-param-assertions", 65 | "-Xno-receiver-assertions" 66 | ) 67 | } 68 | } 69 | } 70 | 71 | dependencies { 72 | val implementation by configurations 73 | 74 | implementation("com.github.recloudstream.cloudstream:library-jvm:master") 75 | 76 | // These dependencies can include any of those which are added by the app, 77 | // but you don't need to include any of them if you don't need them. 78 | // https://github.com/recloudstream/cloudstream/blob/master/app/build.gradle.kts 79 | implementation(kotlin("stdlib")) // Adds Standard Kotlin Features 80 | implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib 81 | implementation("org.jsoup:jsoup:1.18.3") // HTML Parser 82 | // IMPORTANT: Do not bump Jackson above 2.13.1, as newer versions will 83 | // break compatibility on older Android devices. 84 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") // JSON Parser 85 | } 86 | } 87 | 88 | task("clean") { 89 | delete(rootProject.layout.buildDirectory) 90 | } 91 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recloudstream/extensions/45e8de3579ef9b961a888c7f69bb2da629a082ac/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 3 | distributionPath=wrapper/dists 4 | zipStorePath=wrapper/dists 5 | zipStoreBase=GRADLE_USER_HOME -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega -------------------------------------------------------------------------------- /repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cloudstream providers repository", 3 | "description": "Cloudstream extension Repository", 4 | "manifestVersion": 1, 5 | "pluginLists": [ 6 | "https://raw.githubusercontent.com/recloudstream/extensions/builds/plugins.json" 7 | ] 8 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "CloudstreamPlugins" 2 | 3 | // This file sets what projects are included. 4 | // All new projects should get automatically included unless specified in the "disabled" variable. 5 | 6 | val disabled = listOf() 7 | 8 | File(rootDir, ".").eachDir { dir -> 9 | if (!disabled.contains(dir.name) && File(dir, "build.gradle.kts").exists()) { 10 | include(dir.name) 11 | } 12 | } 13 | 14 | fun File.eachDir(block: (File) -> Unit) { 15 | listFiles()?.filter { it.isDirectory }?.forEach { block(it) } 16 | } 17 | 18 | // To only include a single project, comment out the previous lines (except the first one), and include your plugin like so: 19 | // include("PluginName") --------------------------------------------------------------------------------