From 1e79906932b3b92b569a3b4c0d7022f7450efee8 Mon Sep 17 00:00:00 2001 From: Kaif Shaikh Date: Fri, 13 Feb 2026 11:37:49 +0530 Subject: [PATCH 1/4] bump gradle plugin version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 06ff920d5..69855fca3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ buildscript { classpath("com.android.tools.build:gradle:8.7.3") // Cloudstream gradle plugin which makes everything work and builds plugins classpath("com.github.recloudstream:gradle:-SNAPSHOT") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.0") } } From ee5faa2d1acc869a17f0c4274cdae4ba290ab625 Mon Sep 17 00:00:00 2001 From: Kaif Shaikh Date: Fri, 13 Feb 2026 11:38:29 +0530 Subject: [PATCH 2/4] include NewPipeExtractor --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index 69855fca3..fe27b89c4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -82,6 +82,7 @@ subprojects { // IMPORTANT: Do not bump Jackson above 2.13.1, as newer versions will // break compatibility on older Android devices. implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") // JSON Parser + implementation("com.github.teamnewpipe:NewPipeExtractor:v0.25.2") // NewPipe Extractor } } From a8cabdf5ccd4af59fca2bc6a741f4b1baaba5684 Mon Sep 17 00:00:00 2001 From: Kaif Shaikh Date: Fri, 13 Feb 2026 11:38:52 +0530 Subject: [PATCH 3/4] Add YoutubeProvider --- YoutubeProvider/build.gradle.kts | 23 ++ .../kotlin/recloudstream/YoutubePlugin.kt | 11 + .../kotlin/recloudstream/YoutubeProvider.kt | 215 ++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 YoutubeProvider/build.gradle.kts create mode 100644 YoutubeProvider/src/main/kotlin/recloudstream/YoutubePlugin.kt create mode 100644 YoutubeProvider/src/main/kotlin/recloudstream/YoutubeProvider.kt diff --git a/YoutubeProvider/build.gradle.kts b/YoutubeProvider/build.gradle.kts new file mode 100644 index 000000000..e14550015 --- /dev/null +++ b/YoutubeProvider/build.gradle.kts @@ -0,0 +1,23 @@ +// Use an integer for version numbers +version = 1 + +cloudstream { + // All of these properties are optional, you can safely remove any of them. + + description = "Watch Youtube in Cloudstream" + authors = listOf("KaifTaufiq") + + /** + * Status int as one of the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta-only + **/ + status = 1 // Will be 3 if unspecified + + tvTypes = listOf("Other", "Live", "TvSeries") + iconUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/09/YouTube_full-color_icon_%282017%29.svg/3840px-YouTube_full-color_icon_%282017%29.svg.png" + + isCrossPlatform = true +} \ No newline at end of file diff --git a/YoutubeProvider/src/main/kotlin/recloudstream/YoutubePlugin.kt b/YoutubeProvider/src/main/kotlin/recloudstream/YoutubePlugin.kt new file mode 100644 index 000000000..ac390bf9e --- /dev/null +++ b/YoutubeProvider/src/main/kotlin/recloudstream/YoutubePlugin.kt @@ -0,0 +1,11 @@ +package recloudstream + +import com.lagradost.cloudstream3.plugins.BasePlugin +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin + +@CloudstreamPlugin +class YoutubePlugin : BasePlugin() { + override fun load() { + registerMainAPI(YoutubeProvider()) + } +} \ No newline at end of file diff --git a/YoutubeProvider/src/main/kotlin/recloudstream/YoutubeProvider.kt b/YoutubeProvider/src/main/kotlin/recloudstream/YoutubeProvider.kt new file mode 100644 index 000000000..e3160d4fd --- /dev/null +++ b/YoutubeProvider/src/main/kotlin/recloudstream/YoutubeProvider.kt @@ -0,0 +1,215 @@ +package recloudstream + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import org.schabi.newpipe.extractor.ServiceList +import org.schabi.newpipe.extractor.kiosk.KioskExtractor +import org.schabi.newpipe.extractor.InfoItem +import org.schabi.newpipe.extractor.localization.ContentCountry +import org.schabi.newpipe.extractor.stream.StreamInfo +import java.util.Locale + +class YoutubeProvider : MainAPI() { + override var mainUrl = "https://www.youtube.com" + override var name = "YouTube" + override var lang = "en" + override val hasMainPage = true + override val hasQuickSearch = true + override val supportedTypes = setOf( + TvType.Others, + TvType.Live, + TvType.TvSeries + ) + + override val mainPage = mainPageOf( + "live" to "Live", + "trending_podcasts_episodes" to "Podcasts", + "trending_gaming" to "Gaming", + "trending_music" to "Music", + "trending_movies_and_shows" to "Movies & TV" + ) + + private val service = ServiceList.YouTube + + private val pageCache = mutableMapOf() + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val key = request.data + if (page == 1) pageCache.remove(key) + + val extractor = getKioskExtractor(request.data) + + extractor.forceContentCountry(ContentCountry(Locale.getDefault().country)) + + val pageData = if (page == 1) { + extractor.fetchPage() + extractor.initialPage.also { + pageCache[key] = it.nextPage + } + } else { + val next = pageCache[key] ?: return newHomePageResponse(emptyList(), false) + extractor.getPage(next).also { + pageCache[key] = it.nextPage + } + } + + val results = pageData.items.map { + it.toSearchResponse() + } + + return newHomePageResponse( + listOf( + HomePageList( + request.name.ifEmpty { "Trending" }, + results, + true + ) + ), + pageData.hasNextPage() + ) + } + + private val searchPageCache = mutableMapOf() + override suspend fun search(query: String, page: Int): SearchResponseList { + val extractor = service.getSearchExtractor(query) + extractor.forceContentCountry(ContentCountry(Locale.getDefault().country)) + + val pageData = if (!searchPageCache.containsKey(query)) { + extractor.fetchPage() + extractor.initialPage.also { + searchPageCache[query] = it.nextPage + } + } else { + val next = searchPageCache[query] ?: return newSearchResponseList(emptyList(), false) + extractor.getPage(next).also { + searchPageCache[query] = it.nextPage + } + } + + val results = pageData.items.map { + it.toSearchResponse() + } + + return newSearchResponseList( + results, + pageData.hasNextPage() + ) + } + + private fun getKioskExtractor(kioskId: String?): KioskExtractor { + val service = ServiceList.YouTube + return if (kioskId.isNullOrBlank()) { + service.kioskList.getDefaultKioskExtractor(null) + } else { + service.kioskList.getExtractorById(kioskId, null) + } + } + + private fun InfoItem.toSearchResponse(): SearchResponse { + return newMovieSearchResponse( + name ?: "Unknown", + url ?: "", + TvType.Others + ) { + posterUrl = thumbnails.lastOrNull()?.url + } + } + + override suspend fun load(url: String): LoadResponse { + val videoId = extractVideoId(url) + ?: throw RuntimeException("Invalid YouTube URL") + + val extractor = ServiceList.YouTube.getStreamExtractor(url) + extractor.fetchPage() + + val info = StreamInfo.getInfo(extractor) + + return newMovieLoadResponse( + info.name, + url, + if (info.streamType?.name?.contains("LIVE") == true) + TvType.Live else TvType.Others, + videoId + ) { + plot = info.description.content + posterUrl = info.thumbnails.lastOrNull()?.url + duration = info.duration.toInt() + + info.uploaderName?.takeIf { it.isNotBlank() }?.let { uploader -> + actors = listOf( + ActorData( + Actor( + uploader, + info.uploaderAvatars.lastOrNull()?.url ?: "" + ) + ) + ) + } + + tags = info.tags?.take(5)?.toList() + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + return loadExtractor( + "https://youtube.com/watch?v=$data", + subtitleCallback, + callback + ) + } + + private fun extractVideoId(url: String): String? { + val patterns = listOf( + "(?:youtube\\.com/watch\\?v=|youtu\\.be/|youtube\\.com/embed/)([a-zA-Z0-9_-]{11})", + "v=([a-zA-Z0-9_-]{11})" + ) + + return patterns.firstNotNullOfOrNull { + it.toRegex().find(url)?.groupValues?.get(1) + } + } +} + +// class YoutubeProviderDownloader : Downloader() { + +// private val client = okhttp3.OkHttpClient.Builder() +// .followRedirects(true) +// .followSslRedirects(true) +// .build() + +// override fun execute(request: Request): Response { + +// val data = request.dataToSend() +// val body = data?.toRequestBody(null, 0, data.size) + +// val builder = okhttp3.Request.Builder() +// .url(request.url()) +// .method(request.httpMethod(), body) +// .header( +// "User-Agent", +// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + +// "(HTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" +// ) + +// request.headers().forEach { (key, values) -> +// values.forEach { builder.addHeader(key, it) } +// } + +// val response = client.newCall(builder.build()).execute() + +// return Response( +// response.code, +// response.message, +// response.headers.toMultimap(), +// response.body?.string().orEmpty(), +// response.request.url.toString() +// ) +// } +// } \ No newline at end of file From b05865c0c430942ca8074e538c77bed92beb79a1 Mon Sep 17 00:00:00 2001 From: Kaif Shaikh Date: Fri, 13 Feb 2026 11:43:38 +0530 Subject: [PATCH 4/4] remove unwanted leftover Code --- .../kotlin/recloudstream/YoutubeProvider.kt | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/YoutubeProvider/src/main/kotlin/recloudstream/YoutubeProvider.kt b/YoutubeProvider/src/main/kotlin/recloudstream/YoutubeProvider.kt index e3160d4fd..c2633335f 100644 --- a/YoutubeProvider/src/main/kotlin/recloudstream/YoutubeProvider.kt +++ b/YoutubeProvider/src/main/kotlin/recloudstream/YoutubeProvider.kt @@ -175,41 +175,4 @@ class YoutubeProvider : MainAPI() { it.toRegex().find(url)?.groupValues?.get(1) } } -} - -// class YoutubeProviderDownloader : Downloader() { - -// private val client = okhttp3.OkHttpClient.Builder() -// .followRedirects(true) -// .followSslRedirects(true) -// .build() - -// override fun execute(request: Request): Response { - -// val data = request.dataToSend() -// val body = data?.toRequestBody(null, 0, data.size) - -// val builder = okhttp3.Request.Builder() -// .url(request.url()) -// .method(request.httpMethod(), body) -// .header( -// "User-Agent", -// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + -// "(HTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" -// ) - -// request.headers().forEach { (key, values) -> -// values.forEach { builder.addHeader(key, it) } -// } - -// val response = client.newCall(builder.build()).execute() - -// return Response( -// response.code, -// response.message, -// response.headers.toMultimap(), -// response.body?.string().orEmpty(), -// response.request.url.toString() -// ) -// } -// } \ No newline at end of file +} \ No newline at end of file