diff --git a/src/en/nollyverse/AndroidManifest.xml b/src/en/nollyverse/AndroidManifest.xml new file mode 100644 index 000000000..acb4de356 --- /dev/null +++ b/src/en/nollyverse/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/en/nollyverse/build.gradle b/src/en/nollyverse/build.gradle new file mode 100644 index 000000000..f81958038 --- /dev/null +++ b/src/en/nollyverse/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'NollyVerse' + pkgNameSuffix = 'en.nollyverse' + extClass = '.NollyVerse' + extVersionCode = 1 + libVersion = '13' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/nollyverse/res/mipmap-hdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..01b41f68a Binary files /dev/null and b/src/en/nollyverse/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/nollyverse/res/mipmap-mdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..505832322 Binary files /dev/null and b/src/en/nollyverse/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/nollyverse/res/mipmap-xhdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..62f929f72 Binary files /dev/null and b/src/en/nollyverse/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/nollyverse/res/mipmap-xxhdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..f5e1c241b Binary files /dev/null and b/src/en/nollyverse/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/nollyverse/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..beeb4e3c6 Binary files /dev/null and b/src/en/nollyverse/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/nollyverse/res/web_hi_res_512.png b/src/en/nollyverse/res/web_hi_res_512.png new file mode 100644 index 000000000..cd31026f1 Binary files /dev/null and b/src/en/nollyverse/res/web_hi_res_512.png differ diff --git a/src/en/nollyverse/src/eu/kanade/tachiyomi/animeextension/en/nollyverse/NollyVerse.kt b/src/en/nollyverse/src/eu/kanade/tachiyomi/animeextension/en/nollyverse/NollyVerse.kt new file mode 100644 index 000000000..946a30ce5 --- /dev/null +++ b/src/en/nollyverse/src/eu/kanade/tachiyomi/animeextension/en/nollyverse/NollyVerse.kt @@ -0,0 +1,579 @@ +package eu.kanade.tachiyomi.animeextension.en.nollyverse + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +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.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.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.FormBody +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 + +class NollyVerse : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "NollyVerse" + + override val baseUrl = "https://www.nollyverse.com" + + override val lang = "en" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // Popular Anime + + private fun toImgUrl(inputUrl: String): String { + val url = inputUrl.removeSuffix("/").toHttpUrl() + val pathSeg = url.encodedPathSegments.toMutableList() + pathSeg.add(1, "img") + return url.scheme + + "://" + + url.host + + "/" + + pathSeg.joinToString(separator = "/") + + ".jpg" + } + + override fun popularAnimeNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)" + + override fun popularAnimeParse(response: Response): AnimesPage { + val document = response.asJsoup() + + val animes = document.select(popularAnimeSelector()).map { element -> + popularAnimeFromElement(element) + } + + val hasNextPage = popularAnimeNextPageSelector()?.let { selector -> + if (document.select(selector).text() != ">") { + return AnimesPage(animes, false) + } + document.select(selector).first() + } != null + + return AnimesPage(animes, hasNextPage) + } + + override fun popularAnimeSelector(): String = "div.col-md-8 div.row div.col-md-6" + + override fun popularAnimeRequest(page: Int): Request { + return GET("$baseUrl/category/trending-movies/page/$page/") + } + + override fun popularAnimeFromElement(element: Element): SAnime { + val anime = SAnime.create() + anime.title = element.select("div.post-body h3 a").text() + anime.thumbnail_url = element.select("a.post-img img").attr("data-src") + anime.setUrlWithoutDomain(element.select("a.post-img").attr("href").toHttpUrl().encodedPath) + return anime + } + + // Episodes + + override fun episodeListRequest(anime: SAnime): Request { + return if (anime.url.startsWith("/movie/")) { + GET(baseUrl + anime.url + "/download/", headers) + } else { + GET(baseUrl + anime.url + "/seasons/", headers) + } + } + + override fun episodeListSelector() = throw Exception("not used") + + override fun episodeListParse(response: Response): List { + val path = response.request.url.encodedPath + + val document = response.asJsoup() + val episodeList = mutableListOf() + + if (path.startsWith("/movie/")) { + val episode = SEpisode.create() + episode.name = "Movie" + episode.episode_number = 1F + episode.setUrlWithoutDomain(path) + episodeList.add(episode) + } else { + var counter = 1 + for (season in document.select("table.table.table-striped tbody tr").reversed()) { + val seasonUrl = season.select("td a[href]").attr("href") + val seasonSoup = client.newCall( + GET(seasonUrl, headers) + ).execute().asJsoup() + + val episodeTable = seasonSoup.select("table.table.table-striped") + val seasonNumber = episodeTable.select("thead th").eachText().find { + t -> + """Season (\d+)""".toRegex().matches(t) + }?.split(" ")!![1] + + for (ep in episodeTable.select("tbody tr")) { + val episode = SEpisode.create() + + episode.name = "Episode S${seasonNumber}E${ep.selectFirst("td").text().split(" ")!![1]}" + episode.episode_number = counter.toFloat() + episode.setUrlWithoutDomain(seasonUrl + "#$counter") + episodeList.add(episode) + + counter++ + } + + // Stop API abuse + Thread.sleep(500) + } + } + + return episodeList.reversed() + } + + override fun episodeFromElement(element: Element): SEpisode { + val episode = SEpisode.create() + episode.episode_number = element.select("td > span.Num").text().toFloat() + val seasonNum = element.ownerDocument().select("div.Title span").text() + episode.name = "Season $seasonNum" + "x" + element.select("td span.Num").text() + " : " + element.select("td.MvTbTtl > a").text() + episode.setUrlWithoutDomain(element.select("td.MvTbPly > a.ClA").attr("abs:href")) + return episode + } + + // Video urls + override fun videoListRequest(episode: SEpisode): Request { + return if (episode.name == "Movie") { + GET(baseUrl + episode.url + "#movie", headers) + } else { + val episodeIndex = """Episode S(\d+)E(?\d+)""".toRegex().matchEntire( + episode.name + )!!.groups["num"]!!.value + GET(baseUrl + episode.url.replaceAfterLast("#", "") + episodeIndex, headers) + } + } + + override fun videoListParse(response: Response): List