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 /** * 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 = 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? = 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 = listOf(), total: Int? = null, extra: MutableMap? = null ) : this(name, link, FileUrl(coverUrl), otherNames, total, extra) constructor( name: String, link: String, coverUrl: String, otherNames: List = listOf(), total: Int? = null ) : this(name, link, FileUrl(coverUrl), otherNames, total) constructor(name: String, link: String, coverUrl: String, otherNames: List = 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) }