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