Bugfix: Arte changed the xml feed url. Grrr\!
[totem-plugin-arte:mainline.git] / arteplus7.vala
1 /*
2  * Totem Arte Plugin allows you to watch streams from arte.tv
3  * Copyright (C) 2009, 2010 Simon Wenner <simon@wenner.ch>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
18  *
19  * The Totem Arte Plugin project hereby grants permission for non-GPL compatible
20  * GStreamer plugins to be used and distributed together with GStreamer, Totem
21  * and Totem Arte Plugin. This permission is above and beyond the permissions
22  * granted by the GPL license by which Totem Arte Plugin is covered.
23  * If you modify this code, you may extend this exception to your version of the
24  * code, but you are not obligated to do so. If you do not wish to do so,
25  * delete this exception statement from your version.
26  *
27  */
28
29 using GLib;
30 using Soup;
31 using Totem;
32 using Gtk;
33 using GConf;
34
35 public enum VideoQuality
36 {
37     UNKNOWN = 0,
38     WMV_MQ,
39     WMV_HQ,
40     FLV_MQ,
41     FLV_HQ
42 }
43
44 public enum Language
45 {
46     UNKNOWN = 0,
47     FRENCH,
48     GERMAN
49 }
50
51 public const string USER_AGENT =
52     "Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.10) Gecko/20100915 Firefox/3.6.10";
53 public const string GCONF_ROOT = "/apps/totem/plugins/arteplus7";
54 public const string GCONF_HTTP_PROXY = "/system/http_proxy";
55 public const string CACHE_PATH_SUFFIX = "/totem/plugins/arteplus7/";
56 public const int THUMBNAIL_WIDTH = 160;
57 public const string DEFAULT_THUMBNAIL = "/usr/share/totem/plugins/arteplus7/arteplus7-default.png";
58 public bool use_proxy = false;
59 public Soup.URI proxy_uri;
60 public string proxy_username;
61 public string proxy_password;
62
63 public static Soup.SessionAsync create_session ()
64 {
65     Soup.SessionAsync session;
66     if (use_proxy) {
67         session = new Soup.SessionAsync.with_options (
68                 Soup.SESSION_USER_AGENT, USER_AGENT,
69                 Soup.SESSION_PROXY_URI, proxy_uri, null);
70
71         session.authenticate.connect((sess, msg, auth, retrying) => {
72             /* watch if authentication is needed */
73             if (!retrying) {
74                 auth.authenticate (proxy_username, proxy_password);
75             } else {
76                 GLib.warning ("Proxy authentication failed!\n");
77             }
78         });
79     } else {
80         session = new Soup.SessionAsync.with_options (
81                 Soup.SESSION_USER_AGENT, USER_AGENT, null);
82     }
83     session.timeout = 15; /* 15 seconds timeout, until we give up and show an error message */
84     return session;
85 }
86
87 public class Video : GLib.Object
88 {
89     public string title = null;
90     public string page_url = null;
91     public string image_url = null;
92     public string desc = null;
93     public GLib.TimeVal publication_date;
94     public GLib.TimeVal offline_date;
95
96     public Video()
97     {
98          publication_date.tv_sec = 0;
99          offline_date.tv_sec = 0;
100     }
101
102     public void print ()
103     {
104         stdout.printf ("Video: %s: %s, %s, %s\n", title,
105                 publication_date.to_iso8601 (),
106                 offline_date.to_iso8601 (), page_url);
107     }
108
109     public string get_stream_uri (VideoQuality q, Language lang)
110         throws ExtractionError
111     {
112         var extractor = new RTMPStreamUrlExtractor ();
113         return extractor.get_url (q, lang, page_url);
114     }
115 }
116
117 public abstract class ArteParser : GLib.Object
118 {
119     public string xml_fr;
120     public string xml_de;
121     public GLib.SList<Video> videos;
122
123     private const MarkupParser parser = {
124             open_tag,
125             close_tag,
126             process_text,
127             null,
128             null
129         };
130
131     public ArteParser ()
132     {
133         videos = new GLib.SList<Video>();
134     }
135
136     public virtual void reset () {}
137     public virtual void set_page (int page) {}
138
139     public void parse (Language lang) throws MarkupError, IOError
140     {
141         Soup.Message msg;
142         if (lang == Language.GERMAN) {
143             msg = new Soup.Message ("GET", xml_de);
144         } else {
145             msg = new Soup.Message ("GET", xml_fr);
146         }
147
148         Soup.SessionAsync session = create_session ();
149
150         session.send_message (msg);
151
152         if (msg.status_code != Soup.KnownStatusCode.OK) {
153             throw new IOError.HOST_NOT_FOUND ("plus7.arte.tv could not be accessed.");
154         }
155
156         var context = new MarkupParseContext (parser,
157                 MarkupParseFlags.TREAT_CDATA_AS_TEXT, this, null);
158         context.parse (msg.response_body.flatten ().data,
159                 (ssize_t) msg.response_body.length);
160         context.end_parse ();
161     }
162
163     protected virtual void open_tag (MarkupParseContext ctx,
164             string elem,
165             string[] attribute_names,
166             string[] attribute_values) throws MarkupError {}
167
168     protected virtual void close_tag (MarkupParseContext ctx,
169             string elem) throws MarkupError {}
170
171     protected virtual void process_text (MarkupParseContext ctx,
172             string text,
173             size_t text_len) throws MarkupError {}
174 }
175
176 public class ArteRSSParser : ArteParser
177 {
178     private Video current_video = null;
179     private string current_data = null;
180
181     public ArteRSSParser ()
182     {
183         /* Parses the official RSS feed */
184         xml_fr =
185             "http://videos.arte.tv/fr/do_delegate/videos/arte7/index-3188666,view,rss.xml";
186         xml_de =
187             "http://videos.arte.tv/de/do_delegate/videos/arte7/index-3188666,view,rss.xml";
188     }
189
190     private override void open_tag (MarkupParseContext ctx,
191             string elem,
192             string[] attribute_names,
193             string[] attribute_values) throws MarkupError
194     {
195         switch (elem) {
196             case "item":
197                 current_video = new Video();
198                 break;
199             default:
200                 current_data = elem;
201                 break;
202         }
203     }
204
205     private override void close_tag (MarkupParseContext ctx,
206             string elem) throws MarkupError
207     {
208         switch (elem) {
209             case "item":
210                 if (current_video != null) {
211                     videos.append (current_video);
212                     current_video = null;
213                 }
214                 break;
215             default:
216                 current_data = null;
217                 break;
218         }
219     }
220
221     private override void process_text (MarkupParseContext ctx,
222             string text,
223             size_t text_len) throws MarkupError
224     {
225         if (current_video != null) {
226             switch (current_data) {
227                 case "title":
228                     current_video.title = text;
229                     break;
230                 case "link":
231                     current_video.page_url = text;
232                     break;
233                 case "description":
234                     current_video.desc = text;
235                     break;
236                 case "pubDate":
237                     current_video.publication_date.from_iso8601 (text);
238                     break;
239             }
240         }
241     }
242 }
243
244 public class ArteXMLParser : ArteParser
245 {
246     private Video current_video = null;
247     private string current_data = null;
248     public int page = 1;
249     /* Parses the XML feed of the Flash preview plugin */
250     private const string xml_tmpl =
251         "http://videos.arte.tv/%s/do_delegate/videos/index-3188698,view,asXml.xml?hash=%s////%d/10/";
252
253     public ArteXMLParser ()
254     {
255         reset ();
256     }
257
258     public override void reset ()
259     {
260         videos = new GLib.SList<Video>();
261         this.page = 1;
262         xml_fr = xml_tmpl.printf ("fr", "fr", page);
263         xml_de = xml_tmpl.printf ("de", "de", page);
264     }
265
266     public override void set_page (int page)
267     {
268         this.page = page;
269         xml_fr = xml_tmpl.printf ("fr", "fr", page);
270         xml_de = xml_tmpl.printf ("de", "de", page);
271     }
272
273     private override void open_tag (MarkupParseContext ctx,
274             string elem,
275             string[] attribute_names,
276             string[] attribute_values) throws MarkupError
277     {
278         switch (elem) {
279             case "video":
280                 current_video = new Video();
281                 break;
282             default:
283                 current_data = elem;
284                 break;
285         }
286     }
287
288     private override void close_tag (MarkupParseContext ctx,
289             string elem) throws MarkupError
290     {
291         switch (elem) {
292             case "video":
293                 if (current_video != null) {
294                     videos.prepend (current_video);
295                     current_video = null;
296                 }
297                 break;
298             default:
299                 current_data = null;
300                 break;
301         }
302     }
303
304     private override void process_text (MarkupParseContext ctx,
305             string text,
306             size_t text_len) throws MarkupError
307     {
308         if (current_video != null) {
309             switch (current_data) {
310                 case "title":
311                     current_video.title = text;
312                     break;
313                 case "targetUrl":
314                     current_video.page_url = "http://videos.arte.tv" + text;
315                     break;
316                 case "imageUrl":
317                     current_video.image_url = "http://videos.arte.tv" + text;
318                     break;
319                 case "teaserText":
320                     current_video.desc = text;
321                     break;
322                 case "startDate":
323                     current_video.publication_date.from_iso8601 (text);
324                     break;
325                 case "endDate":
326                     current_video.offline_date.from_iso8601 (text);
327                     break;
328             }
329         }
330     }
331 }
332
333 class ArtePlugin : Totem.Plugin
334 {
335     private Totem.Object t;
336     private Gtk.Entry search_entry; /* search field with buttons inside */
337     private Gtk.TreeView tree_view; /* list of movie thumbnails */
338     private ArteParser p;
339     private Cache cache; /* image thumbnail cache */
340     private Language language = Language.FRENCH;
341     private VideoQuality quality = VideoQuality.WMV_HQ;
342     private GLib.StaticMutex tree_lock;
343     private bool use_fallback_feed = false;
344     private string? filter = null;
345
346     /* TreeView column names */
347     private enum Col {
348         IMAGE,
349         NAME,
350         DESCRIPTION,
351         VIDEO_OBJECT,
352         N
353     }
354
355     public override bool activate (Totem.Object totem) throws GLib.Error
356     {
357         t = totem;
358         load_properties ();
359         cache = new Cache (Environment.get_user_cache_dir ()
360              + CACHE_PATH_SUFFIX);
361         p = new ArteXMLParser ();
362         tree_view = new Gtk.TreeView ();
363
364         var renderer = new Totem.CellRendererVideo (false);
365         tree_view.insert_column_with_attributes (0, "", renderer,
366                 "thumbnail", Col.IMAGE,
367                 "title", Col.NAME, null);
368         tree_view.set_headers_visible (false);
369         tree_view.set_tooltip_column (Col.DESCRIPTION);
370         tree_view.row_activated.connect (callback_select_video_in_tree_view);
371
372         var scroll_win = new Gtk.ScrolledWindow (null, null);
373         scroll_win.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
374         scroll_win.set_shadow_type (ShadowType.IN);
375         scroll_win.add (tree_view);
376
377         /* add a search entry with a refresh and a cleanup icon */
378         search_entry = new Gtk.Entry ();
379         search_entry.set_icon_from_stock (Gtk.EntryIconPosition.PRIMARY,
380                 Gtk.STOCK_REFRESH);
381         search_entry.set_icon_tooltip_text (Gtk.EntryIconPosition.PRIMARY,
382                 _("Reload feed"));
383         search_entry.set_icon_from_stock (Gtk.EntryIconPosition.SECONDARY,
384                 Gtk.STOCK_CLEAR);
385         search_entry.set_icon_tooltip_text (Gtk.EntryIconPosition.SECONDARY,
386                 _("Clear the search text"));
387         search_entry.set_icon_sensitive (Gtk.EntryIconPosition.SECONDARY, false);
388         /* search as you type */
389         search_entry.changed.connect ((widget) => {
390             Gtk.Entry entry = (Gtk.Entry) widget;
391             entry.set_icon_sensitive (Gtk.EntryIconPosition.SECONDARY,
392                     (entry.get_text () != ""));
393
394             filter = entry.get_text ().down ();
395             var model = (Gtk.TreeModelFilter) tree_view.get_model ();
396             model.refilter ();
397         });
398         /* set focus to the first video on return */
399         search_entry.activate.connect ((entry) => {
400             tree_view.grab_focus ();
401         });
402         /* cleanup or refresh on click */
403         search_entry.icon_press.connect ((entry, position, event) => {
404             if (position == Gtk.EntryIconPosition.PRIMARY)
405                 callback_refresh_rss_feed (entry);
406             else
407                 entry.set_text ("");
408         });
409
410         var main_box = new Gtk.VBox (false, 4);
411         main_box.pack_start (search_entry, false, false, 0);
412         main_box.pack_start (scroll_win, true, true, 0);
413         main_box.show_all ();
414
415         totem.add_sidebar_page ("arte", _("Arte+7"), main_box);
416         GLib.Idle.add (refresh_rss_feed);
417         /* delete all files in the cache that are older than 8 days
418          * with probability 1/5 at every startup */
419         if (GLib.Random.next_int () % 5 == 0) {
420             GLib.Idle.add (() => {
421                 cache.delete_cruft (8);
422                 return false;
423             });
424         }
425
426         /* Refresh the feed on pressing 'F5' */
427         var window = t.get_main_window ();
428         window.key_press_event.connect (callback_F5_pressed);
429
430         return true;
431     }
432
433     public override void deactivate (Totem.Object totem)
434     {
435         /* Remove the 'F5' key event handler */
436         var window = t.get_main_window ();
437         window.key_press_event.disconnect (callback_F5_pressed);
438         /* Remove the plugin tab */
439         totem.remove_sidebar_page ("arte");
440     }
441
442     public override Gtk.Widget create_configure_dialog ()
443     {
444         var langs = new Gtk.ComboBox.text ();
445         langs.append_text (_("German"));
446         langs.append_text (_("French"));
447         if (language == Language.GERMAN)
448             langs.set_active (0);
449         else
450             langs.set_active (1);
451         langs.changed.connect (callback_language_changed);
452
453         var quali_radio_medium = new Gtk.RadioButton.with_mnemonic (null, _("_medium"));
454         var quali_radio_high = new Gtk.RadioButton.with_mnemonic_from_widget (
455                 quali_radio_medium, _("_high"));
456         if (quality == VideoQuality.WMV_MQ)
457             quali_radio_medium.set_active (true);
458         else
459             quali_radio_high.set_active (true);
460
461         quali_radio_medium.toggled.connect (callback_quality_toggled);
462
463         var langs_label = new Gtk.Label (_("Language:"));
464         var langs_box = new HBox (false, 20);
465         langs_box.pack_start (langs_label, false, true, 0);
466         langs_box.pack_start (langs, true, true, 0);
467
468         var quali_label = new Gtk.Label (_("Video quality:"));
469         var quali_box = new HBox (false, 20);
470         quali_box.pack_start (quali_label, false, true, 0);
471         quali_box.pack_start (quali_radio_medium, false, true, 0);
472         quali_box.pack_start (quali_radio_high, true, true, 0);
473
474         var dialog = new Dialog.with_buttons (_("Arte+7 Plugin Properties"),
475                 null, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
476                 Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE, null);
477         dialog.has_separator = false;
478         dialog.resizable = false;
479         dialog.border_width = 5;
480         dialog.vbox.spacing = 10;
481         dialog.vbox.pack_start (langs_box, false, true, 0);
482         dialog.vbox.pack_start (quali_box, false, true, 0);
483         dialog.show_all ();
484
485         dialog.response.connect ((source, response_id) => {
486             if (response_id == Gtk.ResponseType.CLOSE)
487                 dialog.destroy ();
488         });
489
490         return dialog;
491     }
492
493     public bool refresh_rss_feed ()
494     {
495         if (!tree_lock.trylock ())
496             return false;
497
498         search_entry.set_sensitive (false);
499
500         TreeIter iter;
501
502         /* display loading message */
503         var tmp_ls = new ListStore (3, typeof (Gdk.Pixbuf),
504                 typeof (string), typeof (string));
505         tmp_ls.prepend (out iter);
506         tmp_ls.set (iter,
507                 Col.IMAGE, null,
508                 Col.NAME, _("Loading..."),
509                 Col.DESCRIPTION, null, -1);
510         tree_view.set_model (tmp_ls);
511
512         /* download and parse */
513         try {
514             p.reset ();
515             if (!use_fallback_feed) {
516                 for (int i=1; i<10; i++) {
517                     p.set_page (i);
518                     p.parse (language);
519                     GLib.message ("Fetching page %d: Video count: %u", i, p.videos.length ());
520                 }
521             } else {
522                 p.parse (language);
523             }
524             GLib.message ("Total video count: %u", p.videos.length ());
525             /* sort the videos by removal date */
526             p.videos.sort ((a, b) => {
527                 return (int) (((Video) a).offline_date.tv_sec > ((Video) b).offline_date.tv_sec);
528             });
529         } catch (MarkupError e) {
530             GLib.critical ("XML Parse Error: %s", e.message);
531             if (!use_fallback_feed) {
532                 /* The default XML feed parser failed.
533                  * Switch to the RSS fallback feed without thumbnails. */
534                 p = new ArteRSSParser();
535                 use_fallback_feed = true;
536                 tree_lock.unlock ();
537                 /* ... and try again. */
538                 refresh_rss_feed ();
539             } else {
540                 /* We are screwed! */
541                 t.action_error (_("Markup Parser Error"),
542                     _("Sorry, the plugin could not parse the Arte video feed."));
543                 tree_lock.unlock ();
544             }
545             search_entry.set_sensitive (true);
546             return false;
547         } catch (IOError e) {
548             GLib.critical ("Network problems: %s", e.message);
549             if (!use_fallback_feed) {
550                 /* The default XML feed parser failed.
551                  * Switch to the RSS fallback feed without thumbnails. */
552                 p = new ArteRSSParser();
553                 use_fallback_feed = true;
554                 tree_lock.unlock ();
555                 /* ... and try again. */
556                 refresh_rss_feed ();
557             } else {
558                 t.action_error (_("Network problem"),
559                     _("Sorry, the plugin could not download the Arte video feed.\nPlease verify your network settings and (if any) your proxy settings."));
560                 tree_lock.unlock ();
561             }
562             search_entry.set_sensitive (true);
563             return false;
564         }
565
566         /* load the video list */
567         var listmodel = new ListStore (Col.N, typeof (Gdk.Pixbuf),
568                 typeof (string), typeof (string), typeof (Video));
569
570         /* save the last move to detect duplicates */
571         Video last_video = null;
572         int videocount = 0;
573
574         foreach (Video v in p.videos) {
575             /* check for duplicates */
576             if (last_video != null && v.page_url == last_video.page_url) {
577               last_video = v;
578               continue;
579             }
580             last_video = v;
581             videocount++;
582
583             listmodel.append (out iter);
584
585             string desc_str;
586             /* use the description if available, fallback to the title otherwise */
587             if (v.desc != null) {
588               desc_str = v.desc;
589             } else {
590               desc_str = v.title;
591             }
592
593             if (v.offline_date.tv_sec > 0) {
594                 desc_str += "\n";
595                 var now = GLib.TimeVal ();
596                 now.get_current_time ();
597                 double minutes_left = (v.offline_date.tv_sec - now.tv_sec) / 60.0;
598                 if (minutes_left < 59.0) {
599                     if (minutes_left < 1.0)
600                         desc_str += _("Less than 1 minute until removal");
601                     else
602                         desc_str += _("Less than %.0f minutes until removal").printf (minutes_left + 1.0);
603                 } else if (minutes_left < 60.0 * 24.0) {
604                     if (minutes_left <= 60.0)
605                         desc_str += _("Less than 1 hour until removal");
606                     else
607                         desc_str += _("Less than %.0f hours until removal").printf ((minutes_left / 60.0) + 1.0);
608                 } else if (minutes_left < (60.0 * 24.0) * 2.0) {
609                     desc_str += _("1 day until removal");
610                 } else {
611                     desc_str += _("%.0f days until removal").printf (minutes_left / (60.0 * 24.0));
612                 }
613             }
614
615             listmodel.set (iter,
616                     Col.IMAGE, cache.load_pixbuf (v.image_url),
617                     Col.NAME, v.title,
618                     Col.DESCRIPTION, desc_str,
619                     Col.VIDEO_OBJECT, v, -1);
620         }
621
622         var model_filter = new Gtk.TreeModelFilter (listmodel, null);
623         model_filter.set_visible_func (callback_filter_tree);
624
625         tree_view.set_model (model_filter);
626
627         tree_lock.unlock ();
628         search_entry.set_sensitive (true);
629         search_entry.grab_focus ();
630         GLib.message ("Unique video count: %d", videocount);
631
632         /* Download missing thumbnails */
633         check_and_download_missing_thumbnails (listmodel);
634
635         return false;
636     }
637
638     private bool callback_filter_tree (Gtk.TreeModel model, Gtk.TreeIter iter)
639     {
640         string title;
641         model.get (iter, Col.NAME, out title);
642         if (filter == null || title.down ().contains (filter))
643             return true;
644         else
645             return false;
646     }
647
648     private void check_and_download_missing_thumbnails (Gtk.ListStore list)
649     {
650         TreeIter iter;
651         Gdk.Pixbuf pb;
652         string md5_pb;
653         Video v;
654         var path = new TreePath.first ();
655
656         string md5_default_pb = Checksum.compute_for_data (ChecksumType.MD5,
657                 cache.default_thumbnail.get_pixels ());
658
659         for (int i=1; i<=list.length; i++) {
660             list.get_iter (out iter, path);
661             list.get (iter, Col.IMAGE, out pb);
662             md5_pb = Checksum.compute_for_data (ChecksumType.MD5, pb.get_pixels ());
663             if (md5_pb == md5_default_pb) {
664                 list.get (iter, Col.VIDEO_OBJECT, out v);
665                 if (v.image_url != null) {
666                     GLib.message ("Missing thumbnail: %s", v.title); // Debug
667                     list.set (iter, Col.IMAGE, cache.download_pixbuf (v.image_url));
668                 }
669             }
670             path.next ();
671         }
672     }
673
674     /* stores properties in gconf */
675     private void store_properties ()
676     {
677         var gc = GConf.Client.get_default ();
678         try {
679             gc.set_int (GCONF_ROOT + "/quality", (int) quality);
680             gc.set_int (GCONF_ROOT + "/language", (int) language);
681         } catch (GLib.Error e) {
682             GLib.warning ("%s", e.message);
683         }
684     }
685
686     /* loads properties from gconf */
687     private void load_properties ()
688     {
689         var gc = GConf.Client.get_default ();
690         string parsed_proxy_uri = "";
691         int proxy_port;
692         
693         try {
694             quality = (VideoQuality) gc.get_int (GCONF_ROOT + "/quality");
695             language = (Language) gc.get_int (GCONF_ROOT + "/language");
696             use_proxy = gc.get_bool (GCONF_HTTP_PROXY + "/use_http_proxy");
697             if (use_proxy) {
698                 parsed_proxy_uri = gc.get_string (GCONF_HTTP_PROXY + "/host");
699                 proxy_port = gc.get_int (GCONF_HTTP_PROXY + "/port");
700                 if (parsed_proxy_uri == "") {
701                     use_proxy = false; /* necessary to prevent a crash in this case */
702                 } else {
703                     proxy_uri = new Soup.URI ("http://" + parsed_proxy_uri + ":" + proxy_port.to_string());
704                     GLib.message ("Using proxy: %s", proxy_uri.to_string (false));
705                     proxy_username = gc.get_string (GCONF_HTTP_PROXY + "/authentication_user");
706                     proxy_password = gc.get_string (GCONF_HTTP_PROXY + "/authentication_password");
707                 }
708             }
709         } catch (GLib.Error e) {
710             GLib.warning ("%s", e.message);
711         }
712         if (quality == VideoQuality.UNKNOWN) { /* HQ is the default quality */
713             quality = VideoQuality.WMV_HQ;
714             store_properties ();
715         }
716         if (language == Language.UNKNOWN) { /* Try to guess user prefer language at first run */
717             var env_lang = Environment.get_variable ("LANG");
718             if (env_lang != null && env_lang.substring (0,2) == "de") {
719                 language = Language.GERMAN;
720             } else {
721                 language = Language.FRENCH; /* Otherwise, French is the default language */
722             }
723             store_properties ();
724         }
725     }
726
727     private void callback_select_video_in_tree_view (Gtk.TreeView tree_view,
728         Gtk.TreePath path,
729         Gtk.TreeViewColumn column)
730     {
731         var model = tree_view.get_model ();
732
733         Gtk.TreeIter iter;
734         Video v;
735
736         model.get_iter (out iter, path);
737         model.get (iter, Col.VIDEO_OBJECT, out v);
738
739         string uri = null;
740         try {
741             uri = v.get_stream_uri (quality, language);
742         } catch (ExtractionError e) {
743             if(e is ExtractionError.STREAM_NOT_READY) {
744                 /* The video is part of the XML/RSS feed but no stream is available yet */
745                 t.action_error (_("This video is not available yet"),
746                         _("Sorry, the plugin could not find any stream URL.\nIt seems that this video is not available yet, even on the Arte web-player.\n\nPlease retry later."));
747             } else if (e is ExtractionError.DOWNLOAD_FAILED) {
748                 /* Network problems */
749                 t.action_error (_("Video URL Extraction Error"),
750                         _("Sorry, the plugin could not extract a valid stream URL.\nPlease verify your network settings and (if any) your proxy settings."));
751             } else {
752                 /* ExtractionError.EXTRACTION_ERROR or an unspecified error */
753                 t.action_error (_("Video URL Extraction Error"),
754                         _("Sorry, the plugin could not extract a valid stream URL.\nPerhaps this stream is not yet available, you may retry in a few minutes.\n\nBe aware that this service is only available for IPs within Austria, Belgium, Germany, France and Switzerland."));
755             }
756             return;
757         }
758
759         t.add_to_playlist_and_play (uri, v.title, false);
760     }
761
762     private void callback_refresh_rss_feed (Gtk.Widget widget)
763     {
764         use_fallback_feed = false;
765         GLib.Idle.add (refresh_rss_feed);
766     }
767
768     private void callback_language_changed (Gtk.ComboBox box)
769     {
770         Language last = language;
771         string text = box.get_active_text ();
772         if (text == _("German")) {
773             language = Language.GERMAN;
774         } else {
775             language = Language.FRENCH;
776         }
777         if (last != language) {
778             GLib.Idle.add (refresh_rss_feed);
779             store_properties ();
780         }
781     }
782
783     private void callback_quality_toggled (Gtk.ToggleButton button)
784     {
785         VideoQuality last = quality;
786         bool mq_active = button.get_active ();
787         if (mq_active) {
788             quality = VideoQuality.WMV_MQ;
789         } else {
790             quality = VideoQuality.WMV_HQ;
791         }
792         if (last != quality) {
793             store_properties ();
794         }
795     }
796
797     private bool callback_F5_pressed (Gtk.Widget widget, Gdk.EventKey event)
798     {
799         string key = Gdk.keyval_name (event.keyval);
800         if (key == "F5")
801         {
802             callback_refresh_rss_feed (widget);
803         }
804
805         /* propagate the signal to the next handler */
806         return false;
807     }
808 }
809
810 [ModuleInit]
811 public GLib.Type register_totem_plugin (GLib.TypeModule module)
812 {
813     return typeof (ArtePlugin);
814 }