Les rottentomatoes

Utilisation d’une ‘API’ web

Dans le tutoriel TIMFlix, nous avons construit une application qui affichait une liste de vidéos à l’intérieur d’un UITableView.  Les informations des vidéos, ainsi que l’image des affiches, étaient renseignées localement grace à un fichier de propriétés.
L’inconvénient de cette approche est évident lorsque vient le temps d’y ajouter de nouvelles vidéos.
Dans ce tutoriel, nous verrons comment:

 

tomato1

voir note au bas


Pré-requis:

  1. Avoir complété le tutoriel TIMFlix ou bien avoir une maitrise de l’objet UITableView + segue + passage de paramètres entre les scènes d’une app.
  2. S’inscrire sur le site de rottentomatoes
  3. Obtenir une clé développeur

 
 
 
Vidéo du résultat final:
http://www.youtube.com/watch?v=LBi6Z0RnINM
 


 

API de rottenTomatoes

 
Le site ‘RottenTomatoes’ propose une API d’accès aux données qu’il compile.  Ces données nous renseignent sur des films,  les critiques, les acteurs, les personnages, …
 
Pour interroger l’api de rottentomatoes, il suffit de suivre l’URL suivante:
 

http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=VOTRECLE&q=MODELE_DE_RECHERCHE&page_limit=NB_PAGES

 
Comme par exemple, http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=ax2345bv67ncs2734&q=Star&page_limit=10
Note: La clé de l’exemple est invalide.  Vous devez la remplacer par celle que vous avez obtenue de rottentomatoes.  La limite de pages fixée par l’api est de 50 pages.
La requête précédente devrait nous retourner une structure JSON contenant une liste d’au plus 10 films dont la chaîne ‘Star’ apparaît dans le titre du film.
Pour obtenir, à partir du réseau Internet, des données en format JSON et créer un dictionnaire, il faut utiliser la syntaxe suivante:

donnesViaURL = [NSData dataWithContentsOfURL:[NSURL URLWithString:adresseURL]];
NSError *error;
structureJSONConvertie = [NSJSONSerialization JSONObjectWithData:donnesViaURL options:kNilOptions error:&error];

 


 

Étape 1

ACTION – Ouvrons le projet de départ et ajoutons le code suivant à la méthode ‘rechercherFilms’ de la classe contrôleur de la scène principale.
 

//  ViewController.m
// --------------------------------------------------------
-(BOOL) rechercherFilms:(NSString *) film
// --------------------------------------------------------
//  Description:  Méthode qui ...
{
    // Construire l'URL pointant sur l'API
    NSString * URLFilm = [NSString stringWithFormat:@"http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=%@&q=%@&page_limit=%@", CLE_API,film, NB_FILMS];
    // Obtenir les données en format brut
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:URLFilm]];
    // Si data est vide alors quitter la méthode
    if (!data) { NSLog(@"URL n'a pas retourné de données!"); return NO;}
    // Convertir les données brutes en format JSON et les placer
    // dans tableauDesFilms
    NSError *error;
    tableauDesFilms = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
    // Vérifier si la conversion a fonctionné
    if (!tableauDesFilms) {
        NSLog(@"Pour une raison étrange, la requête chez 'rottentomatoes' a retournée une structure JSON mal formée...");
        return NO;
    }
    NSLog(@"tableauDesFilms = %@", tableauDesFilms);
    self.nbResultat.text = [NSString stringWithFormat:@"%d", ((NSArray*)tableauDesFilms[@"movies"]).count];
    return YES;
} // FIN -> rechercherFilms

 
 
ACTION –  Testons l’application.  Remarquez que la méthode ‘rechercherFilm’ est appelée dans ‘viewDidLoad’.
 
rotten-001
 
