Shows must go on

Publié dans développement
le

Un bien joli titre pour poursuivre le développement de notre gem nommée shows !

Aujourd'hui, nous allons faire en sorte d'aller récupérer le résumé d'une série selon son nom, de gérer les éventuelles erreurs et tout cela grâce à l'API de thetvdb.

Avant d'aller plus loin

Vous vous souvenez de mon paragraphe du dernier post dans lequel j'expliquais qu'en général, on préférera ne pas inclure de base la partie interface dans l'import de base de notre gem ? Et bien faisons le de suite, dans le fichier lib/shows.rb, nous enlevons la ligne :

require 'shows/cli'

Et dans le fichier bin/shows, on rectifie l'import de manière à avoir ceci :

# -*- encoding: utf-8 -*-
#!/usr/bin/env ruby

require 'shows/cli'

Shows::execute

Voilà, maintenant nous pouvons continuer.

TVDB

Pour récupérer les informations de nos séries préférées, nous allons utiliser l'API du site thetvdb. Et donc, il va nous falloir un token, je vous invite à vous inscrire sur le site. Une fois inscrit et connecté, rendez-vous sur la page suivante, donner un nom à votre projet, une URL (pour ma part, ce sera shows et l'adresse Github du projet) et le tour est joué.

Une fois le formulaire validé, vous êtes redirigé vers votre profil et vous pouvez récupérer votre précieuse clef. Garder la bien au chaud, nous en aurons besoin dans les prochains articles.

La classe Show

Dans le répertoire lib/shows de notre application, nous allons créer la classe Show qui représentera toute la partie "série" de notre application. Nous avons donc dans le fichier lib/shows/show.rb :

# -*- encoding: utf-8 -*-
module Shows

    class Show

        attr_reader :id, :name, :summary

        def initialize(id, name, summary)
            @id = id
            @name = name
            @summary = summary
        end

        def self.find_by_name(name)
            # Devra retourner un objet Show
        end

    end

end

Rien de fantastique, un constructeur nous permettant de préciser l'id tvdb, le nom et le résumé de la série. Vous remarquerez aussi une méthode de classe find_by_name qui nous permettra de récupérer un object Show en passant le nom d'une série, nous y reviendrons.

De manière à ce que cette classe soit chargée lors du require 'shows', ajoutons la au fichier lib/shows.rb:

# -*- encoding: utf-8 -*-
require 'shows/show'

Et pour boucler la boucle, dans le fichier bin/shows :

# -*- encoding: utf-8 -*-
#!/usr/bin/env ruby

require 'shows'
require 'shows/cli'

Shows::execute

De cette manière, dans notre exécutable, nous incluons la librairie shows ainsi que le composant particulier shows/cli nous permettant de profiter d'un interpréteur de commande basique. On aurait d'ailleur pu mettre tout le code propre à l'interface console directement dans le fichier bin/shows.

Un peu d'architecture

Dans le but d'avoir une architecture un temps soit peu modulable, nous allons créer la classe de base pour nos fournisseurs. Nous créerons ensuite l'implémentation de notre fournisseur pour le site tvdb. On crée donc le fichier lib/provider.rb tel que :

# -*- encoding: utf-8 -*-
module Shows

    module Provider

        @@provider = nil

        def self.set(provider)
            @@provider = provider
        end

        def self.shows_by_name(show_name)
            raise NotImplementedError.new if @@provider.nil? || !@@provider.respond_to?(:shows_by_name)

            @@provider.shows_by_name(show_name)
        end

    end

end

Ici, on a une sorte de façade qui nous permet d'appeler le fournisseur identifié grâce au Provider::set qu'on utilisera par la suite. On a ajouté une simple vérification pour lever une exception si le fournisseur n'est pas initialisé ou s'il ne répond pas à la méthode que l'on s'apprète à appeler.

On ajoute aussi une ligne dans le fichier lib/shows.rb pour charger ce fichier :

require 'shows/provider'

Du côté de notre classe Show dans lib/shows/show.rb, on complète la méthode find_by_name :

def self.find_by_name(name)
    shows = Provider::shows_by_name(name)
end

Nous allons désormais pouvoir nous attaquer à notre fournisseur ! On crée le fichier lib/shows/providers/tvdb.rb qui pour le moment ne comporte que le strict minimum :

# -*- encoding: utf-8 -*-
module Shows

    module TVDB

        def self.shows_by_name(show_name)
            # @TODO À implémenter
        end

    end

