diff --git a/src/all/mangadex/build.gradle b/src/all/mangadex/build.gradle
index 2b99a01ca..54a5cc5d7 100644
--- a/src/all/mangadex/build.gradle
+++ b/src/all/mangadex/build.gradle
@@ -6,7 +6,7 @@ ext {
     extName = 'MangaDex'
     pkgNameSuffix = 'all.mangadex'
     extClass = '.MangaDexFactory'
-    extVersionCode = 170
+    extVersionCode = 171
     isNsfw = true
 }
 
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt
index 0161eda47..a4fdc068d 100644
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt
+++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt
@@ -108,9 +108,18 @@ object MDConstants {
         return "${hasSanitizedUuidsPref}_$dexLang"
     }
 
+    private const val tryUsingFirstVolumeCoverPref = "tryUsingFirstVolumeCover"
+    const val tryUsingFirstVolumeCoverDefault = false
+    fun getTryUsingFirstVolumeCoverPrefKey(dexLang: String): String {
+        return "${tryUsingFirstVolumeCoverPref}_$dexLang"
+    }
+
     private const val tagGroupContent = "content"
     private const val tagGroupFormat = "format"
     private const val tagGroupGenre = "genre"
     private const val tagGroupTheme = "theme"
     val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme)
+
+    const val tagAnthologyUuid = "51d83883-4103-437c-b4b1-731cb73d786c"
+    const val tagOneShotUuid = "0234a31e-a729-4e28-9d6a-3f87c4966b9e"
 }
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt
index 069a5c688..0c59e0640 100644
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt
+++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt
@@ -9,10 +9,13 @@ import androidx.preference.MultiSelectListPreference
 import androidx.preference.PreferenceScreen
 import androidx.preference.SwitchPreferenceCompat
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateDto
+import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDto
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterListDto
+import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverListDto
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListDto
+import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDataDto
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDto
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaListDto
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.RelationshipDto
@@ -26,7 +29,6 @@ import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.source.model.SChapter
 import eu.kanade.tachiyomi.source.model.SManga
 import eu.kanade.tachiyomi.source.online.HttpSource
-import kotlinx.serialization.SerializationException
 import kotlinx.serialization.decodeFromString
 import okhttp3.CacheControl
 import okhttp3.Headers
@@ -97,9 +99,10 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
         val hasMoreResults = mangaListDto.limit + mangaListDto.offset < mangaListDto.total
 
         val coverSuffix = preferences.coverQuality
+        val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty()
 
         val mangaList = mangaListDto.data.map { mangaDataDto ->
-            val fileName = mangaDataDto.relationships
+            val fileName = firstVolumeCovers[mangaDataDto.id] ?: mangaDataDto.relationships
                 .firstOrNull { it.type.equals(MDConstants.coverArt, true) }
                 ?.attributes?.fileName
             helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
@@ -129,13 +132,14 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
         val mangaRequest = GET(mangaUrl.build().toString(), headers, CacheControl.FORCE_NETWORK)
         val mangaResponse = client.newCall(mangaRequest).execute()
         val mangaListDto = mangaResponse.parseAs<MangaListDto>()
+        val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty()
 
         val mangaDtoMap = mangaListDto.data.associateBy({ it.id }, { it })
 
         val coverSuffix = preferences.coverQuality
 
         val mangaList = mangaIds.mapNotNull { mangaDtoMap[it] }.map { mangaDataDto ->
-            val fileName = mangaDataDto.relationships
+            val fileName = firstVolumeCovers[mangaDataDto.id] ?: mangaDataDto.relationships
                 .firstOrNull { it.type.equals(MDConstants.coverArt, true) }
                 ?.attributes?.fileName
             helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
@@ -182,7 +186,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
                     .map { searchMangaListRequest(it, page) }
 
             else ->
-                return super.fetchSearchManga(page, query, filters)
+                return super.fetchSearchManga(page, query.trim(), filters)
         }
     }
 
@@ -292,11 +296,12 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
         }
 
         val mangaListDto = response.parseAs<MangaListDto>()
+        val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty()
 
         val coverSuffix = preferences.coverQuality
 
         val mangaList = mangaListDto.data.map { mangaDataDto ->
-            val fileName = mangaDataDto.relationships
+            val fileName = firstVolumeCovers[mangaDataDto.id] ?: mangaDataDto.relationships
                 .firstOrNull { it.type.equals(MDConstants.coverArt, true) }
                 ?.attributes?.fileName
             helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
@@ -365,33 +370,74 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
         return helper.createManga(
             manga.data,
             fetchSimpleChapterList(manga, dexLang),
+            fetchFirstVolumeCover(manga),
             dexLang,
             preferences.coverQuality
         )
     }
 
     /**
-     * get a quick-n-dirty list of the chapters to be used in determining the manga status.
-     * uses the 'aggregate' endpoint
+     * Get a quick-n-dirty list of the chapters to be used in determining the manga status.
+     * Uses the 'aggregate' endpoint.
+     *
      * @see MangaDexHelper.getPublicationStatus
      * @see AggregateDto
      */