Nous devrions obtenir le résultat suivant dans la zone ‘debug »  d’Xcode:
 

 tableauDesFilms = {
    "link_template" = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?q={search-term}&page_limit={results-per-page}&page={page-number}";
    links =     {
        next = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?q=star&page_limit=1&page=2";
        self = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?q=star&page_limit=1&page=1";
    };
    movies =     (  //  <-- tableau de films
                {   //  <-- dictionnaire de données du premier film de la liste
            "abridged_cast" =             (
                                {
                    characters =                     (
                        "Anakin Skywalker/Darth Vader"
                    );
                    id = 162652153;
                    name = "Hayden Christensen";
                },
                                {
                    characters =                     (
                        "Obi-Wan Kenobi"
                    );
                    id = 162652152;
                    name = "Ewan McGregor";
                },
                                {
                    characters =                     (
                        "R2-D2"
                    );
                    id = 418638213;
                    name = "Kenny Baker";
                },
                                {
                    characters =                     (
                        "Ruwee Naberrie"
                    );
                    id = 548155708;
                    name = "Graeme Blundell";
                },
                                {
                    characters =                     (
                        "Captain Colton"
                    );
                    id = 358317901;
                    name = "Jeremy Bulloch";
                }
            );
            "alternate_ids" =             {
                imdb = 0121766;
            };
            "critics_consensus" = "This sixth and final installment of George Lucas' epic space opera will please die-hard fanatics and non-believers alike -- largely due to awesome digital effects and the sheer power of the mythology.";
            id = 9;
            links =             {
                alternate = "http://www.rottentomatoes.com/m/star_wars_episode_iii_revenge_of_the_sith_3d/";
                cast = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/cast.json";
                clips = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/clips.json";
                reviews = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/reviews.json";
                self = "http://api.rottentomatoes.com/api/public/v1.0/movies/9.json";
                similar = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/similar.json";
            };
            "mpaa_rating" = "PG-13";
            posters =             {
                detailed = "http://content8.flixster.com/movie/10/94/47/10944718_det.jpg";
                original = "http://content8.flixster.com/movie/10/94/47/10944718_ori.jpg";
                profile = "http://content8.flixster.com/movie/10/94/47/10944718_pro.jpg";
                thumbnail = "http://content8.flixster.com/movie/10/94/47/10944718_mob.jpg";
            };
            ratings =             {
                "audience_rating" = Upright;
                "audience_score" = 65;
                "critics_rating" = "Certified Fresh";
                "critics_score" = 80;
            };
            "release_dates" =             {
                dvd = "2005-11-01";
            };
            runtime = 140;
            synopsis = "";
            title = "Star Wars: Episode III - Revenge of the Sith 3D";
            year = 2005;  // <-- date du premier film de la liste
        },  // <-- fin des données du premier film de la liste
               {  // <-- deuxième film de la liste
            "abridged_cast" =             ();
            ...
            "mpaa_rating" = "PG-13";
            posters =             {
                original = "http://content9.flixster.com/movie/11/17/38/11173843_ori.jpg";
            };
            runtime = 127;
            synopsis = "";
            title = "Star Trek";
            year = 2009;
        },
        ...
    );
    total = 1319;
}

 
En analysant la structure précédente, il est possible d’énoncer les axiomes suivants:

  1. Le dictionnaire contient un tableau de films à l’élément dictionnaire[@ »movies »].
  2. Toutes les informations d’un film sont disponibles à dictionnaire[@ »movies »][indiceDuFilm].
  3.  [@ »movies »][indiceDuFilm] retourne un dictionnaire
  4. Une information sur un film est disponible à dictionnaire[@ »movies »][indiceDuFilm] [@ »nomChamp »].

Ainsi, pour obtenir la date du premier film de la liste nous écrirons:

 dateDuPremierFilm = dictionnaire[@ »movies »][0][@ »year »]

QUESTION:  Comment obtenir l’affiche originale du deuxième film?
 
ACTION – Affichons le titre des films reçus (à la fin de la méthode: ‘rechercherFilms’)
:

// Corriger l'erreur avec une signature pour tableauDesFilms[@"movies"]
  for (int i = 0 ; i< tableauDesFilms[@"movies"].count; i++){
        NSLog(@"Titre %d = %@", i, tableauDesFilms[@"movies"][i][@"title"]);
}

 
 


