add FuzzyMovieSearchFilter
This commit is contained in:
@@ -25,7 +25,7 @@ class VboxModule {
|
||||
@Single
|
||||
fun provideUserAgent(): UserAgentProvider {
|
||||
return object : UserAgentProvider {
|
||||
override fun provide(): String? = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
|
||||
override fun provide() = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package ru.shadowsparky.vbox.backend.data
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import ru.shadowsparky.vbox.shared.domain.model.VideoItem
|
||||
import kotlin.math.max
|
||||
|
||||
@Single
|
||||
class FuzzyMovieSearchFilter {
|
||||
|
||||
fun filter(original: List<VideoItem>, searchQuery: String): List<VideoItem> {
|
||||
val query = searchQuery.normalize()
|
||||
if (query.isBlank()) return emptyList()
|
||||
return original.map { movie ->
|
||||
val titleNormalized = movie.title.normalize()
|
||||
if (titleNormalized.contains(query)) {
|
||||
return@map Pair(movie, 1.0)
|
||||
}
|
||||
val queryWords = query.split(" ")
|
||||
val titleWords = titleNormalized.split(" ")
|
||||
var maxScore = 0.0
|
||||
for (qWord in queryWords) {
|
||||
for (tWord in titleWords) {
|
||||
val score = tWord.fuzzySimilarity(qWord)
|
||||
if (score > maxScore) maxScore = score
|
||||
}
|
||||
}
|
||||
Pair(movie, maxScore)
|
||||
}
|
||||
.filter { it.second >= THRESHOLD }
|
||||
.sortedByDescending { it.second }
|
||||
.map { it.first }
|
||||
}
|
||||
|
||||
private fun String.normalize(): String {
|
||||
if (this.isBlank()) return ""
|
||||
return this.lowercase()
|
||||
.replace('ё', 'е')
|
||||
.convertRomanToArabic()
|
||||
.replace(Regex("[^a-zа-я0-9\\s]"), "")
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
private fun String.convertRomanToArabic(): String {
|
||||
return this.split(" ").joinToString(" ") { word ->
|
||||
when (word) {
|
||||
"i" -> "1"
|
||||
"ii" -> "2"
|
||||
"iii" -> "3"
|
||||
"iv" -> "4"
|
||||
"v" -> "5"
|
||||
"vi" -> "6"
|
||||
"vii" -> "7"
|
||||
"viii" -> "8"
|
||||
"ix" -> "9"
|
||||
"x" -> "10"
|
||||
else -> word
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.fuzzySimilarity(other: String): Double {
|
||||
if (this == other) return 1.0
|
||||
if (this.isEmpty() || other.isEmpty()) return 0.0
|
||||
val s1 = this
|
||||
val dp = IntArray(other.length + 1) { it }
|
||||
for (i in 1..s1.length) {
|
||||
var prev = dp[0]
|
||||
dp[0] = i
|
||||
for (j in 1..other.length) {
|
||||
val temp = dp[j]
|
||||
if (s1[i - 1] == other[j - 1]) {
|
||||
dp[j] = prev
|
||||
} else {
|
||||
dp[j] = minOf(dp[j - 1], dp[j], prev) + 1
|
||||
}
|
||||
prev = temp
|
||||
}
|
||||
}
|
||||
val maxLen = max(s1.length, other.length)
|
||||
return (maxLen - dp[other.length]).toDouble() / maxLen
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val THRESHOLD: Double = 0.5
|
||||
}
|
||||
}
|
||||
+15
-5
@@ -2,6 +2,7 @@ package ru.shadowsparky.vbox.backend.data.http
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import ru.shadowsparky.vbox.backend.data.FuzzyMovieSearchFilter
|
||||
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
||||
import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
|
||||
import ru.shadowsparky.vbox.shared.domain.model.VideoLinksResponse
|
||||
@@ -9,7 +10,10 @@ import ru.shadowsparky.vbox.shared.domain.model.VideosResponse
|
||||
import java.util.Collections
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
class SessionVideoApi(private val wrapper: VideoApi) : VideoApi {
|
||||
class SessionVideoApi(
|
||||
private val wrapper: VideoApi,
|
||||
private val filter: FuzzyMovieSearchFilter
|
||||
) : VideoApi {
|
||||
private val cacheTtlMs = 3.days.inWholeMilliseconds
|
||||
private val maxCacheSize = 100
|
||||
|
||||
@@ -26,11 +30,17 @@ class SessionVideoApi(private val wrapper: VideoApi) : VideoApi {
|
||||
videosCache.getValid(cacheKey)?.let { return it }
|
||||
return videosMutex.withLock {
|
||||
videosCache.getValid(cacheKey) ?: run {
|
||||
var remoteData = wrapper.fetchNewVideos(query)
|
||||
remoteData = if (remoteData == videosCache[""]?.data) {
|
||||
VideosResponse(false, emptyList(), 1)
|
||||
val remoteData = wrapper.fetchNewVideos(query).let { response ->
|
||||
if (cacheKey.isNotEmpty()) {
|
||||
response.copy(
|
||||
items = filter.filter(
|
||||
response.items,
|
||||
cacheKey
|
||||
)
|
||||
)
|
||||
} else {
|
||||
remoteData
|
||||
response
|
||||
}
|
||||
}
|
||||
videosCache[cacheKey] = CacheEntry(remoteData)
|
||||
remoteData
|
||||
|
||||
@@ -2,6 +2,7 @@ package ru.shadowsparky.vbox.backend.di
|
||||
|
||||
import org.koin.core.annotation.Factory
|
||||
import org.koin.core.annotation.Module
|
||||
import ru.shadowsparky.vbox.backend.data.FuzzyMovieSearchFilter
|
||||
import ru.shadowsparky.vbox.backend.data.http.ExternalBackendApi
|
||||
import ru.shadowsparky.vbox.backend.data.http.SessionVideoApi
|
||||
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
||||
@@ -10,7 +11,10 @@ import ru.shadowsparky.vbox.shared.domain.VideoApi
|
||||
class HttpModule {
|
||||
|
||||
@Factory
|
||||
fun provideVideoApi(impl: ExternalBackendApi): VideoApi {
|
||||
return SessionVideoApi(impl)
|
||||
fun provideVideoApi(
|
||||
impl: ExternalBackendApi,
|
||||
filter: FuzzyMovieSearchFilter
|
||||
): VideoApi {
|
||||
return SessionVideoApi(impl, filter)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -100,9 +100,7 @@ class HttpClientSettings(
|
||||
defaultRequest {
|
||||
header(HttpHeaders.Accept, ContentType.Application.Json)
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json)
|
||||
userAgentProvider?.provide() ?: appConfig?.userAgent?.let {
|
||||
header(HttpHeaders.UserAgent, it)
|
||||
}
|
||||
header(HttpHeaders.UserAgent, (userAgentProvider?.provide() ?: appConfig?.userAgent))
|
||||
}
|
||||
|
||||
install(HttpTimeout) {
|
||||
|
||||
Reference in New Issue
Block a user