package com.streamflixreborn.streamflix.extractors import androidx.media3.common.MimeTypes import com.tanasi.retrofit_jsoup.converter.JsoupConverterFactory import com.streamflixreborn.streamflix.models.Video import com.streamflixreborn.streamflix.utils.UserPreferences import okhttp3.ResponseBody import org.json.JSONObject import retrofit2.Response import retrofit2.Retrofit import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Url import java.net.URL import java.net.URLEncoder import kotlin.text.Charsets.UTF_8 open class VeevExtractor : Extractor() { override val name = "Veev" override val mainUrl = "https://veev.to" override val aliasUrls= listOf("https://veev.to", "https://kinoger.pw", "https://poophq.com", "https://doods.to") private val pattern = Regex("""(?://|\.)((?:veev|kinoger|poophq|doods)\.(?:to|pw|com))/(?:e|d)/([0-9a-zA-Z]+)""") companion object { const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0" } override suspend fun extract(link: String): Video { val match = pattern.find(link) ?: throw Exception("Invalid Veev URL") val host = match.destructured.component1() var mediaId = match.destructured.component2() val webUrl = "https://$host/e/$mediaId" val service = Service.build("https://$host") val referer = try { UserPreferences.currentProvider?.baseUrl ?: link } catch (_: Throwable) { link } val headers = mapOf( "User-Agent" to DEFAULT_USER_AGENT, "Referer" to referer, "Origin" to referer ) val response = service.getWithHeaders(webUrl, referer, DEFAULT_USER_AGENT, webUrl) val finalUrl = response.raw().request.url.toString() pattern.find(finalUrl)?.let { val newMediaId = it.destructured.component2() if (newMediaId != mediaId) { mediaId = newMediaId } } val responseBody = response.body()?.string() ?: throw Exception("Empty response from Veev") val itemsRegex = Regex("""[\.\s'](?:fc|_vvto\[[^\]]*)(?:['\]]*)?\s*[:=]\s*['"]([^'"]+)""") val items = itemsRegex.findAll(responseBody).map { it.groupValues[1] }.toList() if (items.isNotEmpty()) { for (f in items.asReversed()) { val ch = try { veevDecode(f) } catch (_: Exception) { f } if (ch != f) { val params = mapOf( "op" to "player_api", "cmd" to "gi", "file_code" to mediaId, "r" to URLEncoder.encode(referer, "UTF-8"), "ch" to ch, "ie" to "1" ) val mainLink = URL(link).protocol + "://" + URL(link).host val downloadUrl = "$mainLink/dl?" + params.map { "${it.key}=${it.value}" }.joinToString("&") val jsonResponse = service.getWithHeaders(downloadUrl, referer, DEFAULT_USER_AGENT, mainUrl).body()?.string() ?: throw Exception("Empty JSON response from Veev") val fileJson = JSONObject(jsonResponse).optJSONObject("file") ?: throw Exception("Video removed") if (fileJson.optString("file_status") == "OK") { val dv = fileJson.getJSONArray("dv").getJSONObject(0).getString("s") val sourceUrl = decodeUrl(veevDecode(dv), buildArray(ch)[0]) val fileMimeType = fileJson.optString("file_mime_type", "") val exoMimeType = fileMimeType.toExoPlayerMimeType() return Video(source = sourceUrl, type = exoMimeType, headers = headers) } else { throw Exception("Video removed") } } } throw Exception("Unable to locate video") } else { throw Exception("Video removed") } } fun String.toExoPlayerMimeType(): String { return when (this.lowercase()) { "video/x-matroska", "video/webm" -> MimeTypes.VIDEO_MATROSKA "video/mp4" -> MimeTypes.VIDEO_MP4 "application/x-mpegurl", "application/vnd.apple.mpegurl" -> MimeTypes.APPLICATION_M3U8 "video/avi" -> MimeTypes.VIDEO_AVI else -> "" } } private fun veevDecode(etext: String): String { val result = StringBuilder() val lut = mutableMapOf() var n = 256 var c = etext[0].toString() result.append(c) for (char in etext.drop(1)) { val code = char.code val nc = if (code < 256) char.toString() else lut[code] ?: (c + c[0]) result.append(nc) lut[n] = c + nc[0] n += 1 c = nc } return result.toString() } private fun jsInt(x: Char): Int = if (x.isDigit()) x.toString().toInt() else 0 private fun buildArray(encodedString: String): List> { val d = mutableListOf>() val c = encodedString.toMutableList() var count = jsInt(c.removeAt(0)) while (count != 0) { val currentArray = mutableListOf() repeat(count) { currentArray.add(0, jsInt(c.removeAt(0))) } d.add(currentArray) count = jsInt(c.removeAt(0)) } return d } private fun decodeUrl(etext: String, tarray: List): String { var ds = etext for (t in tarray) { if (t == 1) { ds = ds.reversed() } val bytes = ds.chunked(2).map { it.toInt(16).toByte() }.toByteArray() ds = bytes.toString(UTF_8) ds = ds.replace("dXRmOA==", "") } return ds } private interface Service { @GET suspend fun getWithHeaders( @Url url: String, @Header("Referer") referer: String, @Header("User-Agent") userAgent: String, @Header("Origin") origin: String ): Response companion object { fun build(baseUrl: String): Service { val retrofit = Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(JsoupConverterFactory.create()) .build() return retrofit.create(Service::class.java) } } } }