diff --git a/src/pt/puraymoe/AndroidManifest.xml b/src/pt/puraymoe/AndroidManifest.xml
new file mode 100644
index 000000000..eab6844c7
--- /dev/null
+++ b/src/pt/puraymoe/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pt/puraymoe/build.gradle b/src/pt/puraymoe/build.gradle
new file mode 100644
index 000000000..8b9053aa8
--- /dev/null
+++ b/src/pt/puraymoe/build.gradle
@@ -0,0 +1,18 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Puray.moe'
+ pkgNameSuffix = 'pt.puraymoe'
+ extClass = '.PurayMoe'
+ extVersionCode = 1
+ libVersion = '12'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+}
+
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/pt/puraymoe/res/mipmap-hdpi/ic_launcher.png b/src/pt/puraymoe/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..6a0309e94
Binary files /dev/null and b/src/pt/puraymoe/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/puraymoe/res/mipmap-mdpi/ic_launcher.png b/src/pt/puraymoe/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c0b840f45
Binary files /dev/null and b/src/pt/puraymoe/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/puraymoe/res/mipmap-xhdpi/ic_launcher.png b/src/pt/puraymoe/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..8d7f710d7
Binary files /dev/null and b/src/pt/puraymoe/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/puraymoe/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/puraymoe/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d45510f84
Binary files /dev/null and b/src/pt/puraymoe/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/puraymoe/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/puraymoe/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..49aa0032b
Binary files /dev/null and b/src/pt/puraymoe/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/puraymoe/src/eu/kanade/tachiyomi/animeextension/pt/puraymoe/PMUrlActivity.kt b/src/pt/puraymoe/src/eu/kanade/tachiyomi/animeextension/pt/puraymoe/PMUrlActivity.kt
new file mode 100644
index 000000000..79c19cbfd
--- /dev/null
+++ b/src/pt/puraymoe/src/eu/kanade/tachiyomi/animeextension/pt/puraymoe/PMUrlActivity.kt
@@ -0,0 +1,42 @@
+package eu.kanade.tachiyomi.animeextension.pt.puraymoe
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import kotlin.system.exitProcess
+
+/**
+ * Springboard that accepts https://puray.moe/anime/ intents
+ * and redirects them to the main Aniyomi process.
+ */
+class PMUrlActivity : Activity() {
+
+ private val TAG = "PMUrlActivity"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val pathSegments = intent?.data?.pathSegments
+ if (pathSegments != null && pathSegments.size > 1) {
+ val id = pathSegments[1]
+ val searchQuery = PurayMoe.PREFIX_SEARCH + id
+ val mainIntent = Intent().apply {
+ action = "eu.kanade.tachiyomi.ANIMESEARCH"
+ putExtra("query", searchQuery)
+ putExtra("filter", packageName)
+ }
+
+ try {
+ startActivity(mainIntent)
+ } catch (e: ActivityNotFoundException) {
+ Log.e(TAG, e.toString())
+ }
+ } else {
+ Log.e(TAG, "could not parse uri from intent $intent")
+ }
+
+ finish()
+ exitProcess(0)
+ }
+}
diff --git a/src/pt/puraymoe/src/eu/kanade/tachiyomi/animeextension/pt/puraymoe/PurayMoe.kt b/src/pt/puraymoe/src/eu/kanade/tachiyomi/animeextension/pt/puraymoe/PurayMoe.kt
new file mode 100644
index 000000000..43b20eb0b
--- /dev/null
+++ b/src/pt/puraymoe/src/eu/kanade/tachiyomi/animeextension/pt/puraymoe/PurayMoe.kt
@@ -0,0 +1,324 @@
+package eu.kanade.tachiyomi.animeextension.pt.puraymoe
+
+import android.app.Application
+import android.content.SharedPreferences
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.animeextension.pt.puraymoe.dto.AnimeDto
+import eu.kanade.tachiyomi.animeextension.pt.puraymoe.dto.EpisodeDataDto
+import eu.kanade.tachiyomi.animeextension.pt.puraymoe.dto.MinimalEpisodeDto
+import eu.kanade.tachiyomi.animeextension.pt.puraymoe.dto.SearchDto
+import eu.kanade.tachiyomi.animeextension.pt.puraymoe.dto.SeasonInfoDto
+import eu.kanade.tachiyomi.animeextension.pt.puraymoe.dto.SeasonListDto
+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.AnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.Headers
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.lang.Exception
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class PurayMoe : ConfigurableAnimeSource, AnimeHttpSource() {
+
+ override val name = "Puray.moe"
+
+ override val baseUrl = "https://puray.moe"
+
+ override val lang = "pt-BR"
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ private val json = Json {
+ ignoreUnknownKeys = true
+ }
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder()
+ .add("Referer", baseUrl)
+ .add("Accept-Language", ACCEPT_LANGUAGE)
+
+ // ============================== Popular ===============================
+
+ override fun popularAnimeRequest(page: Int): Request =
+ GET("$API_URL/animes/genero/25/")
+
+ override fun popularAnimeParse(response: Response): AnimesPage {
+ val animeList = response.parseAs>()
+ val animes = animeList.map(::animeDetailsFromObject).toList()
+ return AnimesPage(animes, false)
+ }
+
+ // ============================== Episodes ==============================
+
+ private fun getSeasonList(anime: SAnime): SeasonListDto {
+ val id = anime.url.getId()
+ val request = GET("$API_URL/temporadas/?anime__id_animes=$id")
+ val response = client.newCall(request).execute()
+ return response.parseAs()
+ }
+
+ override fun fetchEpisodeList(anime: SAnime): Observable> {
+ val seasonsList: SeasonListDto = getSeasonList(anime)
+
+ val showOnly = preferences.getString(CONF_SHOW_ONLY, null) ?: ""
+ val dub_item = ANIME_TYPES_VALUES.elementAt(1)
+ val sub_item = ANIME_TYPES_VALUES.last()
+ var filteredSeasons = seasonsList.seasons.filter {
+ val lowerName = it.name.lowercase()
+ when (showOnly) {
+ dub_item -> lowerName.contains(dub_item)
+ sub_item -> !lowerName.contains(dub_item)
+ else -> true
+ }
+ }
+ if (filteredSeasons.size < 1) filteredSeasons = seasonsList.seasons
+
+ val episodeList = mutableListOf()
+ filteredSeasons.reversed().forEach {
+ val request: Request = episodeListRequest(it.id)
+ val response: Response = client.newCall(request).execute()
+ val season_episodes = episodeListParse(response, it)
+ episodeList.addAll(season_episodes.reversed())
+ }
+ return Observable.just(episodeList)
+ }
+
+ override fun episodeListRequest(anime: SAnime): Request =
+ throw Exception("not used")
+
+ private fun episodeListRequest(season_id: Int): Request =
+ GET("$API_URL/episodios/?temporada__id_temporadas=$season_id")
+
+ override fun episodeListParse(response: Response) = throw Exception("not used")
+
+ private fun episodeListParse(response: Response, season: SeasonInfoDto): List {
+ val episodesData = response.parseAs()
+ val seasonNumber = if (season.number.equals("0")) "1" else season.number
+ val format = if ("dub" in season.name.lowercase()) "DUBLADO" else "LEGENDADO"
+ return episodesData.episodes.map {
+ val episode = SEpisode.create()
+ episode.name = "Temp $seasonNumber ($format) EP ${it.ep_number}: ${it.name}"
+ episode.episode_number = try {
+ it.ep_number.toFloat()
+ } catch (e: NumberFormatException) { 0F }
+ episode.url = it.id.toString()
+ episode.date_upload = it.release_date.toDate()
+ episode
+ }.toList()
+ }
+
+ // ============================ Video Links =============================
+
+ override fun videoListRequest(episode: SEpisode): Request =
+ GET("$API_URL/episodios/${episode.url}/m3u8/mp4/")
+
+ override fun videoListParse(response: Response): List