Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(de/streamcloud): Fix video extractor #2463

Merged
merged 5 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/de/streamcloud/build.gradle
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}

ext {
extName = 'StreamCloud'
pkgNameSuffix = 'de.streamcloud'
extClass = '.StreamCloud'
extVersionCode = 5
extVersionCode = 6
libVersion = '13'
}

dependencies {
implementation(project(':lib-dood-extractor'))
implementation(project(':lib-streamtape-extractor'))
implementation(project(':lib-mixdrop-extractor'))
}

apply from: "$rootDir/common.gradle"
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.animeextension.de.streamcloud

import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
Expand All @@ -12,11 +11,10 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
Expand All @@ -28,198 +26,141 @@ class StreamCloud : ConfigurableAnimeSource, ParsedAnimeHttpSource() {

override val name = "StreamCloud"

override val baseUrl = "https://streamcloud.cam"
override val baseUrl = "https://streamcloud.movie"

override val lang = "de"

override val supportsLatest = false

override val client: OkHttpClient = network.cloudflareClient
override val client = network.cloudflareClient

private val preferences: SharedPreferences by lazy {
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}

override fun popularAnimeSelector(): String = "#dle-content div.item"
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/beliebte-filme/")

override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/beliebte-filme/")
override fun popularAnimeSelector() = "div#dle-content > div.item > div.thumb > a"

override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div.thumb a").attr("href"))
anime.thumbnail_url = baseUrl + element.select("div.thumb a img").attr("src")
anime.title = element.select("div.thumb a img").attr("alt")
return anime
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst("img")!!.run {
thumbnail_url = absUrl("src")
title = attr("alt")
}
}

override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeNextPageSelector() = null

// episodes
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector() = throw Exception("Not used")

override fun episodeListSelector() = throw Exception("not used")
override fun latestUpdatesFromElement(element: Element) = throw Exception("Not used")

override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
val episode = SEpisode.create()
episode.name = document.select("#title span.title").text()
episode.episode_number = 1F
episode.setUrlWithoutDomain(document.select("meta[property=\"og:url\"]").attr("content"))
episodeList.add(episode)
return episodeList.reversed()
}
override fun latestUpdatesRequest(page: Int) = throw Exception("Not used")

override fun episodeListRequest(anime: SAnime): Request {
return GET(baseUrl + anime.url, headers = Headers.headersOf("if-modified-since", ""))
}
override fun latestUpdatesSelector() = throw Exception("Not used")

override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
GET("$baseUrl/index.php?do=search&subaction=search&search_start=$page&full_search=0&story=$query")

// Video Extractor
override fun searchAnimeSelector() = popularAnimeSelector()

override fun videoListRequest(episode: SEpisode): Request {
return GET(baseUrl + episode.url, headers = Headers.headersOf("if-modified-since", ""))
}
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)

override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return videosFromElement(document)
}

