81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
import type {
|
|
ContentVariants,
|
|
SearchFilters,
|
|
SearchPage,
|
|
SearchSort,
|
|
ServiceStats,
|
|
TorrentDetail,
|
|
} from '@/types/api'
|
|
|
|
interface SearchInput {
|
|
query: string
|
|
offset: number
|
|
limit: number
|
|
sort: SearchSort
|
|
filters: SearchFilters
|
|
}
|
|
|
|
function optionalNumber(params: URLSearchParams, key: string, value: string, scale = 1) {
|
|
if (value.trim() === '') return
|
|
const parsed = Number(value)
|
|
if (Number.isFinite(parsed) && parsed >= 0) params.set(key, String(Math.round(parsed * scale)))
|
|
}
|
|
|
|
function optionalDate(params: URLSearchParams, key: string, value: string) {
|
|
if (!value) return
|
|
const timestamp = new Date(value).getTime()
|
|
if (Number.isFinite(timestamp)) {
|
|
const endOfDay = key.endsWith('_before') ? 86_399 : 0
|
|
params.set(key, String(Math.floor(timestamp / 1000) + endOfDay))
|
|
}
|
|
}
|
|
|
|
async function request<T>(path: string, signal?: AbortSignal): Promise<T> {
|
|
const response = await fetch(path, { headers: { Accept: 'application/json' }, signal })
|
|
if (!response.ok) {
|
|
let message = `请求失败 (${response.status})`
|
|
try {
|
|
const body = (await response.json()) as { error?: string }
|
|
if (body.error) message = body.error
|
|
} catch {
|
|
// 保留包含状态码的稳定错误信息
|
|
}
|
|
throw new Error(message)
|
|
}
|
|
return response.json() as Promise<T>
|
|
}
|
|
|
|
export function search(input: SearchInput, signal?: AbortSignal): Promise<SearchPage> {
|
|
const params = new URLSearchParams({
|
|
q: input.query.trim(),
|
|
offset: String(input.offset),
|
|
limit: String(input.limit),
|
|
sort: input.sort,
|
|
})
|
|
const { filters } = input
|
|
optionalNumber(params, 'min_size', filters.minSizeMiB, 1024 * 1024)
|
|
optionalNumber(params, 'max_size', filters.maxSizeMiB, 1024 * 1024)
|
|
optionalNumber(params, 'min_files', filters.minFiles)
|
|
optionalNumber(params, 'max_files', filters.maxFiles)
|
|
optionalDate(params, 'first_seen_after', filters.firstSeenAfter)
|
|
optionalDate(params, 'first_seen_before', filters.firstSeenBefore)
|
|
optionalDate(params, 'last_seen_after', filters.lastSeenAfter)
|
|
optionalDate(params, 'last_seen_before', filters.lastSeenBefore)
|
|
if (filters.extension.trim()) params.set('extension', filters.extension.trim().replace(/^\./, ''))
|
|
if (filters.availability) params.set('availability', filters.availability)
|
|
if (filters.heat) params.set('heat', filters.heat)
|
|
return request<SearchPage>(`/search?${params}`, signal)
|
|
}
|
|
|
|
export function getTorrent(infoHash: string, signal?: AbortSignal): Promise<TorrentDetail> {
|
|
return request<TorrentDetail>(`/torrents/${encodeURIComponent(infoHash)}`, signal)
|
|
}
|
|
|
|
export function getVariants(contentKey: string, signal?: AbortSignal): Promise<ContentVariants> {
|
|
return request<ContentVariants>(`/contents/${encodeURIComponent(contentKey)}?limit=100`, signal)
|
|
}
|
|
|
|
export function getStats(signal?: AbortSignal): Promise<ServiceStats> {
|
|
return request<ServiceStats>('/stats', signal)
|
|
}
|