import Foundation enum APIError: LocalizedError { case invalidURL case networkError(Error) case invalidResponse(Int) case decodingError(Error) case noData var errorDescription: String? { switch self { case .invalidURL: return "无效的 URL" case .networkError(let error): return "网络错误: \(error.localizedDescription)" case .invalidResponse(let code): return "服务器错误: \(code)" case .decodingError(let error): return "解析错误: \(error.localizedDescription)" case .noData: return "无数据" } } } actor APIClient { static let shared = APIClient() private let baseURL = "https://ddys.io" private let session: URLSession private init() { let config = URLSessionConfiguration.default config.httpAdditionalHeaders = [ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Referer": "https://ddys.io/", ] config.httpCookieStorage = .shared self.session = URLSession(configuration: config) } // MARK: - HTML 页面请求 func fetchHTML(path: String) async throws -> String { guard let url = URL(string: baseURL + path) else { throw APIError.invalidURL } let (data, response) = try await session.data(from: url) guard let httpResponse = response as? HTTPURLResponse else { throw APIError.noData } guard (200...299).contains(httpResponse.statusCode) else { throw APIError.invalidResponse(httpResponse.statusCode) } guard let html = String(data: data, encoding: .utf8) else { throw APIError.noData } return html } // MARK: - 首页 func fetchHomePage() async throws -> String { try await fetchHTML(path: "/") } // MARK: - 分类列表 func fetchCategoryPage(category: ContentCategory, page: Int = 1, filter: FilterState = FilterState()) async throws -> String { var path = filter.buildPath(base: category.pathPrefix) if page > 1 { path += "/page/\(page)" } return try await fetchHTML(path: path) } // MARK: - 详情页 func fetchDetailPage(path: String) async throws -> String { try await fetchHTML(path: path) } // MARK: - 搜索 func fetchSearchPage(query: String, page: Int = 1) async throws -> String { let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query var path = "/search?q=\(encoded)&type=all" if page > 1 { path += "&page=\(page)" } return try await fetchHTML(path: path) } // MARK: - JSON API func fetchHotMovies() async throws -> [HotMovieItem] { guard let url = URL(string: baseURL + "/api/hot-movies") else { throw APIError.invalidURL } let (data, response) = try await session.data(from: url) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { throw APIError.invalidResponse((response as? HTTPURLResponse)?.statusCode ?? 0) } let result = try JSONDecoder().decode(HotMoviesResponse.self, from: data) return result.data } } // MARK: - API Response Models struct HotMoviesResponse: Codable { let success: Bool let data: [HotMovieItem] } struct HotMovieItem: Codable, Identifiable { let id: Int? let title: String let slug: String let poster: String? var itemId: String { slug } }