diff --git a/src/en/animeowl/AndroidManifest.xml b/src/en/animeowl/AndroidManifest.xml new file mode 100644 index 000000000..acb4de356 --- /dev/null +++ b/src/en/animeowl/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/en/animeowl/build.gradle b/src/en/animeowl/build.gradle new file mode 100644 index 000000000..7a5950f42 --- /dev/null +++ b/src/en/animeowl/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'AnimeOwl' + pkgNameSuffix = 'en.animeowl' + extClass = '.AnimeOwl' + extVersionCode = 1 + libVersion = '13' +} + +dependencies { + implementation(project(':lib-dood-extractor')) + implementation(project(':lib-streamsb-extractor')) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/animeowl/res/mipmap-anydpi-v26/ic_launcher.xml b/src/en/animeowl/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..90f958096 --- /dev/null +++ b/src/en/animeowl/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/en/animeowl/res/mipmap-hdpi/ic_launcher.png b/src/en/animeowl/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..57c450c2a Binary files /dev/null and b/src/en/animeowl/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/animeowl/res/mipmap-hdpi/ic_launcher_adaptive_back.png b/src/en/animeowl/res/mipmap-hdpi/ic_launcher_adaptive_back.png new file mode 100644 index 000000000..4ea92bd5e Binary files /dev/null and b/src/en/animeowl/res/mipmap-hdpi/ic_launcher_adaptive_back.png differ diff --git a/src/en/animeowl/res/mipmap-hdpi/ic_launcher_adaptive_fore.png b/src/en/animeowl/res/mipmap-hdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 000000000..8014e4da6 Binary files /dev/null and b/src/en/animeowl/res/mipmap-hdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/en/animeowl/res/mipmap-mdpi/ic_launcher.png b/src/en/animeowl/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..41bcb73ac Binary files /dev/null and b/src/en/animeowl/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/animeowl/res/mipmap-mdpi/ic_launcher_adaptive_back.png b/src/en/animeowl/res/mipmap-mdpi/ic_launcher_adaptive_back.png new file mode 100644 index 000000000..8d66ef758 Binary files /dev/null and b/src/en/animeowl/res/mipmap-mdpi/ic_launcher_adaptive_back.png differ diff --git a/src/en/animeowl/res/mipmap-mdpi/ic_launcher_adaptive_fore.png b/src/en/animeowl/res/mipmap-mdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 000000000..108d5a9ce Binary files /dev/null and b/src/en/animeowl/res/mipmap-mdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/en/animeowl/res/mipmap-xhdpi/ic_launcher.png b/src/en/animeowl/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..ce921ca78 Binary files /dev/null and b/src/en/animeowl/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/animeowl/res/mipmap-xhdpi/ic_launcher_adaptive_back.png b/src/en/animeowl/res/mipmap-xhdpi/ic_launcher_adaptive_back.png new file mode 100644 index 000000000..156f64239 Binary files /dev/null and b/src/en/animeowl/res/mipmap-xhdpi/ic_launcher_adaptive_back.png differ diff --git a/src/en/animeowl/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/src/en/animeowl/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 000000000..dfaadd63e Binary files /dev/null and b/src/en/animeowl/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/en/animeowl/res/mipmap-xxhdpi/ic_launcher.png b/src/en/animeowl/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b4c5b268e Binary files /dev/null and b/src/en/animeowl/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/animeowl/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/src/en/animeowl/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png new file mode 100644 index 000000000..0425f0ad0 Binary files /dev/null and b/src/en/animeowl/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png differ diff --git a/src/en/animeowl/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/src/en/animeowl/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 000000000..dfdaf4301 Binary files /dev/null and b/src/en/animeowl/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..d739f97cd Binary files /dev/null and b/src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png new file mode 100644 index 000000000..8ebe16407 Binary files /dev/null and b/src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png differ diff --git a/src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 000000000..ffae00518 Binary files /dev/null and b/src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/en/animeowl/res/play_store_512.png b/src/en/animeowl/res/play_store_512.png new file mode 100644 index 000000000..8305be176 Binary files /dev/null and b/src/en/animeowl/res/play_store_512.png differ diff --git a/src/en/animeowl/src/eu/kanade/tachiyomi/animeextension/en/animeowl/AnimeOwl.kt b/src/en/animeowl/src/eu/kanade/tachiyomi/animeextension/en/animeowl/AnimeOwl.kt new file mode 100644 index 000000000..5dd3e3502 --- /dev/null +++ b/src/en/animeowl/src/eu/kanade/tachiyomi/animeextension/en/animeowl/AnimeOwl.kt @@ -0,0 +1,396 @@ +package eu.kanade.tachiyomi.animeextension.en.animeowl + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.en.animeowl.extractors.GogoCdnExtractor +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.streamsbextractor.StreamSBExtractor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.float +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +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 uy.kohesive.injekt.injectLazy +import kotlin.math.ceil + +@ExperimentalSerializationApi +class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "AnimeOwl" + + override val baseUrl by lazy { preferences.getString("preferred_domain", "https://animeowl.net")!! } + + override val lang = "en" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + private val json: Json by injectLazy() + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending?page=$page") + + override fun popularAnimeSelector(): String = "div#anime-list > div.recent-anime" + + override fun popularAnimeNextPageSelector(): String = "ul.pagination > li.page-item > a[rel=next]" + + override fun popularAnimeFromElement(element: Element): SAnime { + return SAnime.create().apply { + setUrlWithoutDomain(element.select("div > a").attr("href")) + thumbnail_url = element.select("div.img-container > a > img").attr("src") + title = element.select("a.title-link").text() + } + } + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/recent-episode/all") + + override fun latestUpdatesSelector(): String = popularAnimeSelector() + + override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector() + + override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element) + + // =============================== Search =============================== + override fun fetchSearchAnime( + page: Int, + query: String, + filters: AnimeFilterList + ): Observable { + val limit = 30 + val mediaType = "application/json; charset=utf-8".toMediaType() + val body = """{"limit":$limit,"page":${page - 1},"pageCount":0,"value":"$query","sort":4,"selected":{"type":[],"genre":[],"year":[],"country":[],"season":[],"status":[],"sort":[],"language":[]}}""".toRequestBody(mediaType) + + val response = client.newCall(POST("$baseUrl/api/advance-search", body = body, headers = headers)).execute() + val result = json.decodeFromString(response.body!!.string()) + + val total = result["total"]!!.jsonPrimitive.int + val nextPage = ceil(total.toFloat() / limit).toInt() > page + val data = result["results"]!!.jsonArray + val animes = data.map { item -> + SAnime.create().apply { + setUrlWithoutDomain("/anime/${item.jsonObject["anime_slug"]!!.jsonPrimitive.content}/") + thumbnail_url = "$baseUrl${item.jsonObject["image"]!!.jsonPrimitive.content}" + title = item.jsonObject["anime_name"]!!.jsonPrimitive.content + } + } + + return Observable.just(AnimesPage(animes, nextPage)) + } + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = + throw Exception("Not Used") + + override fun searchAnimeSelector(): String = throw Exception("Not Used") + + override fun searchAnimeNextPageSelector(): String = throw Exception("Not Used") + + override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("Not Used") + + // =========================== Anime Details ============================ + override fun animeDetailsParse(document: Document): SAnime { + val anime = SAnime.create() + anime.title = document.select("h3.anime-name").text() + anime.genre = document.select("div.genre > a").joinToString { it.text() } + anime.description = document.select("div.anime-desc.desc-content").text() + // No author info so use type of anime + anime.author = document.select("div.type > a").text() + anime.status = parseStatus(document.select("div.status > span").text()) + + // add alternative name to anime description + val altName = "Other name(s): " + document.select("h4.anime-alternatives").text()?.let { + if (it.isBlank().not()) { + anime.description = when { + anime.description.isNullOrBlank() -> altName + it + else -> anime.description + "\n\n$altName" + it + } + } + } + return anime + } + + // ============================== Episodes ============================== + override fun episodeListParse(response: Response): List { + val animeId = response.asJsoup().select("div#unq-anime-id").attr("animeId") + val episodesJson = client.newCall(GET("$baseUrl/api/anime/$animeId/episodes")).execute().body!!.string() + val episodes = json.decodeFromString(episodesJson) + val subList = episodes["sub"]!!.jsonArray + val dubList = episodes["dub"]!!.jsonArray + val subSlug = episodes["sub_slug"]!!.jsonPrimitive.content + val dubSlug = episodes["dub_slug"]!!.jsonPrimitive.content + return subList.map { item -> + val dub = dubList.find { + it.jsonObject["name"]!!.jsonPrimitive.content == item.jsonObject["name"]!!.jsonPrimitive.content + } + SEpisode.create().apply { + url = "{\"Sub\": \"https://portablegaming.co/watch/$subSlug/${item.jsonObject["episode_index"]!!.jsonPrimitive.content}\"," + + if (dub != null) { + "\"Dub\": \"https://portablegaming.co/watch/$dubSlug/${dub.jsonObject["episode_index"]!!.jsonPrimitive.content}\"}" + } else { "\"Dub\": \"\"}" } + episode_number = item.jsonObject["name"]!!.jsonPrimitive.float + name = "Episode " + item.jsonObject["name"]!!.jsonPrimitive.content + } + }.reversed() + } + + override fun episodeListSelector(): String = throw Exception("Not Used") + + override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not Used") + + // ============================ Video Links ============================= + override fun fetchVideoList(episode: SEpisode): Observable> { + val urlJson = json.decodeFromString(episode.url) + val videoList = mutableListOf