-    private fun fetchSimpleChapterList(manga: MangaDto, langCode: String): List<String> {
+    private fun fetchSimpleChapterList(manga: MangaDto, langCode: String): Map<String, AggregateVolume> {
         val url = "${MDConstants.apiMangaUrl}/${manga.data.id}/aggregate?translatedLanguage[]=$langCode"
         val response = client.newCall(GET(url, headers)).execute()
-        val chapters: AggregateDto
 
-        try {
-            chapters = response.parseAs()
-        } catch (e: SerializationException) {
-            return emptyList()
+        return runCatching { response.parseAs<AggregateDto>() }
+            .getOrNull()?.volumes.orEmpty()
+    }
+
+    /**
+     * Attempt to get the first volume cover if the setting is enabled.
+     * Uses the 'covers' endpoint.
+     *
+     * @see CoverListDto
+     */
+    private fun fetchFirstVolumeCover(manga: MangaDto): String? {
+        return fetchFirstVolumeCovers(listOf(manga.data))?.get(manga.data.id)
+    }
+
+    /**
+     * Attempt to get the first volume cover if the setting is enabled.
+     * Uses the 'covers' endpoint.
+     *
+     * @see CoverListDto
+     */
+    private fun fetchFirstVolumeCovers(mangaList: List<MangaDataDto>): Map<String, String>? {
+        if (!preferences.tryUsingFirstVolumeCover) {
+            return null
         }
 
-        if (chapters.volumes.isNullOrEmpty()) return emptyList()
+        val mangaMap = mangaList.associate { it.id to it.attributes }
+            .filterValues { !it.originalLanguage.isNullOrEmpty() }
+        val locales = mangaList.mapNotNull { it.attributes.originalLanguage }.distinct()
+        val limit = (mangaMap.size * locales.size).coerceAtMost(100)
 
-        return chapters.volumes.values
-            .flatMap { it.chapters.values }
-            .map { it.chapter }
+        val apiUrl = "${MDConstants.apiUrl}/cover".toHttpUrl().newBuilder()
+            .addQueryParameter("order[volume]", "asc")
+            .addQueryParameter("manga[]", mangaMap.keys)
+            .addQueryParameter("locales[]", locales.toSet())
+            .addQueryParameter("limit", limit.toString())
+            .addQueryParameter("offset", "0")
+            .toString()
+
+        val result = runCatching {
+            client.newCall(GET(apiUrl, headers)).execute().parseAs<CoverListDto>().data
+        }
+
+        val covers = result.getOrNull() ?: return null
+
+        return covers
+            .groupBy { it.relationships.find { r -> r.type == MDConstants.manga }!!.id }
+            .mapValues {
+                it.value.find { c -> c.attributes?.locale == mangaMap[it.key]?.originalLanguage }
+            }
+            .filterValues { !it?.attributes?.fileName.isNullOrEmpty() }
+            .mapValues { it.value!!.attributes!!.fileName!! }
     }
 
     // Chapter list section
@@ -525,6 +571,21 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
             }
         }
 
+        val tryUsingFirstVolumeCoverPref = SwitchPreferenceCompat(screen.context).apply {
+            key = MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang)
+            title = helper.intl.tryUsingFirstVolumeCover
+            summary = helper.intl.tryUsingFirstVolumeCoverSummary
+            setDefaultValue(MDConstants.tryUsingFirstVolumeCoverDefault)
+
+            setOnPreferenceChangeListener { _, newValue ->
+                val checkValue = newValue as Boolean
+
+                preferences.edit()
+                    .putBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), checkValue)
+                    .commit()
+            }
+        }
+
         val dataSaverPref = SwitchPreferenceCompat(screen.context).apply {
             key = MDConstants.getDataSaverPreferenceKey(dexLang)
             title = helper.intl.dataSaver
@@ -636,6 +697,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
         }
 
         screen.addPreference(coverQualityPref)
+        screen.addPreference(tryUsingFirstVolumeCoverPref)
         screen.addPreference(dataSaverPref)
         screen.addPreference(standardHttpsPortPref)
         screen.addPreference(contentRatingPref)
@@ -680,6 +742,12 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
     private val SharedPreferences.coverQuality
         get() = getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "")
 
+    private val SharedPreferences.tryUsingFirstVolumeCover
+        get() = getBoolean(
+            MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang),
+            MDConstants.tryUsingFirstVolumeCoverDefault
+        )
+
     private val SharedPreferences.blockedGroups
         get() = getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")
             ?.split(",")
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt
index 681443916..0fc169e05 100644
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt
+++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt
@@ -5,18 +5,18 @@ import android.text.TextWatcher
 import android.util.Log
 import android.widget.Button
 import android.widget.EditText
