diff --git a/src/de/einfach/AndroidManifest.xml b/src/de/einfach/AndroidManifest.xml new file mode 100644 index 000000000..da5c29686 --- /dev/null +++ b/src/de/einfach/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/src/de/einfach/build.gradle b/src/de/einfach/build.gradle new file mode 100644 index 000000000..87115650b --- /dev/null +++ b/src/de/einfach/build.gradle @@ -0,0 +1,25 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +ext { + extName = 'Einfach' + pkgNameSuffix = 'de.einfach' + extClass = '.Einfach' + extVersionCode = 1 + libVersion = '13' +} + +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") +} + +apply from: "$rootDir/common.gradle" diff --git a/src/de/einfach/res/mipmap-hdpi/ic_launcher.png b/src/de/einfach/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..76d30abe6 Binary files /dev/null and b/src/de/einfach/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/de/einfach/res/mipmap-mdpi/ic_launcher.png b/src/de/einfach/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..fd4493044 Binary files /dev/null and b/src/de/einfach/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/de/einfach/res/mipmap-xhdpi/ic_launcher.png b/src/de/einfach/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..7401c072a Binary files /dev/null and b/src/de/einfach/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/de/einfach/res/mipmap-xxhdpi/ic_launcher.png b/src/de/einfach/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..9e44d1e1a Binary files /dev/null and b/src/de/einfach/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/de/einfach/res/mipmap-xxxhdpi/ic_launcher.png b/src/de/einfach/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..399267758 Binary files /dev/null and b/src/de/einfach/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/de/einfach/src/eu/kanade/tachiyomi/animeextension/de/einfach/Einfach.kt b/src/de/einfach/src/eu/kanade/tachiyomi/animeextension/de/einfach/Einfach.kt new file mode 100644 index 000000000..f86c3ef90 --- /dev/null +++ b/src/de/einfach/src/eu/kanade/tachiyomi/animeextension/de/einfach/Einfach.kt @@ -0,0 +1,310 @@ +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.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +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().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 fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler + val path = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/$path")) + .asObservableSuccess() + .map(::searchAnimeByPathParse) + } else { + super.fetchSearchAnime(page, query, filters) + } + } + + private fun searchAnimeByPathParse(response: Response): AnimesPage { + val details = animeDetailsParse(response.use { it.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 fun fetchEpisodeList(anime: SAnime): Observable> { + if (anime.url.contains("/filme/")) { + val episode = SEpisode.create().apply { + url = anime.url + name = "Movie - ${anime.title}" + episode_number = 1F + } + return Observable.just(listOf(episode)) + } + + return super.fetchEpisodeList(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