diff --git a/src/es/verseriesonline/AndroidManifest.xml b/src/es/verseriesonline/AndroidManifest.xml new file mode 100644 index 0000000000..55938ea165 --- /dev/null +++ b/src/es/verseriesonline/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/es/verseriesonline/build.gradle b/src/es/verseriesonline/build.gradle new file mode 100644 index 0000000000..355f0d153b --- /dev/null +++ b/src/es/verseriesonline/build.gradle @@ -0,0 +1,16 @@ +ext { + extName = 'VerSeriesOnline' + extClass = '.VerSeriesOnline' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(':lib:streamwish-extractor')) + implementation(project(':lib:streamtape-extractor')) + implementation(project(':lib:dood-extractor')) + implementation(project(':lib:voe-extractor')) + implementation(project(':lib:uqload-extractor')) + implementation(project(':lib:vudeo-extractor')) +} \ No newline at end of file diff --git a/src/es/verseriesonline/res/mipmap-hdpi/ic_launcher.png b/src/es/verseriesonline/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..eb4a077b80 Binary files /dev/null and b/src/es/verseriesonline/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/es/verseriesonline/res/mipmap-mdpi/ic_launcher.png b/src/es/verseriesonline/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..eaaf053476 Binary files /dev/null and b/src/es/verseriesonline/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/es/verseriesonline/res/mipmap-xhdpi/ic_launcher.png b/src/es/verseriesonline/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..da24582313 Binary files /dev/null and b/src/es/verseriesonline/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/es/verseriesonline/res/mipmap-xxhdpi/ic_launcher.png b/src/es/verseriesonline/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..7981f8b674 Binary files /dev/null and b/src/es/verseriesonline/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/es/verseriesonline/res/mipmap-xxxhdpi/ic_launcher.png b/src/es/verseriesonline/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..4acac13643 Binary files /dev/null and b/src/es/verseriesonline/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/es/verseriesonline/src/eu/kanade/tachiyomi/animeextension/es/verseriesonline/VerSeriesOnline.kt b/src/es/verseriesonline/src/eu/kanade/tachiyomi/animeextension/es/verseriesonline/VerSeriesOnline.kt new file mode 100644 index 0000000000..c27cb2d283 --- /dev/null +++ b/src/es/verseriesonline/src/eu/kanade/tachiyomi/animeextension/es/verseriesonline/VerSeriesOnline.kt @@ -0,0 +1,431 @@ +package eu.kanade.tachiyomi.animeextension.es.verseriesonline + +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.lib.doodextractor.DoodExtractor +import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor +import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor +import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor +import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor +import eu.kanade.tachiyomi.lib.vudeoextractor.VudeoExtractor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Cookie +import okhttp3.FormBody +import okhttp3.Request +import okhttp3.Response +import org.json.JSONObject +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class VerSeriesOnline : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "VerSeriesOnline" + + override val baseUrl = "https://www.verseriesonline.net" + + override val lang = "es" + + override val supportsLatest = false + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override fun popularAnimeRequest(page: Int): Request { + return GET("$baseUrl/series-online/page/$page", headers) + } + + override fun popularAnimeSelector(): String { + return "div.short.gridder-list" + } + + override fun popularAnimeFromElement(element: Element): SAnime { + val anime = SAnime.create() + anime.setUrlWithoutDomain(element.selectFirst("a.short_img")!!.attr("href")) + anime.title = element.selectFirst("div.short_title a")!!.text() + val image = element.selectFirst("a.short_img img")!!.attr("data-src") + anime.thumbnail_url = "$baseUrl/$image" + return anime + } + + override fun popularAnimeNextPageSelector(): String { + return ".navigation a:last-of-type" + } + + override fun latestUpdatesRequest(page: Int): Request { + throw UnsupportedOperationException() + } + + override fun latestUpdatesSelector(): String { + throw UnsupportedOperationException() + } + + override fun latestUpdatesFromElement(element: Element): SAnime { + throw UnsupportedOperationException() + } + + override fun latestUpdatesNextPageSelector(): String? { + throw UnsupportedOperationException() + } + + override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage { + return if (query.startsWith(PREFIX_SEARCH)) { + val id = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/recherche?q=$id", headers)) + .awaitSuccess() + .use(::searchAnimeByIdParse) + } else { + val url = buildSearchUrl(query, page, filters) + client.newCall(GET(url, headers)).awaitSuccess().use { response -> + val document = response.asJsoup() + val animeList = document.select(searchAnimeSelector()).map { element -> + searchAnimeFromElement(element) + } + val hasNextPage = searchAnimeNextPageSelector().let { + document.select(it).isNotEmpty() + } + + AnimesPage(animeList, hasNextPage) + } + } + } + + private fun buildSearchUrl(query: String, page: Int, filters: AnimeFilterList): String { + val genreFilter = filters.find { it is GenreFilter } as? GenreFilter + val yearFilter = filters.find { it is YearFilter } as? YearFilter + val genre = genreFilter?.toUriPart() ?: "" + val year = yearFilter?.toUriPart() ?: "" + + return if (query.isNotEmpty()) { + "$baseUrl/recherche?q=$query&page=$page" + } else if (year != "" && genre == "") { + "$baseUrl/series-online/ano/$year/page/$page" + } else { + "$baseUrl/series-online/genero/$genre/page/$page" + } + } + + private fun searchAnimeByIdParse(response: Response): AnimesPage { + val details = animeDetailsParse(response.asJsoup()) + .apply { + setUrlWithoutDomain(response.request.url.toString()) + initialized = true + } + return AnimesPage(listOf(details), false) + } + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + return GET("$baseUrl/recherche?q=$query&page=$page", headers) + } + + override fun searchAnimeSelector(): String { + return popularAnimeSelector() + } + + override fun searchAnimeFromElement(element: Element): SAnime { + return popularAnimeFromElement(element) + } + + override fun searchAnimeNextPageSelector(): String { + return popularAnimeNextPageSelector() + } + + override fun animeDetailsParse(document: Document): SAnime { + val anime = SAnime.create() + + anime.thumbnail_url = document.selectFirst("img.lazy-loaded")?.attr("data-src") + anime.description = document.selectFirst("div.full_content-desc p span")?.text() ?: "Descripción no encontrada" + anime.genre = document.select("ul#full_info li.vis span:contains(Genre:) + a") + .joinToString(", ") { it.text() } + anime.author = document.select("ul#full_info li.vis span:contains(Director:) + a").text() + anime.status = SAnime.UNKNOWN + + return anime + } + + override fun episodeListSelector(): String { + return "div.seasontab div.floats a.th-hover" + } + + override fun episodeFromElement(element: Element): SEpisode { + throw UnsupportedOperationException() + } + + private fun seasonListSelector(): String { + return "div.floats a" + } + + private fun seasonEpisodesSelector(): String { + return "#dle-content > article > div > div:nth-child(3) > div > div > a" + } + + override fun episodeListParse(response: Response): List { + val document = response.asJsoup() + val episodeList = mutableListOf() + + document.select(seasonListSelector()).forEach { seasonElement -> + val seasonUrl = seasonElement.attr("href") + val seasonNumber = Regex("temporada-(\\d+)").find(seasonUrl)?.groups?.get(1)?.value?.toIntOrNull() ?: 1 + val seasonDocument = client.newCall(GET(seasonUrl)).execute().asJsoup() + + seasonDocument.select(seasonEpisodesSelector()).forEach { episodeElement -> + val episode = SEpisode.create() + val episodeUrl = episodeElement.attr("href") + episode.setUrlWithoutDomain(episodeUrl) + val episodeName = episodeElement.selectFirst("span.name")?.text()?.trim() ?: "Episodio desconocido" + episode.name = "Temporada $seasonNumber - $episodeName" + val episodeNumber = Regex("Capítulo (\\d+)").find(episodeName)?.groups?.get(1)?.value?.toFloatOrNull() ?: 0F + episode.episode_number = episodeNumber + episodeList.add(episode) + } + } + + return episodeList + } + + private val doodExtractor by lazy { DoodExtractor(client) } + private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) } + private val streamtapeExtractor by lazy { StreamTapeExtractor(client) } + private val voeExtractor by lazy { VoeExtractor(client) } + private val uqloadExtractor by lazy { UqloadExtractor(client) } + private val vudeoExtractor by lazy { VudeoExtractor(client) } + + override fun videoListParse(response: Response): List