You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用TMDB API获取随机电影时总页数无法获取求助

问题:TMDB API无法获取总页数

我基于TMDB API开发随机电影获取功能,核心逻辑是先获取符合筛选条件的总页数,再随机选页码拉取电影,但目前无法成功获取总页数,相关代码如下:

1. 核心业务逻辑函数

performTask(获取总页数并触发后续流程)

func performTask(numOption: Int, selectedGenresViewModel: SelectedGenresViewModel, selectedProviderViewModel: SelectedProviderViewModel, completion: @escaping ([Int]) -> Void) {
let genreIDs = selectedGenresViewModel.selectedGenres.map { $0.id }
let providerIDs = selectedProviderViewModel.selectedProvider.map { $0.provider_id }

RandomMovieStore.shared.discoverMovies(page: 1, genres: genreIDs.map(String.init).joined(separator: ","), providers: providerIDs.map(String.init).joined(separator: ",")) { result in
    switch result {
    case .success(let (_, totalPages)):
        let pageNumbers = getRandomPageNumbers(numOption: numOption, totalPages: totalPages)
        applyFiltersAndFindMovieIDs(pages: pageNumbers, genres: genreIDs, providers: providerIDs) { ids in
            completion(ids)
            print("total pages are: \(totalPages)")
        }
    case .failure(let error):
        print("API call failed with error: \(error)")
        completion([])
    }
}

getRandomPageNumbers(生成随机页码)

func getRandomPageNumbers(numOption: Int, totalPages: Int) -> [Int] {
     let randomNumbers = Array(1...totalPages).shuffled().prefix(numOption)
     return Array(randomNumbers)
 }

applyFiltersAndFindMovieIDs(批量拉取随机页电影)

func applyFiltersAndFindMovieIDs(pages: [Int], genres: [Int], providers: [Int], completion: @escaping ([Int]) -> Void) {
     var movieIDs = [Int]()
     let group = DispatchGroup()

     for page in pages {
         group.enter()

         let genreIDs = genres.map(String.init).joined(separator: ",")
         let providerIDs = providers.map(String.init).joined(separator: ",")

         RandomMovieStore.shared.discoverMovies(page: page, genres: genreIDs, providers: providerIDs) { result in
             switch result {
             case .success(let (response, _)):
                 if let randomMovie = response.results?.randomElement() {
                     let randomMovieID = randomMovie.id
                     print("Random movie ID:", randomMovieID)
                     movieIDs.append(randomMovieID)
                 }
             case .failure(let error):
                 print("API call failed with error: \(error)")
             }
             group.leave()
         }
     }

     group.notify(queue: .main) {
         print("All API calls completed")
         completion(movieIDs)
     }
 }

2. 按钮触发逻辑

Button(action: {
    Task {
        performTask(
                    numOption: numOption,
                    selectedGenresViewModel: selectedGenresViewModel,
                    selectedProviderViewModel: selectedProviderViewModel
                ) { movieIDs in
            var fetchedMovies = [Movie]()
            let dispatchGroup = DispatchGroup()
            
            // 获取电影ID以提取相关信息
            for id in movieIDs {
                dispatchGroup.enter()
                print("Fetching movie with ID: \(id)")
                MovieStore.shared.fetchMovie(id: id) { result in
                    switch result {
                    case .success(let movie):
                        fetchedMovies.append(movie)
                    case .failure(let error):
                        print(error.localizedDescription)
                    }
                    dispatchGroup.leave()
                }
            }
            
            dispatchGroup.notify(queue: .main) {
                self.filteredMovies = fetchedMovies
                self.movieTitles = fetchedMovies.map({ $0.title })
                print(self.movieTitles)
            }
        }
    }
})

3. API调用核心实现

func discoverMovies(page: Int, genres: String, providers: String, completion: @escaping (Result<(RandomMovieResponse, Int?), MovieError>) -> ()) {
        guard let url = URL(string: "https://api.themoviedb.org/3/discover/movie?api_key=\(apiKey)&include_adult=false&page=\(page)&with_genres=\(genres)&watch_region=NL&vote_average.ite=1&with_watch_providers=\(providers)") else {
            completion(.failure(.invalidEndpoint))
            return
        }
        
        self.loadURLAndDecode(url: url) { [weak self] (result: Result<RandomMovieResponse, MovieError>) in
            guard let self = self else { return }
            
            switch result {
            case .success(let response):
                self.totalPages = response.total_pages
                completion(.success((response, response.total_pages)))
            case .failure(let error):
                completion(.failure(error))
            }
        }
        
        print(url)
    }
    
    private func loadURLAndDecode<D: Decodable>(url: URL, params: [String: String]? = nil, completion: @escaping (Result<D, MovieError>) -> ()) {
        guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            completion(.failure(.invalidEndpoint))
            return
        }
        
        var queryItems = [URLQueryItem(name: "api_key", value: apiKey)]
        if let params = params {
            queryItems.append(contentsOf: params.map { URLQueryItem(name: $0.key, value: $0.value) })
        }
        
        urlComponents.queryItems = queryItems
        
        guard let finalURL = urlComponents.url else {
            completion(.failure(.invalidEndpoint))
            return
        }
        
        urlSession.dataTask(with: finalURL) { [weak self] (data, response, error) in
            guard let self = self else { return }
            
            if error != nil {
                self.executeCompletionHandlerInMainThread(with: .failure(.apiError), completion: completion)
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
                self.executeCompletionHandlerInMainThread(with: .failure(.invalidResponse), completion: completion)
                return
            }
            guard let data = data else {
                self.executeCompletionHandlerInMainThread(with: .failure(.noData), completion: completion)
                return
            }
            
            do {
                let decodedResponse = try self.jsonDecoder.decode(D.self, from: data)
                self.executeCompletionHandlerInMainThread(with: .success(decodedResponse), completion: completion)
            } catch {
                self.executeCompletionHandlerInMainThread(with: .failure(.serializationError), completion: completion)
            }
        }.resume()
    }
    
    private func executeCompletionHandlerInMainThread<D: Decodable>(with result: Result<D, MovieError>, completion: @escaping (Result<D, MovieError>) -> ()) {
        DispatchQueue.main.async {
            completion(result)
        }
    }
}

