Serieswatcher projet
Projet java réalisé sous Eclipse : Serieswatcher !

Début d’un nouveau projet nommé Serieswatcher. Ce projet consiste à créer un logiciel en java qui permet d’être mis au courant, d’être averti grâce à une alerte, des dernières dates de sorties des épisodes d’une série tv donnée. Par exemple je suis fan de la série Lost, Dexter et Greys anatomy, et j’aime regarder les épisodes sur internet dés qu’ils sortent. Serieswatcher permettra à tout fan de série tv de pouvoir être mis au courant directement lorsqu’un nouvel épisode est à l’affiche.

Dans un premier temps, le projet consiste à parser une page html afin d’en extraire les multiples infos pertinentes sur une série tv précise. Certains sites web proposent des informations complètes sur les dates de sorties des séries. Néanmoins, il peut être intéressant d’être directement alerter, mis sous alerte, lorsqu’un nouvel épisode sort en tv.

La première version de Serieswatcher pourrait être une version qui s’installe sur pc, avec icône dans la barre des taches qui émet une alerte dans la barre de taches en cas de nouvel épisode des séries tv choisis. Plus tard, peut-être une version pour gsm (Android), une version sous forme de ria (appli Facebook, applications web,…).

Voici une première ébauche des classes qui composent ce projet. Serieswatcher dans sa première version, utilise déjà plusieurs classes déjà prédéfini dans le jdk. Par exemple la classe Pattern, la classe Matcher qui sont deux classes très importante pour parser le contenu d’une page html, afin de trouver des mises à jour d’informations. Excellent pour s’initier doucement aux concepts d’expressions régulières. L’expression à chercher se trouve dans le pattern, le matcher est le moteur de comparaison entre le modèle pattern compilé et le matcher.

La classe urlConnection provenant de java.net pour se connecter à un site web avec java.

Les objets de type LinkedList aussi sont utilisées dans Serieswatcher. Chaque saison, chaque épisode est contenu dans une liste doublement chainée LinkedList. Pas de tableau ici, car les tableaux en java ne peuvent grandir. Une LinkedList est donc mieux adaptée.

Dans cette première version du logiciel Serieswatcher, une première interface graphique (gui) super basique à été commencée, il faut bien entendu continuer de la développer.
Voici les classes qui composent, lors de cette version, le projet Serieswatcher (Main, Site, Serie, Saison, Episode, Modele, Vue, Controleur)

Classe Main


public class Main {
	public static void main(String[] args) {
		// on instancie les classes mvc, mais la Gui doit encore être programmée
		Controleur controleur = new Controleur();
		Vue vue = new Vue(controleur);
		Modele modele = new Modele(controleur);
		controleur.setVue(vue);
		controleur.setModele(modele);

		vue.pack();
		vue.setVisible(true);
	}
}

Affichage de la classe Site

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Site {
	/**
	 * Cette méthode permet de récupérer une page HTTP (en HTML) depuis un site
	 * donné avec un nom de série donné sous forme d'une liste chainée.
	 *
	 * @pre    serieName doit être le nom d'une série dans le format du
	 * 		 	site (ex.: "PrisonBreak" ok, "Prison Break" pas ok)
	 * @return une page HTML sous forme d'une liste chainée de String
	 * 			où chaque String contient une ligne du code source de la page
	 *
	 * @todo : traiter le cas où la série n'existe pas + lever exception
	 */
	public static LinkedList<String> getHttpPage(String serieName) {
		String sURL = "http://www.epguides.com/" + serieName;
		String line = ""; // empty HTML page
		LinkedList<String> pageCodeList = new LinkedList<String>();

		try {
			URL epguides = new URL(sURL);
	        URLConnection conn = epguides.openConnection();

	        // version BufferedReader
	        BufferedReader bReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));

	        while((line = bReader.readLine()) != null) {
	        	pageCodeList.add(line); // on l'ajoute dans la liste
	        }

// version Scanner
/*	        String page2 = "";
	        Scanner scan = new Scanner(conn.getInputStream());

	        while(scan.hasNext()) {
	        	page2 += scan.next();
	        }
*/
		} catch(Exception e) {
			e.printStackTrace(); // todo : traiter avec GUI
		}

		return pageCodeList;
	}

	/**
	 * Cette méthode permet de tester notre pattern matcher sur le code
	 * source d'une page HTML.
	 *
	 * @pre  htmlSource est le code source HTML d'une page qui contient des
	 * 	      informations au sujet d'une série donnée
	 * @post les lignes qui matchent le pattern (voir code) seront affichées
	 *        à l'écran
	 */
	public static void showMatchingLines(LinkedList<String> htmlSource) {
		Pattern pattern = Pattern.compile("^\\s*\\d+\\.{0,1}\\s+.*$");
		//pattern = Pattern.compile("^\\s*\\d+\\.{0,1}\\s+4{1}-.*$"); // récupère les épisodes de la saison i

		String line = "";
		Matcher matcher;

		for(int i = 0; i < htmlSource.size(); i++) {
			line = htmlSource.get(i);
			matcher = pattern.matcher(line);
			if(matcher.find()) {
				System.out.println(line);
			}
		}
	}
}

