fix: Fixed Q1N source using UniversalExtractor as fallback #1019

Merged
webditto merged 1 commit from webditto/extensions-source:fix/pt/q1n into main 2025-06-19 12:55:44 -05:00
3 changed files with 138 additions and 2 deletions
Showing only changes of commit e39934a7e1 - Show all commits

View file

@ -1,9 +1,10 @@
ext {
extKmkVersionCode = 1
extName = 'Q1N'
extClass = '.Q1N'
themePkg = 'dooplay'
baseUrl = 'https://q1n.net'
overrideVersionCode = 20
overrideVersionCode = 21
}
apply from: "$rootDir/common.gradle"
@ -13,5 +14,6 @@ dependencies {
implementation(project(":lib:filemoon-extractor"))
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:mixdrop-extractor"))
implementation(project(":lib:playlist-utils"))
implementation(project(":lib:streamtape-extractor"))
}

View file

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgratis
import android.util.Log
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.NoaExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.RuplayExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.UniversalExtractor
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
@ -27,6 +29,8 @@ class Q1N : DooPlay(
"https://q1n.net",
) {
private val tag by lazy { javaClass.simpleName }
override val id: Long = 2969482460524685571L
override val dateFormatter by lazy {
@ -118,10 +122,12 @@ class Q1N : DooPlay(
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val mixDropExtractor by lazy { MixDropExtractor(client) }
private val universalExtractor by lazy { UniversalExtractor(client) }
private fun getPlayerVideos(player: Element): List<Video> {
val name = player.selectFirst("span.title")!!.text().lowercase()
val url = getPlayerUrl(player) ?: return emptyList()
Log.d(tag, "Fetching videos from: $url")
return when {
"ruplay" in name -> ruplayExtractor.videosFromUrl(url)
"streamwish" in name -> streamWishExtractor.videosFromUrl(url)
@ -131,7 +137,7 @@ class Q1N : DooPlay(
"noa" in name -> noaExtractor.videosFromUrl(url)
"mdplayer" in name -> noaExtractor.videosFromUrl(url, "MDPLAYER")
"/player/" in url -> bloggerExtractor.videosFromUrl(url, headers)
else -> emptyList()
else -> universalExtractor.videosFromUrl(url, headers)
}
}

View file

@ -0,0 +1,128 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class UniversalExtractor(private val client: OkHttpClient) {
private val tag by lazy { javaClass.simpleName }
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
@SuppressLint("SetJavaScriptEnabled")
fun videosFromUrl(origRequestUrl: String, origRequestHeader: Headers): List<Video> {
Log.d(tag, "Fetching videos from: $origRequestUrl")
val host = origRequestUrl.toHttpUrl().host.substringBefore(".").proper()
val latch = CountDownLatch(1)
var webView: WebView? = null
var resultUrl = ""
val playlistUtils by lazy { PlaylistUtils(client, origRequestHeader) }
val headers = origRequestHeader.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
handler.post {
val newView = WebView(context)
webView = newView
with(newView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = origRequestHeader["User-Agent"]
}
newView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
Log.d(tag, "Page loaded, injecting script")
view?.evaluateJavascript(CHECK_SCRIPT) {}
}
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
val url = request.url.toString()
Log.d(tag, "Intercepted URL: $url")
if (VIDEO_REGEX.containsMatchIn(url)) {
resultUrl = url
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl("$origRequestUrl&dl=1", headers)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return when {
"m3u8" in resultUrl -> {
Log.d(tag, "m3u8 URL: $resultUrl")
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$host: $it" })
}
"mpd" in resultUrl -> {
Log.d(tag, "mpd URL: $resultUrl")
playlistUtils.extractFromDash(resultUrl, { it -> "$host: $it" }, referer = origRequestUrl)
}
"mp4" in resultUrl -> {
Log.d(tag, "mp4 URL: $resultUrl")
Video(resultUrl, "$host: mp4", resultUrl, Headers.headersOf("referer", origRequestUrl)).let(::listOf)
}
else -> emptyList()
}
}
private fun String.proper(): String {
return this.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
Locale.getDefault(),
)
} else it.toString()
}
}
companion object {
const val TIMEOUT_SEC: Long = 10
private val VIDEO_REGEX by lazy { Regex(".*\\.(mp4|m3u8|mpd)(\\?.*)?$") }
private val CHECK_SCRIPT by lazy {
"""
setInterval(() => {
var playButton = document.getElementById('player-button-container')
if (playButton) {
playButton.click()
}
var downloadButton = document.querySelector(".downloader-button")
if (downloadButton) {
if (downloadButton.href) {
location.href = downloadButton.href
} else {
downloadButton.click()
}
}
}, 2500)
""".trimIndent()
}
}
}