+import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDataDto
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaAttributesDto
 import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDataDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.asMdMap
+import eu.kanade.tachiyomi.extension.all.mangadex.dto.toLocalizedString
 import eu.kanade.tachiyomi.network.GET
 import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.source.model.SChapter
 import eu.kanade.tachiyomi.source.model.SManga
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.jsonArray
 import okhttp3.CacheControl
 import okhttp3.Headers
 import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -27,7 +27,7 @@ import java.util.Date
 import java.util.Locale
 import java.util.concurrent.TimeUnit
 
-class MangaDexHelper(private val lang: String) {
+class MangaDexHelper(lang: String) {
 
     val mdFilters = MangaDexFilters()
 
@@ -98,7 +98,11 @@ class MangaDexHelper(private val lang: String) {
      * Maps dex status to Tachi status.
      * Adapted from the MangaDex handler from TachiyomiSY.
      */
-    fun getPublicationStatus(attr: MangaAttributesDto, chapters: List<String>): Int {
+    fun getPublicationStatus(attr: MangaAttributesDto, volumes: Map<String, AggregateVolume>): Int {
+        val chaptersList = volumes.values
+            .flatMap { it.chapters.values }
+            .map { it.chapter }
+
         val tempStatus = when (attr.status) {
             "ongoing" -> SManga.ONGOING
             "cancelled" -> SManga.CANCELLED
@@ -110,10 +114,13 @@ class MangaDexHelper(private val lang: String) {
         val publishedOrCancelled = tempStatus == SManga.PUBLISHING_FINISHED ||
             tempStatus == SManga.CANCELLED
 
-        return if (chapters.contains(attr.lastChapter) && publishedOrCancelled) {
-            SManga.COMPLETED
-        } else {
-            tempStatus
+        val isOneShot = attr.tags.any { it.id == MDConstants.tagOneShotUuid } &&
+            attr.tags.none { it.id == MDConstants.tagAnthologyUuid }
+
+        return when {
+            chaptersList.contains(attr.lastChapter) && publishedOrCancelled -> SManga.COMPLETED
+            isOneShot && volumes["none"]?.chapters?.get("none") != null -> SManga.COMPLETED
+            else -> tempStatus
         }
     }
 
@@ -210,15 +217,14 @@ class MangaDexHelper(private val lang: String) {
     ): SManga {
         return SManga.create().apply {
             url = "/manga/${mangaDataDto.id}"
-            val titleMap = mangaDataDto.attributes.title.asMdMap()
+            val titleMap = mangaDataDto.attributes.title.toLocalizedString()
             val dirtyTitle = titleMap[lang]
                 ?: titleMap["en"]
                 ?: titleMap["ja-ro"]
-                ?: mangaDataDto.attributes.altTitles.jsonArray
-                    .find {
-                        val altTitle = it.asMdMap()
-                        (altTitle[lang] ?: altTitle["en"]) != null
-                    }?.asMdMap()?.values?.singleOrNull()
+                ?: mangaDataDto.attributes.altTitles
+                    .map { it.toLocalizedString() }
+                    .find { (it[lang] ?: it["en"]) !== null }
+                    ?.values?.singleOrNull()
                 ?: titleMap["ja"] // romaji titles are sometimes ja (and are not altTitles)
                 ?: titleMap.values.firstOrNull() // use literally anything from title as a last resort
             title = cleanString(dirtyTitle ?: "")
@@ -237,7 +243,8 @@ class MangaDexHelper(private val lang: String) {
      */
     fun createManga(
         mangaDataDto: MangaDataDto,
-        chapters: List<String>,
+        chapters: Map<String, AggregateVolume>,
+        firstVolumeCover: String?,
         lang: String,
         coverSuffix: String?
     ): SManga {
@@ -275,7 +282,7 @@ class MangaDexHelper(private val lang: String) {
                 .mapNotNull { it.attributes!!.name }
                 .distinct()
 
-            val coverFileName = mangaDataDto.relationships
+            val coverFileName = firstVolumeCover ?: mangaDataDto.relationships
                 .firstOrNull { it.type.equals(MDConstants.coverArt, true) }
                 ?.attributes?.fileName
 
@@ -293,7 +300,7 @@ class MangaDexHelper(private val lang: String) {
             val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } +
                 nonGenres.filterNotNull()
 
-            val desc = attr.description.asMdMap()
+            val desc = attr.description.toLocalizedString()
 
             return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply {
                 description = cleanString(desc[lang] ?: desc["en"] ?: "")
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt
index 63f71122f..7f25aed19 100644
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt
+++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt
@@ -96,18 +96,18 @@ class MangaDexIntl(lang: String) {
         BRAZILIAN_PORTUGUESE, PORTUGUESE ->
             "Ative para fazer requisições em somente servidores de imagem que usem a porta 443. " +
                 "Isso permite com que usuários com regras mais restritas de firewall possam acessar " +
-                "as imagens do MangaDex."
+                "as imagens do $MANGADEX_NAME."
         SPANISH_LATAM, SPANISH ->
             "Habilite esta opción solicitar las imágenes a los servidores que usan el puerto 443. " +
                 "Esto permite a los usuarios con restricciones estrictas de firewall acceder " +
-                "a las imagenes en MangaDex"
+                "a las imagenes en $MANGADEX_NAME"
         RUSSIAN ->
             "Запрашивает изображения только с серверов которые используют порт 443. " +
                 "Это позволяет пользователям со строгими правилами брандмауэра загружать " +
-                "изображения с Mangadex."
+                "изображения с $MANGADEX_NAME."
         else ->
             "Enable to only request image servers that use port 443. This allows users with " +
-                "stricter firewall restrictions to access MangaDex images"
+                "stricter firewall restrictions to access $MANGADEX_NAME images"
     }
 
     val contentRating: String = when (availableLang) {
@@ -265,6 +265,20 @@ class MangaDexIntl(lang: String) {
                 "Enter as a Comma-separated list of uploader UUIDs"
     }
 
+    val tryUsingFirstVolumeCover: String = when (availableLang) {
+        BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Tentar usar a capa do primeiro volume como capa"
+        else -> "Attempt to use the first volume cover as cover"
+    }
+
+    val tryUsingFirstVolumeCoverSummary: String = when (availableLang) {
+        BRAZILIAN_PORTUGUESE, PORTUGUESE ->
+            "Pode ser necessário atualizar os itens já adicionados na biblioteca. " +
+                "Alternativamente, limpe o banco de dados para as novas capas aparecerem."
+        else ->
+            "May need to manually refresh entries already in library. " +
+                "Otherwise, clear database to have new covers to show up."
+    }
+
     val publicationDemographic: String = when (availableLang) {
         BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Demografia da publicação"
         SPANISH_LATAM, SPANISH -> "Demografía"
@@ -748,7 +762,7 @@ class MangaDexIntl(lang: String) {
         BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Pós-apocalíptico"
         SPANISH_LATAM, SPANISH -> "Post-Apocalíptico"
         RUSSIAN -> "Постапокалиптика"
-        else -> "Post-Apocalypytic"
+        else -> "Post-Apocalyptic"
     }
 
     val themePsychological: String = when (availableLang) {
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverDto.kt
new file mode 100644
index 000000000..9f8c714a5
--- /dev/null
+++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverDto.kt
@@ -0,0 +1,22 @@
+package eu.kanade.tachiyomi.extension.all.mangadex.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CoverListDto(
+    val data: List<CoverDto> = emptyList()
+)
+
+@Serializable
+data class CoverDto(
+    val id: String,
+    val attributes: CoverAttributesDto? = null,
+    val relationships: List<RelationshipDto> = emptyList()
+)
+
+@Serializable
+data class CoverAttributesDto(
+    val name: String? = null,
+    val fileName: String? = null,
+    val locale: String? = null
+)
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/MangaDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/MangaDto.kt
index 5637f7ff3..78ed3a718 100644
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/MangaDto.kt
+++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/MangaDto.kt
@@ -1,6 +1,7 @@
 package eu.kanade.tachiyomi.extension.all.mangadex.dto
 
 import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonArray
 import kotlinx.serialization.json.JsonElement
 import kotlinx.serialization.json.JsonObject
 import kotlinx.serialization.json.contentOrNull
@@ -45,7 +46,7 @@ data class MangaDataDto(
 @Serializable
 data class MangaAttributesDto(
     val title: JsonElement,
-    val altTitles: JsonElement,
+    val altTitles: JsonArray,
     val description: JsonElement,
     val originalLanguage: String?,
     val lastVolume: String?,
@@ -67,8 +68,14 @@ data class TagAttributesDto(
     val group: String
 )
 
-fun JsonElement.asMdMap(): Map<String, String> {
-    return runCatching {
-        (this as JsonObject).map { it.key to (it.value.jsonPrimitive.contentOrNull ?: "") }.toMap()
-    }.getOrElse { emptyMap() }
+typealias LocalizedString = Map<String, String>
+
+/**
+ * Temporary workaround while Dex API still returns arrays instead of objects
+ * in the places that uses [LocalizedString].
+ */
+fun JsonElement.toLocalizedString(): LocalizedString {
+    return (this as? JsonObject)?.entries
+        ?.associate { (key, value) -> key to (value.jsonPrimitive.contentOrNull ?: "") }
+        .orEmpty()
 }