Affichage de la classe Série

import java.util.Calendar;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Serie {
    String name; // ex.: Prison Break
    String urlName; // ex.: PrisonBreak
    Calendar startDate;
    Calendar endDate;
    LinkedList<Season> seasonsList;

    public Serie() {
	name = "<Unknown>";
	startDate = null;
	endDate = null;
	seasonsList = new LinkedList<Season>();
    }

    /**
     * Cette méthode permet de récupérer un objet de type Série
     * totalement complet et correctement initialisé.
     *
     * @pre    serieName doit être le nom d'une série dans le format du
     * 		 	site (ex.: "PrisonBreak" ok, "Prison Break" pas ok) et
     * 			qui existe
     * @return une série correctement construire et complète
     */
    public static Serie serieBuilder(String serieName) {
	LinkedList<String> htmlSource = Site.getHttpPage(serieName);

	Serie serie = new Serie();
	int seasonIndex = 1;
	boolean currentSeasonExists = true;

	Pattern pattern;
	Matcher matcher;

	// on commence la partie pattern matching pour les épisodes de chaque saison
	while(currentSeasonExists) {
	    Season currentSeason = new Season(serie, seasonIndex);

	    pattern = Pattern.compile("^\\s*\\d+\\.{0,1}\\s+" + seasonIndex + "{1}-.*$"); // récupère les épisodes de la saison i

	    // on parcourt tout le code source, ligne par ligne
	    for(int i = 0; i < htmlSource.size(); i++) {
		String line = htmlSource.get(i); // ligne courante
		matcher = pattern.matcher(line);
		if(matcher.find()) {
		    // si on trouve, cela signifie que 'line' est une ligne qui représente un épisode
		    // de la saison 'currentSeason'
		    Episode ep = Episode.episodeBuilder(currentSeason, line);
		    currentSeason.episodesList.add(ep);
		}
	    }

	    if(currentSeason.episodesList.size() > 0) {
		serie.seasonsList.add(currentSeason);
		seasonIndex++;
	    } else {
		currentSeasonExists = false;
	    }
	}

	return serie;
    }
}

Affichage de la classe Season

import java.util.LinkedList;

public class Season {
	int number;
	Serie serie;
	LinkedList<Episode> episodesList;

	public Season(Serie s, int i) {
		serie = s;
		number = i;
		episodesList = new LinkedList<Episode>();
	}
}

