diff --git a/src/fr/otakufr/AndroidManifest.xml b/src/fr/otakufr/AndroidManifest.xml new file mode 100644 index 000000000..568741e54 --- /dev/null +++ b/src/fr/otakufr/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/fr/otakufr/build.gradle b/src/fr/otakufr/build.gradle new file mode 100644 index 000000000..33d48ead1 --- /dev/null +++ b/src/fr/otakufr/build.gradle @@ -0,0 +1,24 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +ext { + extName = 'OtakuFR' + pkgNameSuffix = 'fr.otakufr' + extClass = '.OtakuFR' + extVersionCode = 1 + libVersion = '13' +} + +dependencies { + implementation(project(':lib-sibnet-extractor')) + implementation(project(':lib-voe-extractor')) + implementation(project(':lib-sendvid-extractor')) + implementation(project(':lib-dood-extractor')) + implementation(project(':lib-okru-extractor')) + implementation(project(":lib-playlist-utils")) + implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1" +} + +apply from: "$rootDir/common.gradle" diff --git a/src/fr/otakufr/res/mipmap-hdpi/ic_launcher.png b/src/fr/otakufr/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..df3abc00c Binary files /dev/null and b/src/fr/otakufr/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/fr/otakufr/res/mipmap-mdpi/ic_launcher.png b/src/fr/otakufr/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..2d0d44345 Binary files /dev/null and b/src/fr/otakufr/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/fr/otakufr/res/mipmap-xhdpi/ic_launcher.png b/src/fr/otakufr/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..20674bc2a Binary files /dev/null and b/src/fr/otakufr/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/fr/otakufr/res/mipmap-xxhdpi/ic_launcher.png b/src/fr/otakufr/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..0153d0edc Binary files /dev/null and b/src/fr/otakufr/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/fr/otakufr/res/mipmap-xxxhdpi/ic_launcher.png b/src/fr/otakufr/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..3ac6e8163 Binary files /dev/null and b/src/fr/otakufr/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/fr/otakufr/res/web_hi_res_512.png b/src/fr/otakufr/res/web_hi_res_512.png new file mode 100644 index 000000000..d992179a0 Binary files /dev/null and b/src/fr/otakufr/res/web_hi_res_512.png differ diff --git a/src/fr/otakufr/src/eu/kanade/tachiyomi/animeextension/fr/otakufr/OtakuFR.kt b/src/fr/otakufr/src/eu/kanade/tachiyomi/animeextension/fr/otakufr/OtakuFR.kt new file mode 100644 index 000000000..65efd0627 --- /dev/null +++ b/src/fr/otakufr/src/eu/kanade/tachiyomi/animeextension/fr/otakufr/OtakuFR.kt @@ -0,0 +1,383 @@ +package eu.kanade.tachiyomi.animeextension.fr.otakufr + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.fr.otakufr.extractors.StreamWishExtractor +import eu.kanade.tachiyomi.animeextension.fr.otakufr.extractors.UpstreamExtractor +import eu.kanade.tachiyomi.animeextension.fr.otakufr.extractors.VidbmExtractor +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +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.okruextractor.OkruExtractor +import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor +import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor +import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.lang.Exception +import java.text.SimpleDateFormat +import java.util.Locale + +class OtakuFR : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "OtakuFR" + + override val baseUrl = "https://otakufr.co" + + override val lang = "fr" + + override val supportsLatest = false + + override val client: OkHttpClient = network.cloudflareClient + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // ============================== Popular =============================== + + override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/en-cours".addPage(page), headers) + + override fun popularAnimeSelector(): String = "div.list > article.card" + + override fun popularAnimeFromElement(element: Element): SAnime { + val a = element.selectFirst("a.episode-name")!! + + return SAnime.create().apply { + thumbnail_url = element.selectFirst("img")!!.attr("abs:src") + setUrlWithoutDomain(a.attr("href")) + title = a.text() + } + } + + override fun popularAnimeNextPageSelector(): String = "ul.pagination > li.active ~ li" + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used") + + override fun latestUpdatesSelector(): String = throw Exception("Not used") + + override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used") + + override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used") + + // =============================== Search =============================== + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val filterList = if (filters.isEmpty()) getFilterList() else filters + val genreFilter = filterList.find { it is GenreFilter } as GenreFilter + val subPageFilter = filterList.find { it is SubPageFilter } as SubPageFilter + + return when { + query.isNotBlank() -> GET("$baseUrl/toute-la-liste-affiches/?q=$query".addPage(page), headers) + genreFilter.state != 0 -> GET("$baseUrl${genreFilter.toUriPart()}".addPage(page), headers) + subPageFilter.state != 0 -> GET("$baseUrl${subPageFilter.toUriPart()}".addPage(page), headers) + else -> throw Exception("Either search something or select a filter") + } + } + + override fun searchAnimeSelector(): String = popularAnimeSelector() + + override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) + + override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector() + + // =========================== Anime Details ============================ + + override fun animeDetailsParse(document: Document): SAnime { + val infoDiv = document.selectFirst("article.card div.episode")!! + + return SAnime.create().apply { + status = infoDiv.selectFirst("li:contains(Statut)")?.let { parseStatus(it.ownText()) } ?: SAnime.UNKNOWN + genre = infoDiv.select("li:contains(Genre:) ul li").joinToString(", ") { it.text() } + author = infoDiv.selectFirst("li:contains(Studio d\\'animation)")?.ownText() + description = buildString { + append(infoDiv.select("> p:not(:has(strong)):not(:empty)").joinToString("\n\n") { it.text() }) + append("\n") + infoDiv.selectFirst("li:contains(Autre Nom)")?.let { append("\n${it.text()}") } + infoDiv.selectFirst("li:contains(Auteur)")?.let { append("\n${it.text()}") } + infoDiv.selectFirst("li:contains(Réalisateur)")?.let { append("\n${it.text()}") } + infoDiv.selectFirst("li:contains(Type)")?.let { append("\n${it.text()}") } + infoDiv.selectFirst("li:contains(Sortie initiale)")?.let { append("\n${it.text()}") } + infoDiv.selectFirst("li:contains(Durée)")?.let { append("\n${it.text()}") } + } + } + } + + // ============================== Episodes ============================== + + override fun episodeListSelector() = "div.list-episodes > a" + + override fun episodeFromElement(element: Element): SEpisode { + val epText = element.ownText() + + return SEpisode.create().apply { + setUrlWithoutDomain(element.attr("abs:href")) + name = epText + episode_number = Regex(" ([\\d.]+) (?:Vostfr|VF)").find(epText) + ?.groupValues + ?.get(1) + ?.toFloatOrNull() + ?: 1F + date_upload = element.selectFirst("span") + ?.text() + ?.let(::parseDate) + ?: 0L + } + } + + // ============================ Video Links ============================= + + private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) } + private val vidbmExtractor by lazy { VidbmExtractor(client, headers) } + private val sendvidExtractor by lazy { SendvidExtractor(client, headers) } + private val upstreamExtractor by lazy { UpstreamExtractor(client, headers) } + private val okruExtractor by lazy { OkruExtractor(client) } + private val doodExtractor by lazy { DoodExtractor(client) } + private val voeExtractor by lazy { VoeExtractor(client) } + private val sibnetExtractor by lazy { SibnetExtractor(client) } + + override fun videoListParse(response: Response): List