Get a random movie from the API call

417 Views Asked by At

I'm using The MovieDatabase API to get movie results. To get a random movie I have a few functions: 1) The task to fetch the total number of pages depending on the filters.

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([])
    }
}
  1. It generates a random page number between 1 and the fetched total page number

     func getRandomPageNumbers(numOption: Int, totalPages: Int) -> [Int] {
         let randomNumbers = Array(1...totalPages).shuffled().prefix(numOption)
         return Array(randomNumbers)
     }
    
  1. It applies everything to the API call

     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)
         }
     }
    

Then we have the button that activates the functions

Button(action: {
    Task {
        performTask(
                    numOption: numOption,
                    selectedGenresViewModel: selectedGenresViewModel,
                    selectedProviderViewModel: selectedProviderViewModel
                ) { movieIDs in
            var fetchedMovies = [Movie]()
            let dispatchGroup = DispatchGroup()
            
            // plaatst de id van de films om de infromatie eruit te halen
            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)
            }
        }
    }
})

Also here is the API call

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]?
}

For some reason it it not calling the total pages. I'm completely at a los.

1

There are 1 best solutions below

0
Joost van Grieken On

I figured it is not possible to fetch the movie ID's and the total_pages in one API call, so I split the two. First it fetches the total number of pages (in the fetchTotalPages func), then it makes a second call (in the discoverMovies func) with a random generated number within the total page number.

But for some reason it keeps failing to fetch the total number of pages. Am I missing something?

Here is the API call

class RandomMovieStore: ObservableObject {
    @Published var randomMovie: Movie?
    @Published var genres = [Genre]()
    @Published var selectedGenres = Set<Int>()
    @Published var totalPages: Int = 1
    
    static let shared = RandomMovieStore()
    private let apiKey = "ae1c9875a55b3f3d23c889e07b973920"
    let urlSession = URLSession.shared
    let jsonDecoder = Utils.jsonDecoder
    
    func fetchTotalPages(genres: [Int], providers: [Int], completion: @escaping (Result<Int, MovieError>) -> ()) {
        let genreIDs = genres.map(String.init).joined(separator: ",")
        let providerIDs = providers.map(String.init).joined(separator: ",")
        
        guard let url = URL(string: "https://api.themoviedb.org/3/discover/movie?api_key=\(apiKey)&include_adult=false&page=1&with_genres=\(genreIDs)&watch_region=NL&vote_average.ite=1&with_watch_providers=\(providerIDs)") else {
            completion(.failure(.invalidEndpoint))
            return
        }
        
        loadURLAndDecode(url: url) { (result: Result<MovieResponsePages, MovieError>) in
            switch result {
            case .success(let pagesResponse):
                DispatchQueue.main.async {
                    self.totalPages = pagesResponse.totalPages
                }
                completion(.success(pagesResponse.totalPages))
            case .failure(let error):
                completion(.failure(error))
            }
        }
        print(totalPages)
    }
    
    func discoverMovies(page: Int, genres: String, providers: String, completion: @escaping (Result<MovieResponse, 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, completion: completion)
        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 MovieResponsePages: Codable {
    let page: Int
    let totalPages: Int
    
    enum CodingKeys: String, CodingKey {
        case page
        case totalPages = "total_pages"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        page = try container.decode(Int.self, forKey: .page)
        totalPages = try container.decode(Int.self, forKey: .totalPages)
    }
}

Here is the button action

struct RandomiserView: View {
    
    …
    
    @State private var numOption: Int = 3
    @StateObject var selectedGenresViewModel = SelectedGenresViewModel()
    @StateObject var selectedProviderViewModel = SelectedProviderViewModel()
    
    var totalPages: RandomMovieStore
    
    var body: some View {
        NavigationView {
            VStack {
                …
                
                Button(action: {
                    Task {
                        performTask(
                                    numOption: numOption,
                                    selectedGenresViewModel: selectedGenresViewModel,
                                    selectedProviderViewModel: selectedProviderViewModel
                        ) { movieIDs,arg  in
                            var fetchedMovies = [Movie]()
                            let dispatchGroup = DispatchGroup()
                            
                            // plaatst de id van de films om de infromatie eruit te halen
                            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)
                            }
                        }
                    }
                }) {
                    …
}

And finally the func to activate the API call

func performTask(numOption: Int, selectedGenresViewModel: SelectedGenresViewModel, selectedProviderViewModel: SelectedProviderViewModel, completion: @escaping ([Int], Int?) -> Void) {
    let genreIDs = selectedGenresViewModel.selectedGenres.map { $0.id }
    let providerIDs = selectedProviderViewModel.selectedProvider.map { $0.provider_id }
    
    RandomMovieStore.shared.fetchTotalPages(genres: genreIDs, providers: providerIDs) { 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, totalPages)
            }
        case .failure(let error):
            print("Failed to fetch total pages: \(error)")
            completion([], nil)
        }
    }
}

// Stap 2: kiest een random pagina nummer
func getRandomPageNumbers(numOption: Int, totalPages: Int) -> [Int] {
    let randomNumbers = Array(1...totalPages).shuffled().prefix(numOption)
    print("number of options: \(numOption)")
    print(randomNumbers)
    return Array(randomNumbers)
}

// Stap 3: Voegt filters toe aan de API call (mocht die er zijn), voegt de pagina in de api call. Bij succes kiest die 1 random film van de resultaten en pakt de film id.
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)
    }
}