Étape 2 – Actualiser la requête suite à ‘textFieldShouldReturn’

 
Remarquer qu’il y a une zone de recherche en haut à droite de l’application.  Nous allons utiliser la méthode de délégation ‘textFieldShouldReturn’ du ‘UITextField’  pour relancer la requête vers l’api de rottenTomatoes.
 
rotten-002
 
ACTION – Ajoutons le code suivant à la méthode ‘textFieldShouldReturn’ de la classe contrôleur de la scène principale.
 

- (BOOL)textFieldShouldReturn:(UITextField *)textField{
    // Convertir la chaine en format 'escaped' pour le web, par exemple, remplacer les espaces par %20, ...
    NSString *escapedText = [textField.text stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSLog(@"escapedText = %@", escapedText);
    // Relancer la requete vers rottenTomatoes
    [self rechercherFilms:escapedText];
    // Actualiser les cellules du UICollectionView
    [self.collectionDeFilms reloadData];
    // Arreter l'animation de progression
    [self.progression stopAnimating];
    [textField resignFirstResponder];
    return YES;
}

 
ACTION – Remplaçons  la trace sur le titre des films par le code suivant (méthode ‘rechercherFilms’).  Nous allons ici, utiliser l’énumération rapide pour parcourir les éléments de ‘tableauDesFilms[@ »movies »]’.
 

-(BOOL) rechercherFilms:(NSString *) film
...
    // Afficher le titre de tous les films trouvés
    for (NSDictionary * film in tableauDesFilms[@"movies"]) {
        NSLog(@"Titre = %@", film[@"title"]);
    }
 return ...

 
ACTION – Testons l’application
 
http://www.youtube.com/watch?v=SgfEfEuKYrU
 


Étape 3 – Renseigner les UICollectionViewCell

Nous savons qu’il est possible de créer un objet de type ‘UIImage’ à partir du nom d’un fichier livré avec l’application de la façon suivante:

[UIImage imageNamed:@ »pochette.jpg »];

Mais lorsque que ce fichier est stocké dans le réseau Internet il faut procéder ainsi:

[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:urlImage]]]

Note: La qualité de l’expérience utilisateur va être affectée par le chargement d’un grand nombre d’images.  Plus tard, nous vous proposerons une solution à ce problème.
 
ACTION –  Ajoutons le code suivant à la méthode ‘cellForItem…’ du fichier ViewController.m »
 

//  ViewController.m
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    CelluleRotten * celluleCourante = [collectionView dequeueReusableCellWithReuseIdentifier:@"modeleCellule" forIndexPath:indexPath];
    NSString * urlImage = tableauDesFilms[@"movies"][indexPath.row][@"posters"][@"original"];
    // Cette opération est très bloquante!!!
    // Charger une image à partir du web
    // À compléter ...
    celluleCourante.filmImage.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:urlImage]]];
    // Renseigner le titre du film
    // À compléter ...
    celluleCourante.titre.text = tableauDesFilms[@"movies"][indexPath.row][@"title"];
    return celluleCourante;
}

 
ACTION – Corrigeons la valeur de retour de  la méthode ‘numberOfItems…’
 

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return ((NSArray*)tableauDesFilms[@"movies"]).count;
}

 
ACTION – Ajoutons le code suivant à la méthode ‘prepareForSegue’
 

//  ViewController.m
// Passer les info à la scène de détail
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender   {
    DetailFilmViewController * vc = [segue destinationViewController];
    // Passer les informations de la section courant à la scène de détail
    // À compléter...
    vc.detailFilm = tableauDesFilms[@"movies"][[[self.ViewVideos indexPathForCell:sender] row]];
}

 
ACTION –  Augmentons le nombre maximum de films pour la requête vers l’api de rottenTomatoes
 

//  ViewController.h
#define NB_FILMS @"50"

 
ACTION – Testons l’application.  Vous allez remarquer un important manque de réponse de l’application.
 
http://www.youtube.com/watch?v=bsFFn6ULjyI
 
Télécharger le projet à cette étape
 