end

Et pour finir cette mise en place, dans le fichier lib/shows/cli.rb, on modifie la méthode execute :

# On ajoute l'import en dessous de: require 'shows/version'
require 'shows/providers/tvdb'

# Et à l'intérieur du module, on modifie la méthode execute
def self.execute
    Provider::set(TVDB)

    Show.find_by_name(ARGV.first)
end

Rien de très compliqué, on initialise notre fournisseur tvdb, de cette manière, tous les appels d'API seront fait via le module Provider, qui, selon l'implémentation donnée, nous retournera le résultat attendu. Vous remarquez qu'on appelle le find_by_name en passant ARGV.first, notre outil s'utilisera donc de la manière suivante shows "Nom de la série".

Une implémentation pour theTVDB

Pour finir cet article, nous allons implémenter la fameuse méthode shows_by_name de notre module TVDB. Cette méthode ira récupérer les informations depuis l'API dont une description est disponible ici. L'API retournant du XML, il sera nécessaire de le parcourir afin de créer nos objets Show.

D'ailleurs le chemin qui nous intéresse est le suivant. Nous avons donc toutes les informations nécessaires, à nos claviers !

Et voici le résultat :

# -*- encoding: utf-8 -*-
require 'cgi'
require 'uri'
require 'net/http'
require 'rexml/document'

module Shows

    module TVDB

        # L'URL de base de l'API (peut être remplacée par un mirroir)
        BASE_URL = "http://thetvdb.com"
        # Le path pour l'API en question
        API_URL = BASE_URL + "/api"

        def self.shows_by_name(show_name)

            # On lève une exception si aucun nom n'est donné
            raise ArgumentError.new('show_name could not be nil') if show_name.nil?

            shows = []

            # On commence par créer notre URI
            uri = URI.parse('%s/GetSeries.php?seriesname=%s' % [API_URL, CGI.escape(show_name)])

            # Ensuite on effectue une requête de manière à récupérer le XML
            res = Net::HTTP.get_response(uri)

            # Si la requête a bien été exécutée
            if res.code == '200'
                # On construit le document xml
                doc = REXML::Document.new(res.body)

                # Et on parcourt tous les noeuds qui nous intéressent
                doc.elements.each('Data/Series') do |s_node|
                    id = s_node.elements['id'] && s_node.elements['id'].text
                    name = s_node.elements['SeriesName'] && s_node.elements['SeriesName'].text
                    overview = s_node.elements['Overview'] && s_node.elements['Overview'].text

                    # Pour chaque série, on crée un object Show
                    shows << Show.new(id, name, overview)
                end
            end

            shows
        end

    end

end

On utilise donc plusieurs modules de la librairie standard. Pour parser le XML, il existe un grand nombre de gems potentiellement plus intéressante, mais je souhaitais avant tout utiliser la boîte à outils de base pour ce petit projet.

Pour le moment, si jamais la requête n'aboutie pas, on retourne un tableau vide, dans le cas contraire, on retourne un tableau de Show qui correspondent au nom recherché.

Nous avons presque terminé, il ne nous reste plus qu'à modifier un peu plus notre fichier lib/shows/show.rb. On commence par définir 2 types d'exceptions supplémentaires propres à notre gem :

class NoShowFound < StandardError
end

class MultipleShowsFound < StandardError
end

Et ensuite nous pouvons modifier notre méthode find_by_name qui utilise ces exceptions :

def self.find_by_name(name)
    shows = Provider::shows_by_name(name)

    raise NoShowFound.new('No show found for "%s"' % name) if shows.empty?
    raise MultipleShowsFound.new('Multiple shows found for "%s"' % name) if shows.length > 1

    shows.first
end

Il ne nous reste plus qu'à modifier notre petit exécutable dans lib/shows/cli.rb de manière à intercepter ces erreurs :

def self.execute
    Provider::set(TVDB)

    begin
        show = Show.find_by_name(ARGV.first)
        puts(show.summary)
    rescue Exception => e
        puts(e.message)
    end
end

Après un petit rake install, vous pouvez désormais utiliser l'application avec la série de votre choix, par exemple: shows "The Office US".

Voilà qui conclut cet article, l'application est désormais capable d'afficher le résumé d'une série et de nous informer en cas d'erreurs. Dans les prochains épisodes, nous essayerons de rendre cette gem plus utile et écrirons sans doute des tests unitaires ;) En attendant, n'oubliez pas que le code source se trouve sur Github.