Dantotsu/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt
2024-02-02 14:49:19 -06:00

240 lines
8.4 KiB
Kotlin

package ani.dantotsu.parsers
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.settings.saving.PrefManager
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.io.Serializable
import java.net.URLDecoder
import java.net.URLEncoder
abstract class BaseParser {
/**
* Name that will be shown in Source Selection
* **/
open val name: String = ""
/**
* Name used to save the ShowResponse selected by user or by autoSearch
* **/
open val saveName: String = ""
/**
* The main URL of the Site
* **/
open val hostUrl: String = ""
/**
* override as `true` if the site **only** has NSFW media
* **/
open val isNSFW = false
/**
* mostly redundant for official app, But override if you want to add different languages
* **/
open val language = "English"
/**
* Search for Anime/Manga/Novel, returns a List of Responses
*
* use `encode(query)` to encode the query for making requests
* **/
abstract suspend fun search(query: String): List<ShowResponse>
/**
* The function app uses to auto find the anime/manga using Media data provided by anilist
*
* Isn't necessary to override, but recommended, if you want to improve auto search results
* **/
open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response: ShowResponse? = loadSavedShowResponse(mediaObj.id)
if (response != null && this !is OfflineMangaParser && this !is OfflineAnimeParser) {
saveShowResponse(mediaObj.id, response, true)
} else {
setUserText("Searching : ${mediaObj.mainName()}")
logger("Searching : ${mediaObj.mainName()}")
val results = search(mediaObj.mainName())
//log all results
results.forEach {
logger("Result: ${it.name}")
}
val sortedResults = if (results.isNotEmpty()) {
results.sortedByDescending {
FuzzySearch.ratio(
it.name.lowercase(),
mediaObj.mainName().lowercase()
)
}
} else {
emptyList()
}
response = sortedResults.firstOrNull()
if (response == null || FuzzySearch.ratio(
response.name.lowercase(),
mediaObj.mainName().lowercase()
) < 100
) {
setUserText("Searching : ${mediaObj.nameRomaji}")
logger("Searching : ${mediaObj.nameRomaji}")
val romajiResults = search(mediaObj.nameRomaji)
val sortedRomajiResults = if (romajiResults.isNotEmpty()) {
romajiResults.sortedByDescending {
FuzzySearch.ratio(
it.name.lowercase(),
mediaObj.nameRomaji.lowercase()
)
}
} else {
emptyList()
}
val closestRomaji = sortedRomajiResults.firstOrNull()
logger("Closest match from RomajiResults: ${closestRomaji?.name ?: "None"}")
response = if (response == null) {
logger("No exact match found in results. Using closest match from RomajiResults.")
closestRomaji
} else {
val romajiRatio = FuzzySearch.ratio(
closestRomaji?.name?.lowercase() ?: "",
mediaObj.nameRomaji.lowercase()
)
val mainNameRatio = FuzzySearch.ratio(
response.name.lowercase(),
mediaObj.mainName().lowercase()
)
logger("Fuzzy ratio for closest match in results: $mainNameRatio for ${response.name.lowercase()}")
logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio for ${closestRomaji?.name?.lowercase() ?: "None"}")
if (romajiRatio > mainNameRatio) {
logger("RomajiResults has a closer match. Replacing response.")
closestRomaji
} else {
logger("Results has a closer or equal match. Keeping existing response.")
response
}
}
}
saveShowResponse(mediaObj.id, response)
}
return response
}
/**
* Used to get an existing Search Response which was selected by the user.
* **/
open suspend fun loadSavedShowResponse(mediaId: Int): ShowResponse? {
checkIfVariablesAreEmpty()
return PrefManager.getNullableCustomVal("${saveName}_$mediaId", null, ShowResponse::class.java)
}
/**
* Used to save Shows Response using `saveName`.
* **/
open fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean = false) {
if (response != null) {
checkIfVariablesAreEmpty()
setUserText(
"${
if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(
R.string.found
)
} : ${response.name}"
)
PrefManager.setCustomVal("${saveName}_$mediaId", response)
}
}
fun checkIfVariablesAreEmpty() {
if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
if (name.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
}
open var showUserText = ""
open var showUserTextListener: ((String) -> Unit)? = null
/**
* Used to show messages & errors to the User, a useful way to convey what's currently happening or what was done.
* **/
fun setUserText(string: String) {
showUserText = string
showUserTextListener?.invoke(showUserText)
}
fun encode(input: String): String = URLEncoder.encode(input, "utf-8").replace("+", "%20")
fun decode(input: String): String = URLDecoder.decode(input, "utf-8")
val defaultImage = "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/default.jpg"
}
/**
* A single show which contains some episodes/chapters which is sent by the site using their search function.
*
* You might wanna include `otherNames` & `total` too, to further improve user experience.
*
* You can also store a Map of Strings if you want to save some extra data.
* **/
data class ShowResponse(
val name: String,
val link: String,
val coverUrl: FileUrl,
//would be Useful for custom search, ig
val otherNames: List<String> = listOf(),
//Total number of Episodes/Chapters in the show.
val total: Int? = null,
//In case you want to sent some extra data
val extra: MutableMap<String, String>? = null,
//SAnime object from Aniyomi
val sAnime: SAnime? = null,
//SManga object from Aniyomi
val sManga: SManga? = null
) : Serializable {
constructor(
name: String,
link: String,
coverUrl: String,
otherNames: List<String> = listOf(),
total: Int? = null,
extra: MutableMap<String, String>? = null
)
: this(name, link, FileUrl(coverUrl), otherNames, total, extra)
constructor(
name: String,
link: String,
coverUrl: String,
otherNames: List<String> = listOf(),
total: Int? = null
)
: this(name, link, FileUrl(coverUrl), otherNames, total)
constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf())
: this(name, link, FileUrl(coverUrl), otherNames)
constructor(name: String, link: String, coverUrl: String)
: this(name, link, FileUrl(coverUrl))
constructor(name: String, link: String, coverUrl: String, sAnime: SAnime)
: this(name, link, FileUrl(coverUrl), sAnime = sAnime)
constructor(name: String, link: String, coverUrl: String, sManga: SManga)
: this(name, link, FileUrl(coverUrl), sManga = sManga)
}