Skip to content

Commit 3713b1c

Browse files
errorcodeQQerrorcodeQQ
authored andcommitted
Modernize plugin template build system, fix CI workflows, and enable local JUnit testing
1 parent e95d3a7 commit 3713b1c

8 files changed

Lines changed: 152 additions & 44 deletions

File tree

.github/workflows/build.yml

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,39 @@ jobs:
1919
runs-on: ubuntu-latest
2020
steps:
2121
- name: Checkout
22-
uses: actions/checkout@master
22+
uses: actions/checkout@v6
2323
with:
2424
path: "src"
2525

2626
- name: Checkout builds
27-
uses: actions/checkout@master
27+
uses: actions/checkout@v6
2828
with:
2929
ref: "builds"
3030
path: "builds"
3131

3232
- name: Clean old builds
3333
run: |
34-
rm -f $GITHUB_WORKSPACE/builds/*.cs3
35-
rm -f $GITHUB_WORKSPACE/builds/*.jar
34+
rm $GITHUB_WORKSPACE/builds/*.cs3 || true
35+
rm $GITHUB_WORKSPACE/builds/*.jar || true
3636
37-
- name: Setup JDK 17
38-
uses: actions/setup-java@v1
37+
- name: Set up JDK 17
38+
uses: actions/setup-java@v5
3939
with:
40-
java-version: 17
40+
distribution: temurin
41+
java-version: '17'
4142

42-
- name: Setup Android SDK
43-
uses: android-actions/setup-android@v2
43+
- name: Setup Gradle
44+
uses: gradle/actions/setup-gradle@v5
45+
with:
46+
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
47+
cache-read-only: false
4448

4549
- name: Build Plugins
4650
run: |
4751
cd $GITHUB_WORKSPACE/src
4852
# Dynamically enable all plugins by clearing disabled list in settings.gradle.kts
49-
python3 -c "import re; content = open('settings.gradle.kts').read(); open('settings.gradle.kts', 'w').write(re.sub(r'val disabled = listOf<String>\([^)]*\)', 'val disabled = listOf<String>()', content))"
5053
chmod +x gradlew
51-
./gradlew make makePluginsJson
54+
./gradlew makePluginsJson
5255
cp **/build/*.cs3 $GITHUB_WORKSPACE/builds
5356
cp **/build/*.jar $GITHUB_WORKSPACE/builds || true
5457
cp build/plugins.json $GITHUB_WORKSPACE/builds

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ cs3_plugin_development_guide.md
3535
cs3 plugin development guide.md
3636
cs3_plugin_development_guide_recovered.md
3737
cs3_plugin_development_guide_recovered_full.md
38+
39+
jdk17*
40+

ExampleProvider/build.gradle.kts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
dependencies {
2-
// Add provider-specific dependencies here if needed.
3-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
4-
}
5-
1+
// Use an integer for version numbers
62
version = 1
73

84
cloudstream {
95
description = "Example Provider Template"
106
authors = listOf("author")
7+
language = "en"
8+
9+
/**
10+
* Status int:
11+
* 0: Down
12+
* 1: Ok
13+
* 2: Slow
14+
* 3: Beta only
15+
*/
1116
status = 3
17+
18+
// List of video source types. See:
19+
// https://recloudstream.github.io/cloudstream/html/app/com.lagradost.cloudstream3/-tv-type/index.html
1220
tvTypes = listOf("Movie", "TvSeries", "Anime", "AnimeMovie")
21+
22+
// iconUrl = "https://raw.githubusercontent.com/<user>/<repo>/<branch>/ExampleProvider/icon.png"
23+
1324
requiresResources = false
14-
language = "en"
1525
isCrossPlatform = true
1626
}
1727

ExampleProvider/src/main/kotlin/com/example/ExampleProvider.kt

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.example
22

