Skip to content

Commit d923c16

Browse files
test
1 parent 783492b commit d923c16

4 files changed

Lines changed: 259 additions & 0 deletions

File tree

DubbindoProvider/build.gradle.kts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// use an integer for version numbers
2+
version = 1
3+
4+
cloudstream {
5+
language = "id"
6+
// All of these properties are optional, you can safely remove them
7+
8+
// description = "Lorem Ipsum"
9+
authors = listOf("Miku")
10+
11+
/**
12+
* Status int as the following:
13+
* 0: Down
14+
* 1: Ok
15+
* 2: Slow
16+
* 3: Beta only
17+
* */
18+
status = 1 // will be 3 if unspecified
19+
tvTypes = listOf(
20+
"TvSeries",
21+
"Movie",
22+
"Cartoon",
23+
"Anime",
24+
)
25+
26+
iconUrl = "https://www.google.com/s2/favicons?domain=www.dubbindo.xyz&sz=%size%"
27+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest package="com.dubbindo"/>
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package com.dubbindo
2+
3+
import com.lagradost.cloudstream3.*
4+
import com.lagradost.cloudstream3.utils.AppUtils.toJson
5+
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
6+
import com.lagradost.cloudstream3.utils.ExtractorLink
7+
import com.lagradost.cloudstream3.utils.INFER_TYPE
8+
import com.lagradost.cloudstream3.utils.Qualities
9+
import com.lagradost.cloudstream3.utils.loadExtractor
10+
import com.lagradost.cloudstream3.utils.newExtractorLink
11+
import org.jsoup.nodes.Element
12+
13+
class Dubbindo : MainAPI() {
14+
override var mainUrl = "https://www.dubbindo.site"
15+
override var name = "Dubbindo"
16+
override val hasMainPage = true
17+
override var lang = "id"
18+
override val hasDownloadSupport = true
19+
20+
override val supportedTypes = setOf(
21+
TvType.TvSeries,
22+
TvType.Movie,
23+
TvType.Cartoon,
24+
TvType.Anime,
25+
)
26+
27+
override val mainPage = mainPageOf(
28+
"$mainUrl/videos/category/1" to "Movie",
29+
"$mainUrl/videos/category/3" to "TV Series",
30+
"$mainUrl/videos/category/5" to "Anime Series",
31+
"$mainUrl/videos/category/4" to "Anime Movie",
32+
"$mainUrl/videos/category/other" to "Other",
33+
)
34+
35+
override suspend fun getMainPage(
36+
page: Int,
37+
request: MainPageRequest
38+
): HomePageResponse {
39+
val document = app.get("${request.data}?page_id=$page").document
40+
41+
// FIX: Selector diperbarui sesuai struktur HTML aktual website.
42+
// Website menggunakan div.video-list sebagai container kartu video,
43+
// bukan div.video-wrapper seperti sebelumnya.
44+
val home = document.select("div.video-list")
45+
.mapNotNull {
46+
it.toSearchResult()
47+
}
48+
return newHomePageResponse(
49+
list = HomePageList(
50+
name = request.name,
51+
list = home,
52+
isHorizontalImages = true
53+
),
54+
hasNext = true
55+
)
56+
}
57+
58+
private fun Element.toSearchResult(): TvSeriesSearchResponse? {
59+
// FIX: Selector title disesuaikan dengan struktur HTML baru.
60+
// Judul ada di dalam div.video-list-title > a > h4
61+
val title = this.selectFirst("div.video-list-title h4, h4")?.text()?.trim() ?: return null
62+
// Ambil href dari link gambar/thumbnail (link pertama di dalam div.video-list-image)
63+
val href = this.selectFirst("div.video-list-image a")?.attr("href")
64+
?: this.selectFirst("a")?.attr("href")
65+
?: return null
66+
val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src"))
67+
return newTvSeriesSearchResponse(title, href, TvType.TvSeries) {
68+
this.posterUrl = posterUrl
69+
}
70+
}
71+
72+
override suspend fun search(query: String): List<SearchResponse> {
73+
val searchResponse = mutableListOf<SearchResponse>()
74+
for (i in 1..10) {
75+
val document =
76+
app.get(
77+
"$mainUrl/search?keyword=$query&page_id=$i",
78+
).document
79+
80+
// FIX: Selector search juga diperbarui ke div.video-list
81+
val results = document.select("div.video-list")
82+
.mapNotNull {
83+
it.toSearchResult()
84+
}
85+
searchResponse.addAll(results)
86+
if (results.isEmpty()) break
87+
}
88+
return searchResponse
89+
}
90+
91+
override suspend fun load(url: String): LoadResponse? {
92+
val document = app.get(url).document
93+
94+
// FIX: Ambil title dari meta tag agar bisa dipakai di kedua jenis halaman,
95+
// lalu bersihkan nama site-nya.
96+
val title = (document.selectFirst("meta[name=title]")?.attr("content")
97+
?: document.title())
98+
.replace(" | UVideo", "").trim()
99+
if (title.isEmpty()) return null
100+
101+
val poster = document.selectFirst("meta[property=og:image]")?.attr("content")
102+
val tags = document.select("div.pt_categories li a").map { it.text() }
103+
104+
// FIX: Tangani dua jenis halaman detail:
105+
// 1. /articles/read/... → link video ada di div.read-article-text a
106+
// 2. /watch/... → sumber video ada di video#my-video source
107+
return if (url.contains("/articles/read/")) {
108+
// --- Halaman Artikel ---
109+
val description = document.selectFirst("div.read-article-description article")?.text()
110+
111+
// Kumpulkan semua link video/extractor dari body artikel
112+
val videoLinks = document.select("div.read-article-text a")
113+
.map { it.attr("href") }
114+
.filter { it.isNotBlank() }
115+
116+
// Related videos di artikel menggunakan div.related-video-wrapper
117+
val recommendations = document.select("div.related-video-wrapper").mapNotNull {
118+
it.toRelatedResult()
119+
}
120+
121+
newMovieLoadResponse(title, url, TvType.Movie, videoLinks.toJson()) {
122+
posterUrl = poster
123+
plot = description
124+
this.tags = tags
125+
this.recommendations = recommendations
126+
}
127+
} else {
128+
// --- Halaman Watch ---
129+
val description = document.select("div.watch-video-description p").text().replace("\u2063", "").trim()
130+
131+
val recommendations = document.select("div.related-video-wrapper").mapNotNull {
132+
it.toRelatedResult()
133+
}
134+
135+
val video = document.select("video#my-video source").map {
136+
Video(
137+
it.attr("src"),
138+
it.attr("res"), // FIX: was "size" — atribut di HTML adalah "res"
139+
it.attr("type"),
140+
)
141+
}
142+
143+
newMovieLoadResponse(title, url, TvType.Movie, video.toJson()) {
144+
posterUrl = poster
145+
plot = description
146+
this.tags = tags
147+
this.recommendations = recommendations
148+
}
149+
}
150+
}
151+
152+
// FIX: Fungsi khusus untuk mengambil related video dari div.related-video-wrapper
153+
// (struktur berbeda dari div.video-list di halaman utama)
154+
private fun Element.toRelatedResult(): TvSeriesSearchResponse? {
155+
val title = this.selectFirst("div.video-title a")?.text()?.trim()
156+
?: this.selectFirst("a")?.text()?.trim()
157+
?: return null
158+
val href = this.selectFirst("div.ra-thumb a")?.attr("href")
159+
?: this.selectFirst("a")?.attr("href")
160+
?: return null
161+
val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src"))
162+
return newTvSeriesSearchResponse(title, href, TvType.TvSeries) {
163+
this.posterUrl = posterUrl
164+
}
165+
}
166+
167+
override suspend fun loadLinks(
168+
data: String,
169+
isCasting: Boolean,
170+
subtitleCallback: (SubtitleFile) -> Unit,
171+
callback: (ExtractorLink) -> Unit
172+
): Boolean {
173+
174+
// FIX: Coba parse sebagai List<Video> dulu (halaman watch)
175+
val videos = tryParseJson<List<Video>>(data)
176+
if (videos != null) {
177+
videos.map { video ->
178+
if (video.type == "video/mp4" || video.type == "video/x-msvideo" || video.type == "video/x-matroska") {
179+
callback.invoke(
180+
newExtractorLink(
181+
this.name,
182+
this.name,
183+
video.src ?: return@map,
184+
INFER_TYPE
185+
) {
186+
this.quality = video.res?.toIntOrNull() ?: Qualities.Unknown.value
187+
}
188+
)
189+
} else {
190+
loadExtractor(video.src ?: return@map, "", subtitleCallback, callback)
191+
}
192+
}
193+
return true
194+
}
195+
196+
// FIX: Jika bukan List<Video>, coba parse sebagai List<String>
197+
// (halaman artikel dengan link extractor langsung)
198+
val urls = tryParseJson<List<String>>(data)
199+
if (urls != null) {
200+
urls.forEach { videoUrl ->
201+
if (videoUrl.isNotBlank()) {
202+
loadExtractor(videoUrl, "", subtitleCallback, callback)
203+
}
204+
}
205+
return true
206+
}
207+
208+
return false
209+
}
210+
211+
data class Video(
212+
val src: String? = null,
213+
val res: String? = null,
214+
val type: String? = null,
215+
)
216+
217+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.dubbindo
2+
3+
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
4+
import com.lagradost.cloudstream3.plugins.Plugin
5+
import android.content.Context
6+
7+
@CloudstreamPlugin
8+
class DubbindoPlugin: Plugin() {
9+
override fun load(context: Context) {
10+
// All providers should be added in this manner. Please don't edit the providers list directly.
11+
registerMainAPI(Dubbindo())
12+
}
13+
}

0 commit comments

Comments
 (0)