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