@@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.utils.INFER_TYPE
88import com.lagradost.cloudstream3.utils.Qualities
99import com.lagradost.cloudstream3.utils.loadExtractor
1010import com.lagradost.cloudstream3.utils.newExtractorLink
11+ import org.jsoup.nodes.Document
1112import org.jsoup.nodes.Element
1213
1314class Dubbindo : MainAPI () {
@@ -24,30 +25,108 @@ class Dubbindo : MainAPI() {
2425 TvType .Anime ,
2526 )
2627
27- // ── Cookies sesi ────────────────────────────────────────────────────────
28- // Video source ada langsung di watch page saat request dengan cookie valid.
29- // Tidak perlu embed iframe sama sekali.
30- private val sessionCookies = mapOf (
31- " user_id" to " f87445d728dc4569f8795891589260aeefc8df0c1773168951fc41dd6bf552a442846634d941ead607" ,
32- " next_up_videos" to " %5B%2227969%22%2C%22142%22%2C%2227988%22%2C%2227943%22%2C%2227976%22%5D" ,
33- " v_shorts" to " %5B%22bkviD7Qw7omhVRD%22%5D" ,
34- " r" to " c3RyaW5n" ,
35- " mode" to " night" ,
36- " _uads" to " a%3A2%3A%7Bs%3A4%3A%26quot%3Bdate%26quot%3B%3Bi%3A1773242984%3Bs%3A5%3A%26quot%3Buaid_%26quot%3B%3Ba%3A0%3A%7B%7D%7D" ,
37- " auto" to " " ,
38- " PHPSESSID" to " t52rria3q613o666lt9g4dtism" ,
28+ // ── Kredensial ────────────────────────────────────────────────────────────
29+ private val USERNAME = " HatsuneMikuUwU"
30+ private val PASSWORD = " weeabooifyprojects123456789"
31+
32+ // ── Session ───────────────────────────────────────────────────────────────
33+ private var sessionCookie = " "
34+
35+ // Track channel ID yang sudah disubscribe agar tidak kirim request berulang
36+ private val subscribedChannels = mutableSetOf<String >()
37+
38+ private val baseHeaders get() = mapOf (
39+ " User-Agent" to " Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 Chrome/124.0 Mobile Safari/537.36" ,
40+ " Referer" to " $mainUrl /"
3941 )
4042
41- private val cookieHeader: String
42- get() = sessionCookies.entries.joinToString(" ; " ) { " ${it.key} =${it.value} " }
43+ private val authedHeaders get() = if (sessionCookie.isNotBlank())
44+ baseHeaders + mapOf (" Cookie" to sessionCookie)
45+ else baseHeaders
46+
47+ // ── Login ─────────────────────────────────────────────────────────────────
48+
49+ private fun parseCookiePair (header : String ): Pair <String , String >? {
50+ val part = header.split(" ;" ).firstOrNull()?.trim() ? : return null
51+ val eq = part.indexOf(' =' )
52+ if (eq < 0 ) return null
53+ return part.substring(0 , eq).trim() to part.substring(eq + 1 ).trim()
54+ }
55+
56+ private suspend fun doLogin (): Boolean {
57+ // Step 1 – GET /login untuk PHPSESSID awal
58+ val getResp = app.get(" $mainUrl /login" , headers = baseHeaders)
59+ val initCookies = getResp.headers
60+ .filter { it.first.equals(" set-cookie" , ignoreCase = true ) }
61+ .mapNotNull { parseCookiePair(it.second) }
62+ .toMap().toMutableMap()
63+
64+ val phpSessId = initCookies[" PHPSESSID" ].orEmpty()
65+
66+ // Step 2 – POST kredensial
67+ val postResp = app.post(
68+ " $mainUrl /login" ,
69+ data = mapOf (
70+ " username" to USERNAME ,
71+ " password" to PASSWORD ,
72+ " remember_device" to " on"
73+ ),
74+ headers = baseHeaders + mapOf (
75+ " Cookie" to if (phpSessId.isNotBlank()) " PHPSESSID=$phpSessId " else " " ,
76+ " Content-Type" to " application/x-www-form-urlencoded" ,
77+ " Origin" to mainUrl,
78+ " Referer" to " $mainUrl /login"
79+ ),
80+ allowRedirects = false
81+ )
82+
83+ val allCookies = initCookies + postResp.headers
84+ .filter { it.first.equals(" set-cookie" , ignoreCase = true ) }
85+ .mapNotNull { parseCookiePair(it.second) }
86+ .toMap()
87+
88+ return if (! allCookies[" user_id" ].isNullOrBlank()) {
89+ sessionCookie = allCookies.entries.joinToString(" ; " ) { " ${it.key} =${it.value} " }
90+ true
91+ } else false
92+ }
93+
94+ private suspend fun ensureSession () {
95+ if (sessionCookie.isNotBlank()) return
96+ doLogin()
97+ }
98+
99+ // ── Auto-subscribe ────────────────────────────────────────────────────────
100+
101+ /* *
102+ * Cek apakah ada tombol Subscribe di halaman, lalu POST ke /aj/subscribe.
103+ * Dipanggil setiap load() agar video dari channel berlangganan bisa diakses.
104+ *
105+ * Selector: <button class="btn-subscribe" data-id="CHANNEL_ID">
106+ * atau: <input id="profile-id" value="CHANNEL_ID">
107+ */
108+ private suspend fun autoSubscribe (document : Document ) {
109+ val channelId = document.selectFirst(" button.btn-subscribe[data-id]" )
110+ ?.attr(" data-id" )?.trim()
111+ ? : document.selectFirst(" input#profile-id" )
112+ ?.attr(" value" )?.trim()
113+ ? : return
43114
44- private val defaultHeaders
45- get() = mapOf (
46- " Cookie" to cookieHeader,
47- " Referer" to " $mainUrl /" ,
48- " User-Agent" to " Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 Chrome/124.0 Mobile Safari/537.36"
115+ if (channelId.isBlank() || subscribedChannels.contains(channelId)) return
116+
117+ app.post(
118+ " $mainUrl /aj/subscribe" ,
119+ data = mapOf (" user_id" to channelId),
120+ headers = authedHeaders + mapOf (
121+ " Content-Type" to " application/x-www-form-urlencoded" ,
122+ " X-Requested-With" to " XMLHttpRequest" ,
123+ " Referer" to " $mainUrl /"
124+ )
49125 )
50126
127+ subscribedChannels.add(channelId)
128+ }
129+
51130 // ── Main page ─────────────────────────────────────────────────────────────
52131 override val mainPage = mainPageOf(
53132 " $mainUrl /videos/category/1" to " Movie" ,
@@ -58,24 +137,24 @@ class Dubbindo : MainAPI() {
58137 )
59138
60139 override suspend fun getMainPage (page : Int , request : MainPageRequest ): HomePageResponse {
61- val document = app.get(" ${request.data} ?page_id=$page " , headers = defaultHeaders).document
140+ ensureSession()
141+ val document = app.get(" ${request.data} ?page_id=$page " , headers = authedHeaders).document
62142 val home = document.select(" div.video-wrapper" ).mapNotNull { it.toCategoryResult() }
63143 return newHomePageResponse(
64144 list = HomePageList (name = request.name, list = home, isHorizontalImages = true ),
65145 hasNext = home.isNotEmpty()
66146 )
67147 }
68148
69- // ── Helpers ──────── ───────────────────────────────────────────────────────
149+ // ── Element helpers ───────────────────────────────────────────────────────
70150 private fun Element.toCategoryResult (): TvSeriesSearchResponse ? {
71151 val title = selectFirst(" div.video-title h4" )?.text()?.trim() ? : return null
72152 if (title.isEmpty()) return null
73153 val href = selectFirst(" div.video-thumb a" )?.attr(" href" )
74154 ? : selectFirst(" a" )?.attr(" href" ) ? : return null
75- val posterUrl = fixUrlNull(selectFirst(" div.video-thumb img" )?.attr(" src" ))
76155 return newTvSeriesSearchResponse(title, href, TvType .TvSeries ) {
77- this . posterUrl = posterUrl
78- this . posterHeaders = mapOf (" Referer" to mainUrl)
156+ posterUrl = fixUrlNull(selectFirst( " div.video-thumb img " )?.attr( " src " ))
157+ posterHeaders = mapOf (" Referer" to mainUrl)
79158 }
80159 }
81160
@@ -85,10 +164,9 @@ class Dubbindo : MainAPI() {
85164 if (title.isEmpty()) return null
86165 val href = selectFirst(" div.video-list-image a" )?.attr(" href" )
87166 ? : selectFirst(" a" )?.attr(" href" ) ? : return null
88- val posterUrl = fixUrlNull(selectFirst(" img" )?.attr(" src" ))
89167 return newTvSeriesSearchResponse(title, href, TvType .TvSeries ) {
90- this . posterUrl = posterUrl
91- this . posterHeaders = mapOf (" Referer" to mainUrl)
168+ posterUrl = fixUrlNull(selectFirst( " img " )?.attr( " src " ))
169+ posterHeaders = mapOf (" Referer" to mainUrl)
92170 }
93171 }
94172
@@ -97,31 +175,44 @@ class Dubbindo : MainAPI() {
97175 ? : selectFirst(" a" )?.text()?.trim() ? : return null
98176 val href = selectFirst(" div.ra-thumb a" )?.attr(" href" )
99177 ? : selectFirst(" a" )?.attr(" href" ) ? : return null
100- val posterUrl = fixUrlNull(selectFirst(" img" )?.attr(" src" ))
101178 return newTvSeriesSearchResponse(title, href, TvType .TvSeries ) {
102- this . posterUrl = posterUrl
103- this . posterHeaders = mapOf (" Referer" to mainUrl)
179+ posterUrl = fixUrlNull(selectFirst( " img " )?.attr( " src " ))
180+ posterHeaders = mapOf (" Referer" to mainUrl)
104181 }
105182 }
106183
107184 // ── Search ────────────────────────────────────────────────────────────────
108185 override suspend fun search (query : String ): List <SearchResponse > {
186+ ensureSession()
109187 val results = mutableListOf<SearchResponse >()
110188 for (i in 1 .. 10 ) {
111- val doc = app.get(
189+ val page = app.get(
112190 " $mainUrl /search?keyword=$query &page_id=$i " ,
113- headers = defaultHeaders
114- ).document
115- val page = doc.select(" div.video-list" ).mapNotNull { it.toSearchResult() }
191+ headers = authedHeaders
192+ ).document.select(" div.video-list" ).mapNotNull { it.toSearchResult() }
116193 results.addAll(page)
117194 if (page.isEmpty()) break
118195 }
119196 return results
120197 }
121198
122199 // ── Load ──────────────────────────────────────────────────────────────────
200+
201+ /* * Ekstrak semua <source> dari elemen video di halaman. */
202+ private fun parseVideoSources (doc : Document ): List <Video > =
203+ doc.select(" video#my-video source, video source" ).mapNotNull { el ->
204+ val src = el.attr(" src" ).trim().ifEmpty { return @mapNotNull null }
205+ Video (
206+ src = src,
207+ res = el.attr(" res" ).ifBlank { el.attr(" data-quality" ).replace(Regex (" [^0-9]" ), " " ) },
208+ type = el.attr(" type" ).ifBlank { " video/mp4" }
209+ )
210+ }
211+
123212 override suspend fun load (url : String ): LoadResponse ? {
124- val document = app.get(url, headers = defaultHeaders).document
213+ ensureSession()
214+
215+ var document = app.get(url, headers = authedHeaders).document
125216
126217 val title = (document.selectFirst(" meta[name=title]" )?.attr(" content" )
127218 ? : document.title()).replace(" | UVideo" , " " ).trim()
@@ -131,55 +222,42 @@ class Dubbindo : MainAPI() {
131222 val tags = document.select(" div.pt_categories li a" ).map { it.text() }
132223
133224 return if (url.contains(" /articles/read/" )) {
134- // ── Halaman artikel ───────────────────────────────────────────────
135225 val description = document.selectFirst(" div.read-article-description article" )?.text()
136226 val videoLinks = document.select(" div.read-article-text a" )
137227 .map { it.attr(" href" ) }.filter { it.isNotBlank() }
138228 val recommendations = document.select(" div.related-video-wrapper" )
139229 .mapNotNull { it.toRelatedResult() }
140-
141230 newMovieLoadResponse(title, url, TvType .Movie , videoLinks.toJson()) {
142231 posterUrl = poster; plot = description
143232 this .tags = tags; this .recommendations = recommendations
144233 }
145234 } else {
146- // ── Halaman watch ─────────────────────────────────────────────────
147235 val description = document.select(" div.watch-video-description p" )
148236 .text().replace(" \u2063 " , " " ).trim()
149237 val recommendations = document.select(" div.related-video-wrapper" )
150238 .mapNotNull { it.toRelatedResult() }
151239
152- // Video source tersedia langsung di <video#my-video source> saat
153- // halaman di-request dengan cookie valid.
154- // Struktur: <source src="URL" res="720" type="video/mp4">
155- val videos = document.select(" video#my-video source" ).mapNotNull { el ->
156- val src = el.attr(" src" ).trim()
157- if (src.isEmpty()) return @mapNotNull null
158- Video (
159- src = src,
160- res = el.attr(" res" ).ifBlank { el.attr(" data-quality" ).replace(Regex (" [^0-9]" ), " " ) },
161- type = el.attr(" type" ).ifBlank { " video/mp4" }
162- )
163- }
240+ var videos = parseVideoSources(document)
241+
242+ if (videos.isEmpty()) {
243+ // Video kosong → subscribe ke channel lalu reload
244+ autoSubscribe(document)
245+ document = app.get(url, headers = authedHeaders).document
246+ videos = parseVideoSources(document)
164247
165- // Fallback 1: <video> lain (tanpa id spesifik)
166- val finalVideos = if (videos.isNotEmpty()) videos else {
167- document.select(" video source" ).mapNotNull { el ->
168- val src = el.attr(" src" ).trim()
169- if (src.isEmpty()) return @mapNotNull null
170- Video (src = src, res = el.attr(" res" ), type = el.attr(" type" ).ifBlank { " video/mp4" })
248+ // Masih kosong → sesi expired, login ulang
249+ if (videos.isEmpty()) {
250+ sessionCookie = " "
251+ if (doLogin()) {
252+ // Subscribe ulang setelah login baru
253+ autoSubscribe(document)
254+ document = app.get(url, headers = authedHeaders).document
255+ videos = parseVideoSources(document)
256+ }
171257 }
172258 }
173259
174- val dataJson = if (finalVideos.isNotEmpty()) finalVideos.toJson()
175- else {
176- // Fallback 2: jika kosong (belum login / wall), coba embed
177- val embedUrl = document.selectFirst(" iframe[src*=/embed/]" )?.attr(" src" )
178- ? : document.selectFirst(" iframe[src*=dubbindo]" )?.attr(" src" )
179- listOfNotNull(embedUrl).toJson()
180- }
181-
182- newMovieLoadResponse(title, url, TvType .Movie , dataJson) {
260+ newMovieLoadResponse(title, url, TvType .Movie , videos.toJson()) {
183261 posterUrl = poster; plot = description
184262 this .tags = tags; this .recommendations = recommendations
185263 }
@@ -193,45 +271,31 @@ class Dubbindo : MainAPI() {
193271 subtitleCallback : (SubtitleFile ) -> Unit ,
194272 callback : (ExtractorLink ) -> Unit
195273 ): Boolean {
196- val authedHeaders = mapOf (
197- " Cookie" to cookieHeader,
198- " Referer" to mainUrl
199- )
274+ val streamHeaders = authedHeaders + mapOf (" Referer" to mainUrl)
200275
201276 val videos = tryParseJson<List <Video >>(data)
202277 if (videos != null ) {
203278 videos.forEach { video ->
204279 val src = video.src ? : return @forEach
205- when {
206- src.endsWith(" .m3u8" ) || video.type == " application/x-mpegURL" ->
207- callback.invoke(
208- newExtractorLink(this .name, this .name, src, INFER_TYPE ) {
209- this .quality = video.res?.toIntOrNull() ? : Qualities .Unknown .value
210- this .headers = authedHeaders
211- }
212- )
213- video.type.orEmpty().startsWith(" video/" ) ->
214- callback.invoke(
215- newExtractorLink(this .name, this .name, src, INFER_TYPE ) {
216- this .quality = video.res?.toIntOrNull() ? : Qualities .Unknown .value
217- this .headers = authedHeaders
218- }
219- )
220- else ->
221- loadExtractor(src, mainUrl, subtitleCallback, callback)
280+ if (src.endsWith(" .m3u8" ) || video.type.orEmpty().startsWith(" video/" )
281+ || video.type == " application/x-mpegURL" ) {
282+ callback.invoke(
283+ newExtractorLink(name, name, src, INFER_TYPE ) {
284+ quality = video.res?.toIntOrNull() ? : Qualities .Unknown .value
285+ headers = streamHeaders
286+ }
287+ )
288+ } else {
289+ loadExtractor(src, mainUrl, subtitleCallback, callback)
222290 }
223291 }
224- return true
292+ return videos.isNotEmpty()
225293 }
226294
227- // Fallback: List<String> dari halaman artikel atau embed URL
228295 val urls = tryParseJson<List <String >>(data)
229296 if (urls != null ) {
230- urls.forEach { videoUrl ->
231- if (videoUrl.isNotBlank())
232- loadExtractor(videoUrl, mainUrl, subtitleCallback, callback)
233- }
234- return true
297+ urls.forEach { if (it.isNotBlank()) loadExtractor(it, mainUrl, subtitleCallback, callback) }
298+ return urls.isNotEmpty()
235299 }
236300
237301 return false
0 commit comments