Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".de.einfach.EinfachUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="einfach.to"
android:pathPattern="/filme/..*"
android:scheme="https" />
<data
android:host="einfach.to"
android:pathPattern="/series/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,18 @@
ext {
extName = 'Einfach'
extClass = '.Einfach'
extVersionCode = 9
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:dood-extractor"))
implementation(project(":lib:filemoon-extractor"))
implementation(project(":lib:mixdrop-extractor"))
implementation(project(":lib:playlist-utils"))
implementation(project(":lib:streamtape-extractor"))
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:voe-extractor"))
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,297 @@
package eu.kanade.tachiyomi.animeextension.de.einfach
import android.app.Application
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.de.einfach.extractors.MyStreamExtractor
import eu.kanade.tachiyomi.animeextension.de.einfach.extractors.UnpackerExtractor
import eu.kanade.tachiyomi.animeextension.de.einfach.extractors.VidozaExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
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.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class Einfach : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Einfach"
override val baseUrl = "https://einfach.to"
override val lang = "de"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
// Actually the source doesn't provide a popular entries page, and the
// "sort by views" filter isn't working, so we'll use the latest series updates instead.
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/series/page/$page")
override fun popularAnimeSelector() = "article.box > div.bx > a.tip"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.attr("title")
thumbnail_url = element.selectFirst("img")?.run {
absUrl("data-lazy-src").ifEmpty { absUrl("src") }
}
}
override fun popularAnimeNextPageSelector() = "div.pagination > a.next"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/filme/page/$page")
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path"))
.awaitSuccess()
.use(::searchAnimeByPathParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
GET("$baseUrl/page/$page/?s=$query")
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val info = document.selectFirst("article div > div.infl")!!
title = info.selectFirst("h1.entry-title")!!.text()
thumbnail_url = info.selectFirst("img")?.run {
absUrl("data-lazy-src").ifEmpty { absUrl("src") }
}
artist = info.getInfo("Stars:")
genre = info.getInfo("Genre:")
author = info.getInfo("Network:")
status = parseStatus(info.getInfo("Status:").orEmpty())
description = info.selectFirst("div.entry-content > p")?.ownText()
}
private fun Element.getInfo(label: String) =
selectFirst("li:has(b:contains($label)) > span.colspan")?.text()?.trim()
private fun parseStatus(status: String) = when (status) {
"Ongoing" -> SAnime.ONGOING
else -> SAnime.COMPLETED
}
// ============================== Episodes ==============================
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
if (anime.url.contains("/filme/")) {
val episode = SEpisode.create().apply {
url = anime.url
name = "Movie - ${anime.title}"
episode_number = 1F
}
return listOf(episode)
}
return super.getEpisodeList(anime)
}
override fun episodeListParse(response: Response) =
super.episodeListParse(response).reversed()
override fun episodeListSelector() = "div.epsdlist > ul > li > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
val eplnum = element.selectFirst(".epl-num")?.text().orEmpty().trim()
episode_number = eplnum.substringAfterLast(" ").toFloatOrNull() ?: 1F
name = eplnum.ifBlank { "S1 EP 1" } + " - " + element.selectFirst(".epl-title")?.text().orEmpty()
date_upload = element.selectFirst(".epl-date")?.text().orEmpty().toDate()
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val selection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!!
val links = doc.select(videoListSelector()).asSequence()
.filter { it.text().lowercase() in selection }
.mapNotNull { element ->
val html = element.attr("data-em").let { b64encoded ->
runCatching {
String(Base64.decode(b64encoded, Base64.DEFAULT))
}.getOrNull()
}
val url = html?.let(Jsoup::parseBodyFragment)
?.selectFirst("iframe")
?.attr("src")
?: return@mapNotNull null
val fixedUrl = url.takeIf { it.startsWith("https:") } ?: "https:$url"
element.text().lowercase() to fixedUrl
}.toList()
return links.parallelCatchingFlatMapBlocking { (name, link) ->
getVideosFromUrl(name, link)
}
}
override fun videoListSelector() = "div.lserv > ul > li > a"
private val doodExtractor by lazy { DoodExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val lulustreamExtractor by lazy { UnpackerExtractor(client, headers) }
private val mixdropExtractor by lazy { MixDropExtractor(client) }
private val mystreamExtractor by lazy { MyStreamExtractor(client, headers) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val vidozaExtractor by lazy { VidozaExtractor(client) }
private val voeExtractor by lazy { VoeExtractor(client) }
private fun getVideosFromUrl(name: String, url: String): List<Video> {
return when (name) {
"doodstream" -> doodExtractor.videosFromUrl(url)
"filelions" -> streamwishExtractor.videosFromUrl(url, videoNameGen = { "FileLions - $it" })
"filemoon" -> filemoonExtractor.videosFromUrl(url)
"lulustream" -> lulustreamExtractor.videosFromUrl(url, "LuLuStream")
"mixdrop" -> mixdropExtractor.videosFromUrl(url)
"streamtape" -> streamtapeExtractor.videosFromUrl(url)
"streamwish" -> streamwishExtractor.videosFromUrl(url)
"vidoza" -> vidozaExtractor.videosFromUrl(url)
"voe" -> voeExtractor.videosFromUrl(url)
"stream in hd" -> mystreamExtractor.videosFromUrl(url)
else -> emptyList()
}
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.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()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
companion object {
const val PREFIX_SEARCH = "path:"
private val DATE_FORMATTER by lazy {
SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
}
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("240p", "360p", "480p", "720p", "1080p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
private const val PREF_HOSTER_SELECTION_KEY = "pref_hoster_selection"
private const val PREF_HOSTER_SELECTION_TITLE = "Enable/Disable video hosters"
private val PREF_HOSTER_SELECTION_ENTRIES = arrayOf(
"DoodStream",
"FileLions",
"Filemoon",
"LuLuStream",
"MixDrop",
"Streamtape",
"StreamWish",
"Vidoza",
"VOE",
"Stream in HD",
)
private val PREF_HOSTER_SELECTION_VALUES by lazy { PREF_HOSTER_SELECTION_ENTRIES.map(String::lowercase).toTypedArray() }
private val PREF_HOSTER_SELECTION_DEFAULT by lazy { PREF_HOSTER_SELECTION_VALUES.toSet() }
}
}

View file

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.animeextension.de.einfach
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://einfach.to/<type>/<item> intents
* and redirects them to the main Aniyomi process.
*/
class EinfachUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 2) {
val type = pathSegments[1]
val item = pathSegments[2]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${Einfach.PREFIX_SEARCH}$type/$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.animeextension.de.einfach.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
// From animeworldindia
class MyStreamExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String): List<Video> {
val host = url.substringBefore("/watch?")
return runCatching {
val response = client.newCall(GET(url, headers)).execute()
val body = response.body.string()
val codePart = body
.substringAfter("sniff(") // Video function
.substringBefore(",[")
val streamCode = codePart
.substringBeforeLast("\",\"")
.substringAfterLast(",\"") // our beloved hash
val id = codePart.substringAfter(",\"").substringBefore('"') // required ID
val streamUrl = "$host/m3u8/$id/$streamCode/master.txt?s=1&cache=1"
val cookie = response.headers.firstOrNull {
it.first.startsWith("set-cookie", true) && it.second.startsWith("PHPSESSID", true)
}?.second?.substringBefore(";") ?: ""
val newHeaders = headers.newBuilder()
.set("cookie", cookie)
.set("accept", "*/*")
.build()
playlistUtils.extractFromHls(
streamUrl,
masterHeaders = newHeaders,
videoHeaders = newHeaders,
videoNameGen = { "MyStream: $it" },
)
}.getOrElse { emptyList<Video>() }
}
}

View file

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.animeextension.de.einfach.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class UnpackerExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, hoster: String): List<Video> {
val doc = client.newCall(GET(url, headers)).execute()
.asJsoup()
val script = doc.selectFirst("script:containsData(eval)")
?.data()
?.let(JsUnpacker::unpackAndCombine)
?: return emptyList()
val playlistUrl = script.substringAfter("file:\"").substringBefore('"')
return playlistUtils.extractFromHls(
playlistUrl,
referer = playlistUrl,
videoNameGen = { "$hoster - $it" },
)
}
}

View file

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.animeextension.de.einfach.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
class VidozaExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute()
.asJsoup()
val script = doc.selectFirst("script:containsData(sourcesCode: [)")
?.data()
?: return emptyList()
return script.substringAfter("sourcesCode: [").substringBefore("],")
.split('{')
.drop(1)
.mapNotNull {
val videoUrl = it.substringAfter("src: \"").substringBefore('"')
val resolution = it.substringAfter("res:\"").substringBefore('"') + "p"
Video(videoUrl, "Vidoza - $resolution", videoUrl)
}
}
}