Skip to content

Commit 0f0e55c

Browse files
test
1 parent c36f0d6 commit 0f0e55c

1 file changed

Lines changed: 157 additions & 93 deletions

File tree

  • DubbindoProvider/src/main/kotlin/com/dubbindo

DubbindoProvider/src/main/kotlin/com/dubbindo/Dubbindo.kt

Lines changed: 157 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.utils.INFER_TYPE
88
import com.lagradost.cloudstream3.utils.Qualities
99
import com.lagradost.cloudstream3.utils.loadExtractor
1010
import com.lagradost.cloudstream3.utils.newExtractorLink
11+
import org.jsoup.nodes.Document
1112
import org.jsoup.nodes.Element
1213

1314
class 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

Comments
 (0)