struct RandomMovieResponse: Decodable {
    let page: Int?
    let total_results: Int?
    let total_pages: Int?
    let results: [Movie]?
}

问题排查与解决方案

1. URL参数拼写错误

discoverMovies里的URL包含无效参数vote_average.ite=1,这是拼写错误,正确的参数名应该是vote_average.gte(大于等于)或vote_average.lte(小于等于),无效参数会导致TMDB API返回错误,无法解析出total_pages。

修正后的URL:

guard let url = URL(string: "https://api.themoviedb.org/3/discover/movie?api_key=\(apiKey)&include_adult=false&page=\(page)&with_genres=\(genres)&watch_region=NL&vote_average.gte=1&with_watch_providers=\(providers)") else {
    completion(.failure(.invalidEndpoint))
    return
}

2. loadURLAndDecode覆盖原URL参数

当前loadURLAndDecode函数会丢弃原URL中的所有query参数,重新创建只包含api_key和传入params的queryItems,导致原URL里的include_adult、page等关键参数丢失,API请求完全不符合预期。

修改loadURLAndDecode,保留原URL的query参数:

private func loadURLAndDecode<D: Decodable>(url: URL, params: [String: String]? = nil, completion: @escaping (Result<D, MovieError>) -> ()) {
    guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
        completion(.failure(.invalidEndpoint))
        return
    }
    
    // 保留原URL的queryItems,为空则初始化
    var queryItems = urlComponents.queryItems ?? []
    
    // 追加传入的参数
    if let params = params {
        queryItems.append(contentsOf: params.map { URLQueryItem(name: $0.key, value: $0.value) })
    }
    
    // 确保api_key存在(避免原URL遗漏)
    if !queryItems.contains(where: { $0.name == "api_key" }) {
        queryItems.append(URLQueryItem(name: "api_key", value: apiKey))
    }
    
    urlComponents.queryItems = queryItems
    
    guard let finalURL = urlComponents.url else {
        completion(.failure(.invalidEndpoint))
        return
    }
    
    // 后续dataTask逻辑不变
}

3. 处理total_pages为nil的情况

RandomMovieResponse中的total_pages是可选类型,若API返回缺失或解析失败会导致nil,进而触发后续逻辑错误。在performTask中增加判断:

case .success(let (_, totalPages)):
    guard let validTotalPages = totalPages, validTotalPages > 0 else {
        print("Invalid total pages count")
        completion([])
        return
    }
    let pageNumbers = getRandomPageNumbers(numOption: numOption, totalPages: validTotalPages)
    // 后续流程

4. 检查API权限与地区限制

确认你的TMDB API Key有权限访问discover接口,且watch_region=NL设置的地区有可用内容。若地区无匹配内容,API会返回total_pages=0,需提示用户调整筛选条件。

内容的提问来源于stack exchange,提问作者Joost van Grieken

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 01:01:57