fix search
This commit is contained in:
@@ -15,6 +15,8 @@ struct SourceBean: Codable, Identifiable, Hashable {
|
||||
let searchable: Int
|
||||
/// 是否允许出现在首页分类:0 不可选,1 可选。
|
||||
let filterable: Int
|
||||
/// 快速搜索开关:0 关闭,1 开启(主要用于 remote 源 quick 参数)。
|
||||
let quickSearch: Int
|
||||
/// 源声明的播放器类型(历史字段,Swift 端目前主要走统一播放器策略)。
|
||||
let playerType: Int
|
||||
/// 源协议类型:0 XML,1 JSON,3 JAR,4 Remote。
|
||||
@@ -23,13 +25,14 @@ struct SourceBean: Codable, Identifiable, Hashable {
|
||||
let ext: String?
|
||||
|
||||
init(key: String = "", name: String = "", api: String = "",
|
||||
searchable: Int = 1, filterable: Int = 1,
|
||||
searchable: Int = 1, filterable: Int = 1, quickSearch: Int = 0,
|
||||
playerType: Int = 0, type: Int = 1, ext: String? = nil) {
|
||||
self.key = key
|
||||
self.name = name
|
||||
self.api = api
|
||||
self.searchable = searchable
|
||||
self.filterable = filterable
|
||||
self.quickSearch = quickSearch
|
||||
self.playerType = playerType
|
||||
self.type = type
|
||||
self.ext = ext
|
||||
@@ -37,6 +40,7 @@ struct SourceBean: Codable, Identifiable, Hashable {
|
||||
|
||||
var isSearchable: Bool { searchable == 1 }
|
||||
var isFilterable: Bool { filterable == 1 }
|
||||
var isQuickSearchEnabled: Bool { quickSearch == 1 }
|
||||
|
||||
/// 是否在 Swift 版中受支持(type=3 为 JAR/Spider,需要 Java 运行时,暂不支持)
|
||||
var isSupportedInSwift: Bool {
|
||||
|
||||
@@ -519,6 +519,7 @@ class ApiConfig: ObservableObject {
|
||||
api: site.api ?? "",
|
||||
searchable: site.searchable?.value ?? 1,
|
||||
filterable: site.filterable?.value ?? 1,
|
||||
quickSearch: site.quickSearch?.value ?? 0,
|
||||
playerType: site.playerType?.value ?? 0,
|
||||
type: site.type?.value ?? 1,
|
||||
ext: site.ext?.stringValue
|
||||
|
||||
@@ -325,9 +325,10 @@ class SourceService {
|
||||
url = "\(api)?wd=\(encodedKeyword)"
|
||||
} else if sourceBean.type == 4 {
|
||||
// Type 4: 远程接口
|
||||
let quickValue = sourceBean.isQuickSearchEnabled ? "true" : "false"
|
||||
url = api.contains("?")
|
||||
? "\(api)&wd=\(encodedKeyword)&ac=detail&quick=false"
|
||||
: "\(api)?wd=\(encodedKeyword)&ac=detail&quick=false"
|
||||
? "\(api)&wd=\(encodedKeyword)&ac=detail&quick=\(quickValue)"
|
||||
: "\(api)?wd=\(encodedKeyword)&ac=detail&quick=\(quickValue)"
|
||||
|
||||
// 加载 extend
|
||||
if let ext = sourceBean.ext, !ext.isEmpty {
|
||||
@@ -345,7 +346,8 @@ class SourceService {
|
||||
}
|
||||
|
||||
let jsonStr = try await network.getString(from: url)
|
||||
return try parseVideoList(jsonStr, sourceKey: sourceBean.key, type: sourceBean.type)
|
||||
let videos = try parseVideoList(jsonStr, sourceKey: sourceBean.key, type: sourceBean.type)
|
||||
return filterSearchResults(videos, keyword: keyword)
|
||||
}
|
||||
|
||||
/// 多源并发搜索
|
||||
@@ -374,6 +376,43 @@ class SourceService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 对源返回结果做本地关键词过滤,规避部分接口返回推荐/无关内容。
|
||||
private func filterSearchResults(_ videos: [Movie.Video], keyword: String) -> [Movie.Video] {
|
||||
let tokens = keyword
|
||||
.split(whereSeparator: \.isWhitespace)
|
||||
.map { normalizeSearchText(String($0)) }
|
||||
.filter { !$0.isEmpty }
|
||||
|
||||
guard !tokens.isEmpty else { return videos }
|
||||
|
||||
return videos.filter { video in
|
||||
let searchableText = normalizeSearchText([
|
||||
video.name,
|
||||
video.note,
|
||||
video.actor,
|
||||
video.director,
|
||||
video.type,
|
||||
video.area,
|
||||
video.year
|
||||
].joined(separator: " "))
|
||||
guard !searchableText.isEmpty else { return false }
|
||||
return tokens.allSatisfy { searchableText.contains($0) }
|
||||
}
|
||||
}
|
||||
|
||||
private func normalizeSearchText(_ text: String) -> String {
|
||||
let folded = text.folding(
|
||||
options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive],
|
||||
locale: .current
|
||||
)
|
||||
let scalars = folded.unicodeScalars.filter { scalar in
|
||||
!CharacterSet.whitespacesAndNewlines.contains(scalar) &&
|
||||
!CharacterSet.punctuationCharacters.contains(scalar) &&
|
||||
!CharacterSet.symbols.contains(scalar)
|
||||
}
|
||||
return String(String.UnicodeScalarView(scalars)).lowercased()
|
||||
}
|
||||
|
||||
// MARK: - Extend 解析
|
||||
|
||||
/// 解析 extend 参数(对应 Android 端 getFixUrl)
|
||||
|
||||
@@ -17,6 +17,8 @@ class SearchViewModel: ObservableObject {
|
||||
|
||||
/// 源数据服务(负责多源并发搜索)。
|
||||
private let sourceService = SourceService.shared
|
||||
/// 搜索请求序号(用于丢弃过期异步结果)。
|
||||
private var latestSearchRequestId: UUID = UUID()
|
||||
|
||||
/// 初始化时同步加载本地历史记录,确保搜索页首次渲染即可展示。
|
||||
init() {
|
||||
@@ -27,6 +29,8 @@ class SearchViewModel: ObservableObject {
|
||||
func search() async {
|
||||
let trimmed = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
let requestId = UUID()
|
||||
latestSearchRequestId = requestId
|
||||
|
||||
isSearching = true
|
||||
errorMessage = nil
|
||||
@@ -37,6 +41,7 @@ class SearchViewModel: ObservableObject {
|
||||
|
||||
// 走多源并发搜索,返回聚合后的影片列表。
|
||||
let videos = await sourceService.searchAll(keyword: trimmed)
|
||||
guard requestId == latestSearchRequestId else { return }
|
||||
self.results = videos
|
||||
|
||||
if videos.isEmpty {
|
||||
@@ -50,13 +55,18 @@ class SearchViewModel: ObservableObject {
|
||||
func searchInSource(_ source: SourceBean) async {
|
||||
let trimmed = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
let requestId = UUID()
|
||||
latestSearchRequestId = requestId
|
||||
|
||||
isSearching = true
|
||||
errorMessage = nil
|
||||
|
||||
do {
|
||||
let videos = try await sourceService.search(sourceBean: source, keyword: trimmed)
|
||||
guard requestId == latestSearchRequestId else { return }
|
||||
self.results = videos
|
||||
} catch {
|
||||
guard requestId == latestSearchRequestId else { return }
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user