Shows must go on
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.