import Foundation enum WallhavenError: Error { case invalidURL case networkError(Error) case decodingError(Error) case noData case apiError(String) } /// Sort order for Wallhaven search results. Maps to the `sorting` query /// parameter on the public API. `random` returns a fresh batch per page. enum WallhavenSorting: String { case dateAdded = "date_added" case toplist case random } @MainActor protocol WallhavenSearching { func searchWallpapers( query: String, categories: String, purity: String, sorting: WallhavenSorting, resolutions: [String], ratios: [String], limit: Int, page: Int ) async throws -> ([Wallpaper], Meta?) } class WallhavenAPI: WallhavenSearching { private let session: URLSession init(session: URLSession = .shared) { self.session = session } func searchWallpapers( query: String, categories: String = "111", purity: String = "100", sorting: WallhavenSorting = .dateAdded, resolutions: [String] = [], ratios: [String] = [], limit: Int = 24, page: Int = 1 ) async throws -> ([Wallpaper], Meta?) { var queryItems = [ URLQueryItem(name: "q", value: query), URLQueryItem(name: "categories", value: categories), URLQueryItem(name: "purity", value: purity), URLQueryItem(name: "sorting", value: sorting.rawValue), URLQueryItem(name: "limit", value: String(limit)), URLQueryItem(name: "page", value: String(page)), ] if !resolutions.isEmpty { queryItems.append( URLQueryItem(name: "resolutions", value: resolutions.joined(separator: ","))) } if !ratios.isEmpty { queryItems.append(URLQueryItem(name: "ratios", value: ratios.joined(separator: ","))) } return try await performSearch(queryItems: queryItems) } private func performSearch(queryItems: [URLQueryItem]) async throws -> ([Wallpaper], Meta?) { var urlComponents = URLComponents( url: Configuration.baseURL.appendingPathComponent("search"), resolvingAgainstBaseURL: true)! urlComponents.queryItems = queryItems let url = urlComponents.url! let (data, response) = try await session.data(from: url) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { throw WallhavenError.apiError("Server returned an error") } do { let decodedResponse = try JSONDecoder().decode(WallhavenResponse.self, from: data) return (decodedResponse.data, decodedResponse.meta) } catch { throw WallhavenError.decodingError(error) } } }