From e58322339f7bcbc4f38188b8b7851c9754b0ab0a Mon Sep 17 00:00:00 2001 From: JinJiangHuang Date: Sun, 1 Mar 2026 22:57:20 +0800 Subject: [PATCH] fix search --- tvbox/Models/SourceBean.swift | 6 +++- tvbox/Services/ApiConfig.swift | 1 + tvbox/Services/SourceService.swift | 45 ++++++++++++++++++++++++-- tvbox/ViewModels/SearchViewModel.swift | 10 ++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/tvbox/Models/SourceBean.swift b/tvbox/Models/SourceBean.swift index 671b8b0..619cea6 100644 --- a/tvbox/Models/SourceBean.swift +++ b/tvbox/Models/SourceBean.swift @@ -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 { diff --git a/tvbox/Services/ApiConfig.swift b/tvbox/Services/ApiConfig.swift index 2abf198..696dc3e 100644 --- a/tvbox/Services/ApiConfig.swift +++ b/tvbox/Services/ApiConfig.swift @@ -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 diff --git a/tvbox/Services/SourceService.swift b/tvbox/Services/SourceService.swift index 643ffc2..67f516b 100644 --- a/tvbox/Services/SourceService.swift +++ b/tvbox/Services/SourceService.swift @@ -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) diff --git a/tvbox/ViewModels/SearchViewModel.swift b/tvbox/ViewModels/SearchViewModel.swift index c563cb4..71567e3 100644 --- a/tvbox/ViewModels/SearchViewModel.swift +++ b/tvbox/ViewModels/SearchViewModel.swift @@ -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 }