33
import com.lagradost.cloudstream3.*
44
import com.lagradost.cloudstream3.utils.*
5-
import kotlinx.coroutines.async
65
import kotlinx.coroutines.coroutineScope
76
import kotlinx.coroutines.launch
87
import java.util.concurrent.ConcurrentHashMap
@@ -14,7 +13,13 @@ class ExampleProvider : MainAPI() {
1413
override var lang = "en"
1514
override val hasDownloadSupport = true
1615

17-
// Use ConcurrentHashMap for session caching if headers/cookies are needed per request.
16+
// Define homepage categories
17+
override val mainPage = mainPageOf(
18+
"trending" to "Trending Content",
19+
"latest" to "Latest Uploads"
20+
)
21+
22+
// Per-session header/cookie cache (thread-safe).
1823
private val sessionHeaders = ConcurrentHashMap<String, String>()
1924

2025
override val supportedTypes = setOf(
@@ -24,28 +29,28 @@ class ExampleProvider : MainAPI() {
2429
)
2530

2631
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
27-
// Implement main page scraping here.
32+
// TODO: fetch home page rows from mainUrl and map to SearchResponse items.
2833
val items = ArrayList<SearchResponse>()
2934
return newHomePageResponse(request.name, items, hasNext = true)
3035
}
3136

3237
override suspend fun search(query: String): List<SearchResponse> {
33-
// Implement search scraping here.
38+
// TODO: call "$mainUrl/search?q=$query" and parse results.
3439
return emptyList()
3540
}
3641

3742
override suspend fun load(url: String): LoadResponse {
38-
// Implement metadata loading here.
43+
// TODO: fetch metadata for the given url and return either a movie or series response.
3944
val title = "Example Title"
4045
val poster = ""
4146
val isMovie = true
42-
43-
if (isMovie) {
44-
return newMovieLoadResponse(title, url, TvType.Movie, url) {
47+
48+
return if (isMovie) {
49+
newMovieLoadResponse(title, url, TvType.Movie, url) {
4550
this.posterUrl = poster
4651
}
4752
} else {
48-
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, emptyList()) {
53+
newTvSeriesLoadResponse(title, url, TvType.TvSeries, emptyList()) {
4954
this.posterUrl = poster
5055
}
5156
}
@@ -57,19 +62,31 @@ class ExampleProvider : MainAPI() {
5762
subtitleCallback: (SubtitleFile) -> Unit,
5863
callback: (ExtractorLink) -> Unit
5964
): Boolean {
60-
// Use coroutines for concurrent HTTP requests when resolving links to prevent bottlenecking.
61-
62-
val linksToResolve = listOf("https://server1.com/video", "https://server2.com/video")
63-
65+
// TODO: collect the list of playable URLs, then resolve them concurrently.
66+
val linksToResolve = listOf<String>() // e.g. listOf("$mainUrl/embed/123")
67+
6468
coroutineScope {
6569
linksToResolve.forEach { link ->
6670
launch {
67-
// Extract link logic here
71+
// Use the built-in extractor cascade:
6872
// loadExtractor(link, subtitleCallback, callback)
73+
74+
// Or build a custom ExtractorLink:
75+
// callback(
76+
// newExtractorLink(
77+
// source = "ExampleServer",
78+
// name = "ExampleServer",
79+
// url = link,
80+
// type = ExtractorLinkType.M3U8
81+
// ) {
82+
// referer = mainUrl
83+
// headers = mapOf("User-Agent" to "Mozilla/5.0")
84+
// }
85+
// )
6986
}
7087
}
7188
}
72-
89+
7390
return true
7491
}
7592
}

ExampleProvider/src/main/kotlin/com/example/ExampleProviderPlugin.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
66
@CloudstreamPlugin
77
class ExampleProviderPlugin : BasePlugin() {
88
override fun load() {
9-
// All providers should be added here
9+
// Register all MainAPI providers here
1010
registerMainAPI(ExampleProvider())
11-
// Register any custom extractors here
11+
12+
// Register any custom extractors here, e.g.:
1213
// registerExtractorAPI(ExampleExtractor())
1314
}
1415
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.example
2+
3+
import com.lagradost.cloudstream3.TvSeriesLoadResponse
4+
import com.lagradost.cloudstream3.utils.ExtractorLink
5+
import kotlinx.coroutines.runBlocking
6+
import org.junit.jupiter.api.Test
7+
8+
class ExampleTest {
9+
private val provider = ExampleProvider()
10+
11+
// Replace with a valid URL from the provider
12+
private val testUrl = "https://example.com/movie-page"
13+
14+
@Test
15+
fun testProviderMetadata() = runBlocking {
16+
println("[*] Loading URL: $testUrl")
17+
val response = provider.load(testUrl)
18+
19+
// 1. Verify Core Details
20+
assert(response.name.isNotBlank()) { "Error: Title card is blank!" }
21+
assert(response.posterUrl?.startsWith("http") == true || response.posterUrl.isNullOrEmpty()) { "Error: Poster URL is invalid: ${response.posterUrl}" }
22+
23+
println("Title: ${response.name}")
24+
println("Poster: ${response.posterUrl}")
25+
26+
// 2. Verify Episode Indexing
27+
val episodes = if (response is TvSeriesLoadResponse) {
28+
response.episodes
29+
} else emptyList()
30+
31+
if (response is TvSeriesLoadResponse) {
32+
assert(episodes.isNotEmpty()) { "Error: Mapped episode list is empty!" }
33+
println("Total Mapped Episodes: ${episodes.size}")
34+
35+
// 3. Verify Individual Episode Integrity
36+
episodes.forEachIndexed { index, ep ->
37+
val epNum = ep.episode ?: 0
38+
assert(ep.name?.isNotBlank() == true || epNum > 0) { "Error: Episode $epNum is missing details!" }
39+
assert(ep.data.isNotBlank()) { "Error: Playback payload for Episode $epNum is empty!" }
40+
}
41+
}
42+
}
43+
44+
@Test
45+
fun testVideoStreamIntegrity() = runBlocking {
46+
println("[*] Fetching and verifying playback stream links directly...")
47+
val response = provider.load(testUrl)
48+
val epData = if (response is TvSeriesLoadResponse) {
49+
response.episodes.firstOrNull()?.data
50+
} else if (response is com.lagradost.cloudstream3.MovieLoadResponse) {
51+
response.dataUrl
52+
} else null
53+
54+
assert(epData != null) { "Error: No episode/movie data payload found!" }
55+
56+
val resolvedLinks = mutableListOf<ExtractorLink>()
57+
provider.loadLinks(epData!!, isCasting = false, subtitleCallback = {}, callback = { link ->
58+
resolvedLinks.add(link)
59+
})
60+
61+
println("[*] Resolved ${resolvedLinks.size} links.")
62+
}
63+
}

build.gradle.kts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ fun Project.cloudstream(configuration: CloudstreamExtension.() -> Unit) = extens
3131

3232
fun Project.android(configuration: LibraryExtension.() -> Unit) {
3333
extensions.getByName<LibraryExtension>("android").apply {
34+
project.extensions.findByType(JavaPluginExtension::class.java)?.apply {
35+
toolchain {
36+
languageVersion.set(JavaLanguageVersion.of(17))
37+
}
38+
}
3439
configuration()
3540
}
3641
}
@@ -95,6 +100,16 @@ subprojects {
95100
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
96101
implementation("org.bouncycastle:bcpkix-jdk18on:1.84")
97102
implementation("org.jspecify:jspecify:1.0.0")
103+
// Testing dependencies
104+
add("testImplementation", "org.junit.jupiter:junit-jupiter-api:5.10.1")
105+
add("testRuntimeOnly", "org.junit.jupiter:junit-jupiter-engine:5.10.1")
106+
add("testRuntimeOnly", "org.junit.platform:junit-platform-launcher:1.10.1")
107+
add("testRuntimeOnly", "org.junit.vintage:junit-vintage-engine:5.10.1")
108+
add("testImplementation", files("${System.getProperty("user.home")}/.gradle/caches/cloudstream/cloudstream/cloudstream.jar"))
109+
}
110+
111+
tasks.withType<Test> {
112+
useJUnitPlatform()
98113
}
99114
}
100115

gradle.properties

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,10 @@
66
# http://www.gradle.org/docs/current/userguide/build_environment.html
77
# Specifies the JVM arguments used for the daemon process.
88
# 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
9+
org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8
10+
org.gradle.parallel=true
1711
android.useAndroidX=true
18-
# Automatically convert third-party libraries to use AndroidX
19-
12+
android.buildFeatures.buildConfig=true
13+
org.gradle.caching=true
14+
org.gradle.configuration-cache=true
15+
android.javaCompile.suppressSourceTargetDeprecationWarning=true

0 commit comments

Comments
 (0)