Move things back into the EH package, no need for them to be in the regular app

This commit is contained in:
Jobobby04
2020-04-21 17:37:03 -04:00
parent b4ade8c15d
commit a393772083
16 changed files with 43 additions and 37 deletions
@@ -2,7 +2,6 @@ package exh.debug
import android.annotation.SuppressLint
import android.app.Activity
import android.text.Html
import android.util.Log
import android.widget.HorizontalScrollView
import android.widget.TextView
@@ -1,7 +1,6 @@
package exh.favorites
import android.content.Context
import android.text.Html
import androidx.core.text.HtmlCompat
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -1,7 +1,6 @@
package exh.log
import android.content.Context
import android.text.Html
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
@@ -2,7 +2,7 @@ package exh.metadata.metadata.base
import com.pushtorefresh.storio.operations.PreparedOperation
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.SearchMetadata
import exh.metadata.sql.models.SearchMetadata
import exh.metadata.sql.models.SearchTag
import exh.metadata.sql.models.SearchTitle
import kotlin.reflect.KClass
@@ -1,9 +1,9 @@
package exh.metadata.metadata.base
import com.google.gson.GsonBuilder
import eu.kanade.tachiyomi.data.database.models.SearchMetadata
import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.forEach
import exh.metadata.sql.models.SearchMetadata
import exh.metadata.sql.models.SearchTag
import exh.metadata.sql.models.SearchTitle
import exh.plusAssign
@@ -69,13 +69,13 @@ abstract class RaisedSearchMetadata {
val extra = raiseFlattenGson.toJson(this)
return FlatMetadata(
SearchMetadata(
mangaId,
uploader,
extra,
indexedExtra,
0
),
SearchMetadata(
mangaId,
uploader,
extra,
indexedExtra,
0
),
tags.map {
SearchTag(
null,
@@ -18,22 +18,22 @@ import exh.metadata.sql.tables.SearchMetadataTable.COL_UPLOADER
import exh.metadata.sql.tables.SearchMetadataTable.TABLE
class SearchMetadataTypeMapping : SQLiteTypeMapping<SearchMetadata>(
SearchMetadataPutResolver(),
SearchMetadataGetResolver(),
SearchMetadataDeleteResolver()
SearchMetadataPutResolver(),
SearchMetadataGetResolver(),
SearchMetadataDeleteResolver()
)
class SearchMetadataPutResolver : DefaultPutResolver<SearchMetadata>() {
override fun mapToInsertQuery(obj: SearchMetadata) = InsertQuery.builder()
.table(TABLE)
.build()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: SearchMetadata) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_MANGA_ID = ?")
.whereArgs(obj.mangaId)
.build()
.table(TABLE)
.where("$COL_MANGA_ID = ?")
.whereArgs(obj.mangaId)
.build()
override fun mapToContentValues(obj: SearchMetadata) = ContentValues(5).apply {
put(COL_MANGA_ID, obj.mangaId)
@@ -46,20 +46,21 @@ class SearchMetadataPutResolver : DefaultPutResolver<SearchMetadata>() {
class SearchMetadataGetResolver : DefaultGetResolver<SearchMetadata>() {
override fun mapFromCursor(cursor: Cursor): SearchMetadata = SearchMetadata(
override fun mapFromCursor(cursor: Cursor): SearchMetadata =
SearchMetadata(
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
uploader = cursor.getString(cursor.getColumnIndex(COL_UPLOADER)),
extra = cursor.getString(cursor.getColumnIndex(COL_EXTRA)),
indexedExtra = cursor.getString(cursor.getColumnIndex(COL_INDEXED_EXTRA)),
extraVersion = cursor.getInt(cursor.getColumnIndex(COL_EXTRA_VERSION))
)
)
}
class SearchMetadataDeleteResolver : DefaultDeleteResolver<SearchMetadata>() {
override fun mapToDeleteQuery(obj: SearchMetadata) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_MANGA_ID = ?")
.whereArgs(obj.mangaId)
.build()
.table(TABLE)
.where("$COL_MANGA_ID = ?")
.whereArgs(obj.mangaId)
.build()
}
+1 -1
View File
@@ -1,6 +1,6 @@
package exh.search
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable
import exh.metadata.sql.tables.SearchMetadataTable
import exh.metadata.sql.tables.SearchTagTable
import exh.metadata.sql.tables.SearchTitleTable
@@ -0,0 +1,197 @@
package exh.smartsearch
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import exh.util.await
import info.debatty.java.stringsimilarity.NormalizedLevenshtein
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.supervisorScope
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class SmartSearchEngine(
parentContext: CoroutineContext,
val extraSearchParams: String? = null
) : CoroutineScope {
override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default
private val db: DatabaseHelper by injectLazy()
private val normalizedLevenshtein = NormalizedLevenshtein()
suspend fun smartSearch(source: CatalogueSource, title: String): SManga? {
val cleanedTitle = cleanSmartSearchTitle(title)
val queries = getSmartSearchQueries(cleanedTitle)
val eligibleManga = supervisorScope {
queries.map { query ->
async(Dispatchers.Default) {
val builtQuery = if (extraSearchParams != null) {
"$query ${extraSearchParams.trim()}"
} else query
val searchResults = source.fetchSearchManga(1, builtQuery, FilterList())
.toSingle().await(Schedulers.io())
searchResults.mangas.map {
val cleanedMangaTitle = cleanSmartSearchTitle(it.title)
val normalizedDistance = normalizedLevenshtein.similarity(cleanedTitle, cleanedMangaTitle)
SearchEntry(it, normalizedDistance)
}.filter { (_, normalizedDistance) ->
normalizedDistance >= MIN_SMART_ELIGIBLE_THRESHOLD
}
}
}.flatMap { it.await() }
}
return eligibleManga.maxBy { it.dist }?.manga
}
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
val eligibleManga = supervisorScope {
val searchQuery = if (extraSearchParams != null) {
"$title ${extraSearchParams.trim()}"
} else title
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
if (searchResults.mangas.size == 1)
return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0))
searchResults.mangas.map {
val normalizedDistance = normalizedLevenshtein.similarity(title, it.title)
SearchEntry(it, normalizedDistance)
}.filter { (_, normalizedDistance) ->
normalizedDistance >= MIN_NORMAL_ELIGIBLE_THRESHOLD
}
}
return eligibleManga.maxBy { it.dist }?.manga
}
private fun getSmartSearchQueries(cleanedTitle: String): List<String> {
val splitCleanedTitle = cleanedTitle.split(" ")
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
if (splitCleanedTitle.isEmpty()) {
return emptyList()
}
// Search cleaned title
// Search two largest words
// Search largest word
// Search first two words
// Search first word
val searchQueries = listOf(
listOf(cleanedTitle),
splitSortedByLargest.take(2),
splitSortedByLargest.take(1),
splitCleanedTitle.take(2),
splitCleanedTitle.take(1)
)
return searchQueries.map {
it.joinToString(" ").trim()
}.distinct()
}
private fun cleanSmartSearchTitle(title: String): String {
val preTitle = title.toLowerCase()
// Remove text in brackets
var cleanedTitle = removeTextInBrackets(preTitle, true)
if (cleanedTitle.length <= 5) { // Title is suspiciously short, try parsing it backwards
cleanedTitle = removeTextInBrackets(preTitle, false)
}
// Strip non-special characters
cleanedTitle = cleanedTitle.replace(titleRegex, " ")
// Strip splitters and consecutive spaces
cleanedTitle = cleanedTitle.trim().replace(" - ", " ").replace(consecutiveSpacesRegex, " ").trim()
return cleanedTitle
}
private fun removeTextInBrackets(text: String, readForward: Boolean): String {
val bracketPairs = listOf(
'(' to ')',
'[' to ']',
'<' to '>',
'{' to '}'
)
var openingBracketPairs = bracketPairs.mapIndexed { index, (opening, _) ->
opening to index
}.toMap()
var closingBracketPairs = bracketPairs.mapIndexed { index, (_, closing) ->
closing to index
}.toMap()
// Reverse pairs if reading backwards
if (!readForward) {
val tmp = openingBracketPairs
openingBracketPairs = closingBracketPairs
closingBracketPairs = tmp
}
val depthPairs = bracketPairs.map { 0 }.toMutableList()
val result = StringBuilder()
for (c in if (readForward) text else text.reversed()) {
val openingBracketDepthIndex = openingBracketPairs[c]
if (openingBracketDepthIndex != null) {
depthPairs[openingBracketDepthIndex]++
} else {
val closingBracketDepthIndex = closingBracketPairs[c]
if (closingBracketDepthIndex != null) {
depthPairs[closingBracketDepthIndex]--
} else {
if (depthPairs.all { it <= 0 }) {
result.append(c)
} else {
// In brackets, do not append to result
}
}
}
}
return result.toString()
}
/**
* Returns a manga from the database for the given manga from network. It creates a new entry
* if the manga is not yet in the database.
*
* @param sManga the manga from the source.
* @return a manga from the database.
*/
suspend fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) {
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
newManga.copyFrom(sManga)
val result = db.insertManga(newManga).executeAsBlocking()
newManga.id = result.insertedId()
localManga = newManga
}
return localManga
}
companion object {
const val MIN_SMART_ELIGIBLE_THRESHOLD = 0.4
const val MIN_NORMAL_ELIGIBLE_THRESHOLD = 0.4
private val titleRegex = Regex("[^a-zA-Z0-9- ]")
private val consecutiveSpacesRegex = Regex(" +")
}
}
data class SearchEntry(val manga: SManga, val dist: Double)
@@ -0,0 +1,93 @@
package exh.ui.smartsearch
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import eu.kanade.tachiyomi.databinding.SmartSearchBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.source.SourceController
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy
class SmartSearchController(bundle: Bundle? = null) : NucleusController<SmartSearchBinding, SmartSearchPresenter>(), CoroutineScope {
override val coroutineContext = Job() + Dispatchers.Main
private val sourceManager: SourceManager by injectLazy()
private val source = sourceManager.get(bundle?.getLong(ARG_SOURCE_ID, -1) ?: -1) as? CatalogueSource
private val smartSearchConfig: SourceController.SmartSearchConfig? = bundle?.getParcelable(
ARG_SMART_SEARCH_CONFIG
)
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SmartSearchBinding.inflate(inflater)
return binding.root
}
override fun getTitle() = source?.name ?: ""
override fun createPresenter() = SmartSearchPresenter(source, smartSearchConfig)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.appbar.bringToFront()
if (source == null || smartSearchConfig == null) {
router.popCurrentController()
applicationContext?.toast("Missing data!")
return
}
// Init presenter now to resolve threading issues
presenter
launch(Dispatchers.Default) {
for (event in presenter.smartSearchChannel) {
withContext(NonCancellable) {
if (event is SmartSearchPresenter.SearchResults.Found) {
val transaction = MangaController(event.manga, true, smartSearchConfig).withFadeTransaction()
withContext(Dispatchers.Main) {
router.replaceTopController(transaction)
}
} else {
if (event is SmartSearchPresenter.SearchResults.NotFound) {
applicationContext?.toast("Couldn't find the manga in the source!")
} else {
applicationContext?.toast("Error performing automatic search!")
}
val transaction = BrowseSourceController(source, smartSearchConfig.origTitle, smartSearchConfig).withFadeTransaction()
withContext(Dispatchers.Main) {
router.replaceTopController(transaction)
}
}
}
}
}
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
companion object {
const val ARG_SOURCE_ID = "SOURCE_ID"
const val ARG_SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
}
}
@@ -0,0 +1,66 @@
package exh.ui.smartsearch
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.source.SourceController
import exh.smartsearch.SmartSearchEngine
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
class SmartSearchPresenter(private val source: CatalogueSource?, private val config: SourceController.SmartSearchConfig?) :
BasePresenter<SmartSearchController>(), CoroutineScope {
override val coroutineContext = Job() + Dispatchers.Main
val smartSearchChannel = Channel<SearchResults>()
private val smartSearchEngine = SmartSearchEngine(coroutineContext)
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
if (source != null && config != null) {
launch(Dispatchers.Default) {
val result = try {
val resultManga = smartSearchEngine.smartSearch(source, config.origTitle)
if (resultManga != null) {
val localManga = smartSearchEngine.networkToLocalManga(resultManga, source.id)
SearchResults.Found(localManga)
} else {
SearchResults.NotFound
}
} catch (e: Exception) {
if (e is CancellationException) {
throw e
} else {
SearchResults.Error
}
}
smartSearchChannel.send(result)
}
}
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
data class SearchEntry(val manga: SManga, val dist: Double)
sealed class SearchResults {
data class Found(val manga: Manga) : SearchResults()
object NotFound : SearchResults()
object Error : SearchResults()
}
}