Affichage classe Episode

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Episode {
    String title;
    Season season;
    Calendar airDate;
    String link;
    int number; // numéro de l'épisode au sein de la saison

    /**
     * Cette méthode nous permet de récupérer les informations d'un épisode
     * à partir d'une ligne de code source.
     *
     * @pre    epSeason est l'object qui représente la saison de l'épisode
     * 		   sourceLine est une ligne de code source qui contient des informations
     * 			sur un épisode donné
     * @return un épisode construit à partir des informations contenues dans sourceLine
     */
    public static Episode episodeBuilder(Season epSeason, String sourceLine) {
	Pattern pat;
	Matcher mat;

	Episode ep = new Episode(); // on crée un objet Episode vide
	int epNumber = -1;
	Calendar airDate = new GregorianCalendar(0,0,0);
	String link = "N/A";
	String title = "N/A";

	pat = Pattern.compile("\\d{1,}"); // matching numéro épisode
	mat = pat.matcher(sourceLine);
	if(mat.find())
	    epNumber = Integer.parseInt(sourceLine.substring(mat.start(), mat.end()));

	pat = Pattern.compile("\\d{1,2}\\s{0,1}/{0,1}[a-zA-Z]{3}\\s{0,1}/{0,1}\\d{2}"); // matching air date de l'épisode
	mat = pat.matcher(sourceLine);
	if(mat.find()) {
	    String matchedDate = sourceLine.substring(mat.start(), mat.end());

	    // pour construire le Calendar, il nous faut le day, month number et year
	    String day = "N/A";
	    pat = Pattern.compile("^\\d{1,2}");
	    mat = pat.matcher(matchedDate);
	    if(mat.find()) day = matchedDate.substring(mat.start(), mat.end());

	    String month = "N/A";
	    pat = Pattern.compile("[a-zA-Z]{3}");
	    mat = pat.matcher(matchedDate);
	    if(mat.find()) month = matchedDate.substring(mat.start(), mat.end());

	    String year = "N/A";
	    pat = Pattern.compile("\\d{2}$");
	    mat = pat.matcher(matchedDate);
	    if(mat.find()) year = matchedDate.substring(mat.start(), mat.end());

	    // conversion du month en son numéro de mois dans l'année (voir doc méthode)
	    int monthNb = convertToMonthNumber(month);

	    airDate = new GregorianCalendar(Integer.parseInt(year), monthNb, Integer.parseInt(day));
	}

	pat = Pattern.compile("http://.*\"");
	mat = pat.matcher(sourceLine);
	if(mat.find()) link = sourceLine.substring(mat.start(), mat.end()-1);

	pat = Pattern.compile(">.*</a>");
	mat = pat.matcher(sourceLine);
	if(mat.find()) title = sourceLine.substring(mat.start()+1, mat.end()-4);

	// remplissage des attributs de l'objet
	ep.number = epNumber;
	ep.airDate = airDate;
	ep.link = link;
	ep.title = title;
	ep.season = epSeason;

	return ep;
    }

    /**
     * Cette méthode convertit un nom de mois anglais de trois caractères
     * en son numéro dans l'année (ex.: Mar => 3).
     *
     * @pre    month est un mois anglais de trois caractères (Jan, Feb, Mar, ...)
     * @return le numéro du mois dans l'année,
     * 			sinon -1 si month n'est pas un mois valide
     */
    public static int convertToMonthNumber(String month) {
	String[] monthsTab = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
	for(int i = 0; i < monthsTab.length; i++) {
	    // on fait la comparaison en lower case (pour être sûrs)
	    if((monthsTab[i].toLowerCase()).equals(month.toLowerCase()))
		return i+1;
	}
	return -1;
    }
}

Affichage classe vue

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

@SuppressWarnings("serial")
public class Vue extends JFrame {
    public Controleur controleur;
    public JComboBox serieChoice;
    public JComboBox seasonChoice;
    public JPanel contentPanel;
    public JPanel superPanel;

    public Vue(Controleur c) {
	controleur = c;

	// construction de l'interface graphique
	buildGUI();
    }

    public void buildGUI() {
	setTitle("Series Watcher v0.1");
	superPanel = new JPanel();
	BoxLayout bL = new BoxLayout(superPanel, BoxLayout.Y_AXIS);
	superPanel.setLayout(bL);
	add(superPanel);

	serieChoice = new JComboBox(new String[]{"Lost", "PrisonBreak", "DesperateHousewives"});
	serieChoice.addActionListener(new ActionListener() {
	    @Override
	    public void actionPerformed(ActionEvent e) {
		controleur.updateSerie();
	    }
	});
	seasonChoice = new JComboBox();
	JScrollPane jScrollPane = new JScrollPane();
	contentPanel = new JPanel();

	jScrollPane.add(contentPanel);

	superPanel.add(serieChoice);
	superPanel.add(seasonChoice);
	superPanel.add(jScrollPane);
    }

    public void showSeasonList(int nbSeasons) {
	seasonChoice.removeAllItems();
	for(int i = 0; i < nbSeasons; i++)
	    seasonChoice.addItem("Season " + (i+1));
    }
}

Affichage classe modele


public class Modele {
    public Controleur controleur;

    public Modele(Controleur c) {
	controleur = c;
    }
}

Affichage de la classe controlleur


public class Controleur {
    public Vue vue;
    public Modele modele;

    public void updateSerie() {
	String serieName = (String) vue.serieChoice.getSelectedItem();
	Serie serie = Serie.serieBuilder(serieName);
	vue.showSeasonList(serie.seasonsList.size());
    }

    public void setVue(Vue v) {
	vue = v;
    }
    public void setModele(Modele m) {
	modele = m;
    }
}

note: il ne s’agit que d’une première version non finale de Serieswatcher. De nombreuses fonctionnalités doivent y être pensées et développées. Cette première version permet de se frotter à un premier projet de conception un peu plus costaud que de simples exercices d’algorithmes. Exercices sur les expressions régulières, la classe urlconnection pour récupérer le contenu d’une page web.