private fun videosFromElement(document: Document): List<Video> {
val videoList = mutableListOf<Video>()
val iframeurl = document.select("div.embed-responsive-item.player-container-wrap iframe").attr("src")
val iframedoc = client.newCall(GET(iframeurl, headers = Headers.headersOf("if-modified-since", ""))).execute().asJsoup()
val lis = iframedoc.select("div._player ul._player-mirrors li")
val hosterSelection = preferences.getStringSet("hoster_selection", setOf("stape", "dood"))
for (li in lis) {
when {
li.text().contains("streamtape.com") && hosterSelection?.contains("stape") == true -> {
val url = li.attr("data-link")
runCatching {
with(
client.newCall(GET(url, headers = Headers.headersOf("Referer", baseUrl, "Cookie", "Fuck Streamtape because they add concatenation to fuck up scrapers")))
.execute().asJsoup(),
) {
linkRegex.find(this.select("script:containsData(document.getElementById('robotlink'))").toString())?.let {
val quality = "Streamtape"
val id = it.groupValues[1].replace("%27+%20(%27xcdb", "")
val videoUrl = "https://streamtape.com/get_video?$id&stream=1".replace("""" + '""", "")
videoList.add(Video(videoUrl, quality, videoUrl))
}
}
}
}
override fun searchAnimeNextPageSelector() = "#nextlink"

li.text().contains("doodstream.com") && hosterSelection?.contains("dood") == true -> {
val quality = "Doodstream"
val link = "https:" + li.attr("data-link")
val video = DoodExtractor(client).videoFromUrl(link, quality, false)
if (video != null) {
videoList.add(video)
}
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("#title span.title")!!.text()
status = SAnime.COMPLETED
with(document.selectFirst("div#longInfo")!!) {
thumbnail_url = selectFirst("img")?.absUrl("src")
genre = selectFirst("span.masha_index10")?.let {
it.text().split("/").joinToString()
}
description = select("#storyline > span > p").eachText().joinToString("\n")
author = selectFirst("strong:contains(Regie:) + div > a")?.text()
}

return videoList
}

private val linkRegex =
Regex("""(i(|" \+ ')d(|" \+ ')=.*?&(|" \+ ')e(|" \+ ')x(|" \+ ')p(|" \+ ')i(|" \+ ')r(|" \+ ')e(|" \+ ')s(|" \+ ')=.*?&(|" \+ ')i(|" \+ ')p(|" \+ ')=.*?&(|" \+ ')t(|" \+ ')o(|" \+ ')k(|" \+ ')e(|" \+ ')n(|" \+ ')=.*)'""")

override fun List<Video>.sort(): List<Video> {
val hoster = preferences.getString("preferred_hoster", "Streamtape")
val hosterList = mutableListOf<Video>()
val otherList = mutableListOf<Video>()
if (hoster != null) {
for (video in this) {
if (video.url.contains(hoster)) {
hosterList.add(video)
} else {
otherList.add(video)
}
}
} else {
otherList += this
}
val newList = mutableListOf<Video>()
var preferred = 0
for (video in hosterList) {
if (hoster?.let { video.quality.contains(it) } == true) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
for (video in otherList) {
if (hoster?.let { video.quality.contains(it) } == true) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.use { it.asJsoup() }
val episode = SEpisode.create().apply {
name = document.selectFirst("#title span.title")!!.text()
episode_number = 1F
setUrlWithoutDomain(document.location())
}
return newList
}

override fun videoListSelector() = throw Exception("not used")

override fun videoFromElement(element: Element) = throw Exception("not used")

override fun videoUrlParse(document: Document) = throw Exception("not used")

// Search

override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div.thumb a").attr("href"))
anime.thumbnail_url = baseUrl + element.select("div.thumb a img").attr("src")
anime.title = element.select("div.thumb a img").attr("alt")
return anime
return listOf(episode)
}

override fun searchAnimeNextPageSelector(): String = "#nextlink"
override fun episodeListSelector() = throw Exception("not used")

override fun searchAnimeSelector(): String = "#dle-content div.item"
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")

override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/index.php?do=search&subaction=search&search_start=$page&full_search=0&story=$query")
// ============================ Video Links =============================
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val mixdropExtractor by lazy { MixDropExtractor(client) }

// Details
override fun videoListParse(response: Response): List<Video> {
val document = response.use { it.asJsoup() }
val iframeurl = document.selectFirst("div.player-container-wrap > iframe")
?.attr("src")
?: error("No videos!")
val iframeDoc = client.newCall(GET(iframeurl)).execute().use { it.asJsoup() }

val hosterSelection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!!
val items = iframeDoc.select("div._player ul._player-mirrors li")

return items.flatMap { element ->
val url = element.attr("data-link").run {
takeIf { startsWith("https") }
?: "https:$this"
}

override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = baseUrl + document.select("#longInfo img").attr("src")
anime.title = document.select("#title span.masha_index4").text()
anime.genre = document.select("#longInfo span.masha_index10").text().split("/").joinToString(", ") { it }
anime.description = document.select("#longInfo #storyline span p").text()
if (document.select("#longInfo div[style] a").attr("href").toString().contains("director")) {
anime.author = document.select("#longInfo div[style] a").toString().substringAfter("</span>").substringBefore("</a>")
runCatching {
when {
url.contains("streamtape") && hosterSelection.contains("stape") -> {
streamtapeExtractor.videosFromUrl(url)
}
url.startsWith("https://dood") && hosterSelection.contains("dood") -> {
doodExtractor.videosFromUrl(url)
}
url.contains("mixdrop.") && hosterSelection.contains("mixdrop") -> {
mixdropExtractor.videosFromUrl(url)
}
else -> emptyList()
}
}.getOrElse { emptyList() }
}
anime.status = SAnime.COMPLETED
return anime
}

// Latest

override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
override fun List<Video>.sort(): List<Video> {
val hoster = preferences.getString(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!

override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
return sortedWith(
compareBy { it.url.contains(hoster) },
).reversed()
}

override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun videoListSelector() = throw Exception("not used")

override fun latestUpdatesSelector(): String = throw Exception("Not used")
override fun videoFromElement(element: Element) = throw Exception("not used")

// Preferences
override fun videoUrlParse(document: Document) = throw Exception("not used")

// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val hosterPref = ListPreference(screen.context).apply {
key = "preferred_hoster"
title = "Standard-Hoster"
entries = arrayOf("Streamtape", "DoodStream")
entryValues = arrayOf("https://streamtape.com", "https://dood.")
setDefaultValue("https://streamtape.com")
ListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = PREF_HOSTER_TITLE
entries = PREF_HOSTER_ENTRIES
entryValues = PREF_HOSTER_VALUES
setDefaultValue(PREF_HOSTER_DEFAULT)
summary = "%s"

setOnPreferenceChangeListener { _, newValue ->
Expand All @@ -228,19 +169,33 @@ class StreamCloud : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val subSelection = MultiSelectListPreference(screen.context).apply {
key = "hoster_selection"
title = "Hoster auswählen"
entries = arrayOf("Streamtape", "Doodstream")
entryValues = arrayOf("stape", "dood")
setDefaultValue(setOf("stape", "dood"))
}.also(screen::addPreference)

MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_SELECTION_KEY
title = PREF_HOSTER_SELECTION_TITLE
entries = PREF_HOSTER_SELECTION_ENTRIES
entryValues = PREF_HOSTER_SELECTION_VALUES
setDefaultValue(PREF_HOSTER_SELECTION_DEFAULT)

setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}
screen.addPreference(hosterPref)
screen.addPreference(subSelection)
}.also(screen::addPreference)
}

companion object {
private const val PREF_HOSTER_KEY = "preferred_hoster"
private const val PREF_HOSTER_TITLE = "Standard-Hoster"
private const val PREF_HOSTER_DEFAULT = "https://streamtape.com"
private val PREF_HOSTER_ENTRIES = arrayOf("Streamtape", "DoodStream")
private val PREF_HOSTER_VALUES = arrayOf("https://streamtape.com", "https://dood.")

private const val PREF_HOSTER_SELECTION_KEY = "hoster_selection"
private const val PREF_HOSTER_SELECTION_TITLE = "Hoster auswählen"
private val PREF_HOSTER_SELECTION_ENTRIES = arrayOf("Streamtape", "DoodStream", "MixDrop")
private val PREF_HOSTER_SELECTION_VALUES = arrayOf("stape", "dood", "mixdrop")
private val PREF_HOSTER_SELECTION_DEFAULT by lazy { PREF_HOSTER_SELECTION_VALUES.toSet() }
}
}