@@ -32,9 +32,6 @@ class Dubbindo : MainAPI() {
3232 // ── Session ───────────────────────────────────────────────────────────────
3333 private var sessionCookie = " "
3434
35- // Track channel ID yang sudah disubscribe agar tidak kirim request berulang
36- private val subscribedChannels = mutableSetOf<String >()
37-
3835 private val baseHeaders get() = mapOf (
3936 " User-Agent" to " Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 Chrome/124.0 Mobile Safari/537.36" ,
4037 " Referer" to " $mainUrl /"
@@ -55,7 +52,7 @@ class Dubbindo : MainAPI() {
5552
5653 private suspend fun doLogin (): Boolean {
5754 // Step 1 – GET /login untuk PHPSESSID awal
58- val getResp = app.get(" $mainUrl /login" , headers = baseHeaders)
55+ val getResp = app.get(" $mainUrl /login" , headers = baseHeaders)
5956 val initCookies = getResp.headers
6057 .filter { it.first.equals(" set-cookie" , ignoreCase = true ) }
6158 .mapNotNull { parseCookiePair(it.second) }
@@ -96,35 +93,43 @@ class Dubbindo : MainAPI() {
9693 doLogin()
9794 }
9895
99- // ── Auto-subscribe ────────────────────────────────────────────────────────
96+ // ── Subscribe ───── ────────────────────────────────────────────────────────
10097
10198 /* *
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">
99+ * Ambil channel ID dari halaman, lalu POST ke /aj/subscribe.
100+ * Mengambil main_session dari halaman untuk header Referer yang benar.
101+ * Return true jika berhasil menemukan channel ID dan mengirim request.
107102 */
108- private suspend fun autoSubscribe (document : Document ) {
109- val channelId = document.selectFirst(" button.btn-subscribe[data-id]" )
103+ private suspend fun subscribeChannel (document : Document , pageUrl : String ): Boolean {
104+ // Cari channel ID dari tombol subscribe di halaman watch/channel
105+ val channelId = document
106+ .selectFirst(" button.btn-subscribe[data-id], .subscribe-btn-container button[data-id]" )
110107 ?.attr(" data-id" )?.trim()
111- ? : document.selectFirst(" input#profile-id" )
112- ?.attr(" value" )?.trim()
113- ? : return
108+ ? : document.selectFirst(" input#profile-id" )?.attr(" value" )?.trim()
109+ ? : return false
114110
115- if (channelId.isBlank() || subscribedChannels.contains(channelId)) return
111+ if (channelId.isBlank()) return false
116112
117- app.post(
113+ val resp = app.post(
118114 " $mainUrl /aj/subscribe" ,
119115 data = mapOf (" user_id" to channelId),
120116 headers = authedHeaders + mapOf (
121- " Content-Type" to " application/x-www-form-urlencoded" ,
117+ " Content-Type" to " application/x-www-form-urlencoded" ,
122118 " X-Requested-With" to " XMLHttpRequest" ,
123- " Referer" to " $mainUrl /"
119+ " Referer" to pageUrl,
120+ " Origin" to mainUrl
124121 )
125122 )
126123
127- subscribedChannels.add(channelId)
124+ return resp.isSuccessful
125+ }
126+
127+ /* * Deteksi apakah halaman masih terkunci (subscribe wall). */
128+ private fun isSubscribeWall (document : Document ): Boolean {
129+ val playerArea = document.selectFirst(" div.video-processing, div.video-player" )
130+ ?.text().orEmpty()
131+ return playerArea.contains(" subscribe to watch" , ignoreCase = true ) ||
132+ document.select(" video#my-video source, video source" ).isEmpty()
128133 }
129134
130135 // ── Main page ─────────────────────────────────────────────────────────────
@@ -153,7 +158,7 @@ class Dubbindo : MainAPI() {
153158 val href = selectFirst(" div.video-thumb a" )?.attr(" href" )
154159 ? : selectFirst(" a" )?.attr(" href" ) ? : return null
155160 return newTvSeriesSearchResponse(title, href, TvType .TvSeries ) {
156- posterUrl = fixUrlNull(selectFirst(" div.video-thumb img" )?.attr(" src" ))
161+ posterUrl = fixUrlNull(selectFirst(" div.video-thumb img" )?.attr(" src" ))
157162 posterHeaders = mapOf (" Referer" to mainUrl)
158163 }
159164 }
@@ -165,7 +170,7 @@ class Dubbindo : MainAPI() {
165170 val href = selectFirst(" div.video-list-image a" )?.attr(" href" )
166171 ? : selectFirst(" a" )?.attr(" href" ) ? : return null
167172 return newTvSeriesSearchResponse(title, href, TvType .TvSeries ) {
168- posterUrl = fixUrlNull(selectFirst(" img" )?.attr(" src" ))
173+ posterUrl = fixUrlNull(selectFirst(" img" )?.attr(" src" ))
169174 posterHeaders = mapOf (" Referer" to mainUrl)
170175 }
171176 }
@@ -176,7 +181,7 @@ class Dubbindo : MainAPI() {
176181 val href = selectFirst(" div.ra-thumb a" )?.attr(" href" )
177182 ? : selectFirst(" a" )?.attr(" href" ) ? : return null
178183 return newTvSeriesSearchResponse(title, href, TvType .TvSeries ) {
179- posterUrl = fixUrlNull(selectFirst(" img" )?.attr(" src" ))
184+ posterUrl = fixUrlNull(selectFirst(" img" )?.attr(" src" ))
180185 posterHeaders = mapOf (" Referer" to mainUrl)
181186 }
182187 }
@@ -198,7 +203,6 @@ class Dubbindo : MainAPI() {
198203
199204 // ── Load ──────────────────────────────────────────────────────────────────
200205
201- /* * Ekstrak semua <source> dari elemen video di halaman. */
202206 private fun parseVideoSources (doc : Document ): List <Video > =
203207 doc.select(" video#my-video source, video source" ).mapNotNull { el ->
204208 val src = el.attr(" src" ).trim().ifEmpty { return @mapNotNull null }
@@ -209,10 +213,49 @@ class Dubbindo : MainAPI() {
209213 )
210214 }
211215
216+ /* *
217+ * Ambil video sources dari URL, dengan logika:
218+ * 1. Load halaman
219+ * 2. Jika kena subscribe wall → subscribe → reload
220+ * 3. Jika masih wall → sesi expired → login ulang → subscribe → reload
221+ */
222+ private suspend fun fetchVideoSources (url : String ): Pair <Document , List <Video >> {
223+ var doc = app.get(url, headers = authedHeaders).document
224+ var videos = parseVideoSources(doc)
225+
226+ if (videos.isNotEmpty()) return doc to videos
227+
228+ // Kena subscribe wall — coba subscribe lalu reload
229+ if (isSubscribeWall(doc)) {
230+ subscribeChannel(doc, url)
231+ doc = app.get(url, headers = authedHeaders).document
232+ videos = parseVideoSources(doc)
233+ }
234+
235+ if (videos.isNotEmpty()) return doc to videos
236+
237+ // Masih kosong — mungkin sesi expired, login ulang
238+ sessionCookie = " "
239+ if (doLogin()) {
240+ // Muat ulang halaman dengan sesi baru
241+ doc = app.get(url, headers = authedHeaders).document
242+ videos = parseVideoSources(doc)
243+
244+ // Masih kena wall setelah re-login → subscribe ulang dengan sesi baru
245+ if (videos.isEmpty() && isSubscribeWall(doc)) {
246+ subscribeChannel(doc, url)
247+ doc = app.get(url, headers = authedHeaders).document
248+ videos = parseVideoSources(doc)
249+ }
250+ }
251+
252+ return doc to videos
253+ }
254+
212255 override suspend fun load (url : String ): LoadResponse ? {
213256 ensureSession()
214257
215- var document = app.get (url, headers = authedHeaders).document
258+ val ( document, _) = fetchVideoSources (url). let { it } // load awal
216259
217260 val title = (document.selectFirst(" meta[name=title]" )?.attr(" content" )
218261 ? : document.title()).replace(" | UVideo" , " " ).trim()
@@ -222,8 +265,8 @@ class Dubbindo : MainAPI() {
222265 val tags = document.select(" div.pt_categories li a" ).map { it.text() }
223266
224267 return if (url.contains(" /articles/read/" )) {
225- val description = document.selectFirst(" div.read-article-description article" )?.text()
226- val videoLinks = document.select(" div.read-article-text a" )
268+ val description = document.selectFirst(" div.read-article-description article" )?.text()
269+ val videoLinks = document.select(" div.read-article-text a" )
227270 .map { it.attr(" href" ) }.filter { it.isNotBlank() }
228271 val recommendations = document.select(" div.related-video-wrapper" )
229272 .mapNotNull { it.toRelatedResult() }
@@ -237,25 +280,8 @@ class Dubbindo : MainAPI() {
237280 val recommendations = document.select(" div.related-video-wrapper" )
238281 .mapNotNull { it.toRelatedResult() }
239282
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)
247-
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- }
257- }
258- }
283+ // Jalankan fetchVideoSources untuk dapat dokumen + sources final
284+ val (_, videos) = fetchVideoSources(url)
259285
260286 newMovieLoadResponse(title, url, TvType .Movie , videos.toJson()) {
261287 posterUrl = poster; plot = description
0 commit comments