├── .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")
--------------------------------------------------------------------------------