add FuzzyMovieSearchFilter
This commit is contained in:
@@ -25,7 +25,7 @@ class VboxModule {
|
|||||||
@Single
|
@Single
|
||||||
fun provideUserAgent(): UserAgentProvider {
|
fun provideUserAgent(): UserAgentProvider {
|
||||||
return object : 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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
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.VideoApi
|
||||||
import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
|
import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
|
||||||
import ru.shadowsparky.vbox.shared.domain.model.VideoLinksResponse
|
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 java.util.Collections
|
||||||
import kotlin.time.Duration.Companion.days
|
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 cacheTtlMs = 3.days.inWholeMilliseconds
|
||||||
private val maxCacheSize = 100
|
private val maxCacheSize = 100
|
||||||
|
|
||||||
@@ -26,11 +30,17 @@ class SessionVideoApi(private val wrapper: VideoApi) : VideoApi {
|
|||||||
videosCache.getValid(cacheKey)?.let { return it }
|
videosCache.getValid(cacheKey)?.let { return it }
|
||||||
return videosMutex.withLock {
|
return videosMutex.withLock {
|
||||||
videosCache.getValid(cacheKey) ?: run {
|
videosCache.getValid(cacheKey) ?: run {
|
||||||
var remoteData = wrapper.fetchNewVideos(query)
|
val remoteData = wrapper.fetchNewVideos(query).let { response ->
|
||||||
remoteData = if (remoteData == videosCache[""]?.data) {
|
if (cacheKey.isNotEmpty()) {
|
||||||
VideosResponse(false, emptyList(), 1)
|
response.copy(
|
||||||
|
items = filter.filter(
|
||||||
|
response.items,
|
||||||
|
cacheKey
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
remoteData
|
response
|
||||||
|
}
|
||||||
}
|
}
|
||||||
videosCache[cacheKey] = CacheEntry(remoteData)
|
videosCache[cacheKey] = CacheEntry(remoteData)
|
||||||
remoteData
|
remoteData
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ru.shadowsparky.vbox.backend.di
|
|||||||
|
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
import org.koin.core.annotation.Module
|
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.ExternalBackendApi
|
||||||
import ru.shadowsparky.vbox.backend.data.http.SessionVideoApi
|
import ru.shadowsparky.vbox.backend.data.http.SessionVideoApi
|
||||||
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
||||||
@@ -10,7 +11,10 @@ import ru.shadowsparky.vbox.shared.domain.VideoApi
|
|||||||
class HttpModule {
|
class HttpModule {
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
fun provideVideoApi(impl: ExternalBackendApi): VideoApi {
|
fun provideVideoApi(
|
||||||
return SessionVideoApi(impl)
|
impl: ExternalBackendApi,
|
||||||
|
filter: FuzzyMovieSearchFilter
|
||||||
|
): VideoApi {
|
||||||
|
return SessionVideoApi(impl, filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -100,9 +100,7 @@ class HttpClientSettings(
|
|||||||
defaultRequest {
|
defaultRequest {
|
||||||
header(HttpHeaders.Accept, ContentType.Application.Json)
|
header(HttpHeaders.Accept, ContentType.Application.Json)
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json)
|
header(HttpHeaders.ContentType, ContentType.Application.Json)
|
||||||
userAgentProvider?.provide() ?: appConfig?.userAgent?.let {
|
header(HttpHeaders.UserAgent, (userAgentProvider?.provide() ?: appConfig?.userAgent))
|
||||||
header(HttpHeaders.UserAgent, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
install(HttpTimeout) {
|
install(HttpTimeout) {
|
||||||
|
|||||||
Reference in New Issue
Block a user