Étape 4 – Améliorer l’expérience utilisateur

 
Présentement, les requêtes web de l’application sont bloquantes, C-A-D que l’application attend la réponse de la requête avant de passer à la ligne de code suivante.  Cela a pour effet de bloquer aussi l’interactivité de l’application.
Le ‘framework’ cocoa nous propose l’objet ‘NSURLConnection‘ et la méthode suivante pour lancer une requête web non bloquante:

 [NSURLConnection
   sendAsynchronousRequest:UneRequete
   queue: uneFileExecution
completionHandler
:UnBlocDeCodeAExecuter  // à la réception des données
]

 
Présentement, nous utilisons la syntaxe suivante pour lancer une requête vers l’api de rottenTomatoes:
 

...
// Obtenir les données en format brut
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:URLFilm]];
...
// Convertir les données brutes en format JSON
tableauDesFilms = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
...

 
 ACTION – Modifions la méthode ‘rechercherFilms’ pour rendre la requête non bloquante

//  ViewController.m
// --------------------------------------------------------
-(BOOL) rechercherFilms:(NSString *) film
// --------------------------------------------------------
//  Description:  Méthode qui ...
{
    [self.progression startAnimating];
    NSString * adresseURL = [NSString stringWithFormat:@"http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=%@&q=%@&page_limit=%@", CLE_API,film, NB_FILMS];
    // Requete via URL non bloquante
    [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:adresseURL]]
                                       queue:[NSOperationQueue currentQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                               if (!error) { NSLog(@"Requete = succes");
                                   tableauDesFilms = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
                                   self.nbResultat.text = [NSString stringWithFormat:@"%d",((NSArray*)tableauDesFilms[@"movies"]).count];
                                   [self.collectionDeFilms reloadData];
                                   [self.progression stopAnimating];
                               } else {
                                   NSLog(@"Oups, problème avec la requête web, erreur = %@", error);
                               } // FIN -> if, else
                           }  // FIN -> completionHandler
     ];
    return YES;
} // FIN -> rechercherFilms

 
ACTION – Testons l’application.
Vous allez remarquer que la scène principale est affichée immédiatement.   Précédemment, la scène était affichée suite au traitement de la requête web qui bloquait l’exécution de l’application.
Par contre, le chargement des pochettes de films est encore bloquant.
Utilisons la même technique pour charger les images via le réseau Internet.
 
ACTION – Dans la méthode ‘cellForItemAtIndexPath‘, remplaçons la ligne de code  qui charge l’image via une URL par:
 

//  ViewController.m
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
...
    // Cette opération n'est plus bloquante!!!
    // Charger une image à partir du web
    //  celluleCourante.filmImage.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:urlImage]]];
    NSMutableURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlImage]];
    celluleCourante.filmImage.image = [UIImage imageNamed:@"loading.gif"];
    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                               if ( !error )
                               {
                                   celluleCourante.filmImage.image = [[UIImage alloc] initWithData:data];
                               }
                           }];
...
}

 
ACTION – À vous de modifier la classe de la scène ‘détail’ pour que le chargement des quatre affiches ne soit plus bloquant.
 
Télécharger le projet terminé 
 


Autres APIs

 
Voici une liste d’APIs disponibles via le web:
 

Répertoire d’APIs web

http://www.programmableweb.com/apis/directory
 

Météo sur Yahoo

http://query.yahooapis.com/v1/public/yql?q=select%20item%20from%20weather.forecast%20where%20location%3D%22CAXX0301%22&format=json
Ce qui donne la météo pour Montréal (CAXX0301)
Voir:  http://developer.yahoo.com/yql/
 

Cotes de la bourse sur Yahoo

http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20yahoo.finance.quotes%20where%20symbol%20in%20%28%22AAPL,AMZN%22%29&env=store://datatables.org/alltableswithkeys&format=json
Ce qui donne les cotes d’Apple et Amazon
 

tou.tv

http://api.tou.tv/v1/toutvapiservice.svc/json/GetPageRepertoire
Ce qui donne le répertoire de tou.tv
Voir:  https://code.google.com/p/tou-tv-for-boxee/wiki/Api
 

GiantBomb (jeux)

http://www.giantbomb.com/api/


Note:  L’image sur cette page est la propriété intellectuelle de Flixster.