[rygel] mediathek: Make item creation async; fixes bgo#638269



commit 1de8e79520526aa02a1f116e763d45352748a69e
Author: Jens Georg <mail jensge org>
Date:   Sat Jan 15 19:41:16 2011 +0100

    mediathek: Make item creation async; fixes bgo#638269

 src/plugins/mediathek/Makefile.am                  |    7 +-
 .../rygel-mediathek-asx-playlist-parser.vala       |  107 +++++++++++++
 .../mediathek/rygel-mediathek-asx-playlist.vala    |  106 -------------
 .../mediathek/rygel-mediathek-root-container.vala  |   32 +++--
 .../mediathek/rygel-mediathek-rss-container.vala   |  157 +++++++++++---------
 .../mediathek/rygel-mediathek-soup-utils.vala      |   31 ++++
 .../rygel-mediathek-video-item-factory.vala        |  124 +++++++++++++++
 .../mediathek/rygel-mediathek-video-item.vala      |  127 ----------------
 8 files changed, 370 insertions(+), 321 deletions(-)
---
diff --git a/src/plugins/mediathek/Makefile.am b/src/plugins/mediathek/Makefile.am
index 544c57e..93a1478 100644
--- a/src/plugins/mediathek/Makefile.am
+++ b/src/plugins/mediathek/Makefile.am
@@ -18,11 +18,12 @@ AM_CFLAGS = $(LIBGUPNP_CFLAGS) \
 	    -DDATA_DIR='"$(shareddir)"' \
 	    -include config.h
 
-librygel_mediathek_la_SOURCES = rygel-mediathek-asx-playlist.vala \
+librygel_mediathek_la_SOURCES = rygel-mediathek-asx-playlist-parser.vala \
 				rygel-mediathek-plugin.vala \
-				rygel-mediathek-video-item.vala \
+				rygel-mediathek-video-item-factory.vala \
 				rygel-mediathek-root-container.vala \
-				rygel-mediathek-rss-container.vala
+				rygel-mediathek-rss-container.vala \
+				rygel-mediathek-soup-utils.vala
 
 librygel_mediathek_la_VALAFLAGS = --vapidir=$(top_srcdir)/src/rygel \
 				  --pkg rygel-1.0 \
diff --git a/src/plugins/mediathek/rygel-mediathek-asx-playlist-parser.vala b/src/plugins/mediathek/rygel-mediathek-asx-playlist-parser.vala
new file mode 100644
index 0000000..14f5fbd
--- /dev/null
+++ b/src/plugins/mediathek/rygel-mediathek-asx-playlist-parser.vala
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2009-2011 Jens Georg
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Gee;
+using Soup;
+using Xml;
+
+/**
+ * This class is a simple ASX playlist parser
+ * 
+ * It does nothing but extracting all href tags from an ASX
+ * and ignore all of the other information that may be in it
+ * 
+ * This parser is //only// intended to work with the simple 
+ * ASX files presented by the ZDF Mediathek streaming server
+ */
+internal class Rygel.Mediathek.AsxPlaylistParser : Object {
+    private Regex normalizer;
+    private Session session;
+
+    public AsxPlaylistParser (Session session) {
+        try {
+            this.normalizer = new Regex ("(<[/]?)([a-zA-Z:]+)");
+        } catch (RegexError error) {};
+        this.session = session;
+    }
+
+    /** 
+     * Get and parse the ASX file.
+     *
+     * This will fetch the ASX file using the soup session configured on
+     * configure time.As ASX seems to be a bit inconsistent with regard to tag
+     * case, all the tags are converted to lowercase. A XPath query is then used
+     * to extract all of the href attributes for every entry in the file.
+     *
+     * @param uri network location of the ASX file
+     * @return a list of uris found in this file
+     */
+    public async Gee.List<string>? parse (string uri) throws VideoItemError {
+        var message = new Soup.Message ("GET", uri);
+
+        yield SoupUtils.queue_message (session, message);
+
+        if (message.status_code != 200) {
+            throw new VideoItemError.NETWORK_ERROR
+                                        ("Playlist download failed: %u (%s)",
+                                         message.status_code,
+                                         Soup.status_get_phrase
+                                                      (message.status_code));
+        }
+
+        try {
+            // lowercase all tags using regex and \L\E syntax
+            var normalized_content = this.normalizer.replace
+                                        ((string) message.response_body.data,
+                                         (long) message.response_body.length,
+                                         0, 
+                                         "\\1\\L\\2\\E");
+
+            var doc = Parser.parse_memory (normalized_content, 
+                                           (int) normalized_content.length);
+            if (doc == null) {
+                throw new VideoItemError.XML_PARSE_ERROR
+                                        ("Could not parse playlist");
+            }
+
+            var context = new XPath.Context (doc);
+            var xpath_object = context.eval ("/asx/entry/ref/@href");
+            if (xpath_object->type == XPath.ObjectType.NODESET) {
+                var uris = new LinkedList<string> ();
+                for (int i = 0;
+                     i < xpath_object->nodesetval->length ();
+                     i++) {
+                    var item = xpath_object->nodesetval->item (i);
+                    uris.add (item->children->content);
+                }
+
+                return uris;
+            }
+
+            delete doc;
+        } catch (RegexError error) {
+            throw new VideoItemError.XML_PARSE_ERROR ("Failed to normalize");
+        }
+
+        return null;
+    }
+}
diff --git a/src/plugins/mediathek/rygel-mediathek-root-container.vala b/src/plugins/mediathek/rygel-mediathek-root-container.vala
index 7b33d3d..13bdfde 100644
--- a/src/plugins/mediathek/rygel-mediathek-root-container.vala
+++ b/src/plugins/mediathek/rygel-mediathek-root-container.vala
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 Jens Georg
+ * Copyright (C) 2009-2011 Jens Georg
  *
  * Author: Jens Georg <mail jensge org>
  *
@@ -24,29 +24,29 @@ using Gee;
 using Soup;
 
 public class Rygel.Mediathek.RootContainer : Rygel.SimpleContainer {
-    internal SessionAsync session;
+    private SessionAsync session;
     private static RootContainer instance;
-
-    private bool on_schedule_update () {
-        message("Scheduling update for all feeds....");
-        foreach (var container in this.children) {
-            ((RssContainer) container).update ();
-        }
-
-        return true;
-    }
+    private static uint UPDATE_TIMEOUT = 1800;
 
     public static RootContainer get_instance () {
         if (RootContainer.instance == null) {
             RootContainer.instance = new RootContainer ();
+            RootContainer.instance.init ();
         }
 
         return instance;
     }
 
+    public static SessionAsync get_default_session () {
+        return get_instance ().session;
+    }
+
     private RootContainer () {
         base.root ("ZDF Mediathek");
         this.session = new Soup.SessionAsync ();
+    }
+
+    private void init () {
         Gee.ArrayList<int> feeds = null;
 
         var config = Rygel.MetaConfig.get_default ();
@@ -65,6 +65,14 @@ public class Rygel.Mediathek.RootContainer : Rygel.SimpleContainer {
             this.add_child_container (new RssContainer (this, id));
         }
 
-        GLib.Timeout.add_seconds (1800, on_schedule_update);
+        Timeout.add_seconds (UPDATE_TIMEOUT, () => {
+            foreach (var child in this.children) {
+                var container = child as RssContainer;
+
+                container.update ();
+            }
+
+            return true;
+        });
     }
 }
diff --git a/src/plugins/mediathek/rygel-mediathek-rss-container.vala b/src/plugins/mediathek/rygel-mediathek-rss-container.vala
index 983a356..6d19771 100644
--- a/src/plugins/mediathek/rygel-mediathek-rss-container.vala
+++ b/src/plugins/mediathek/rygel-mediathek-rss-container.vala
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 Jens Georg
+ * Copyright (C) 2009-2011 Jens Georg
  *
  * Author: Jens Georg <mail jensge org>
  *
@@ -25,95 +25,106 @@ using Soup;
 using Xml;
 
 public class Rygel.Mediathek.RssContainer : Rygel.SimpleContainer {
-    private uint zdf_content_id;
+    private const string uri_template = "http://www.zdf.de/ZDFmediathek/"; +
+                                        "content/%u?view=rss";
+    private uint content_id;
     private Soup.Date last_modified = null;
+    private string feed_uri;
 
-    private void on_feed_got (Soup.Session session, Soup.Message msg) {
-        switch (msg.status_code) {
-            case 304:
-                message("Feed has not changed, nothing to do");
-                break;
-            case 200:
-                if (parse_response ((string) msg.response_body.data,
-                                    (size_t) msg.response_body.length)) {
-                    last_modified = new Soup.Date.from_string (
-                                        msg.response_headers.get_one ("Date"));
-                }
-                break;
-            default:
-                // TODO Need to handle redirects....
-                warning("Got unexpected response %u (%s)",
-                        msg.status_code,
-                        Soup.status_get_phrase (msg.status_code));
-                break;
+    private async bool parse_response (Message message) {
+        var factory = VideoItemFactory.get_default ();
+        unowned MessageBody response = message.response_body;
+
+        var doc = Xml.Parser.parse_memory ((string) response.data,
+                                           (int) response.length);
+        if (doc == null) {
+            warning ("Failed to parse XML document");
+
+            return false;
         }
-    }
 
-    private bool parse_response (string data, size_t length) {
-        bool ret = false;
-        Xml.Doc* doc = Xml.Parser.parse_memory (data, (int) length);
-        if (doc != null) {
-            this.children.clear ();
-            this.child_count = 0;
-
-            var ctx = new XPath.Context (doc);
-            var xpo = ctx.eval ("/rss/channel/title");
-            if (xpo->type == Xml.XPath.ObjectType.NODESET &&
-                xpo->nodesetval->length () > 0) {
-                // just use first title (there should be only one)
-                this.title = xpo->nodesetval->item (0)->get_content ();
-            }
 
-            xpo = ctx.eval ("/rss/channel/item");
-            if (xpo->type == Xml.XPath.ObjectType.NODESET) {
-                for (int i = 0; i < xpo->nodesetval->length (); i++) {
-                    Xml.Node* node = xpo->nodesetval->item (i);
-                    try {
-                        var item = VideoItem.create_from_xml (this, node);
-                        this.add_child_item (item);
-                        ret = true;
-                    }
-                    catch (VideoItemError error) {
-                        warning ("Error creating video item: %s",
-                                 error.message);
-                    }
-                }
-            }
-            else {
-                warning ("XPath query failed");
-            }
+        var context = new XPath.Context (doc);
+        var xpo = context.eval ("/rss/channel/title");
+        if (xpo->type == XPath.ObjectType.NODESET &&
+            xpo->nodesetval->length () > 0) {
+            // just use first title (there should be only one)
+            this.title = xpo->nodesetval->item (0)->get_content ();
+        }
 
-            delete doc;
-            this.updated ();
+        xpo = context.eval ("/rss/channel/item");
+        if (xpo->type != XPath.ObjectType.NODESET) {
+            warning ("RSS feed doesn't have items");
+
+            return false;
         }
-        else {
-            warning ("Failed to parse doc");
+
+        this.children.clear ();
+        this.child_count = 0;
+        for (int i = 0; i < xpo->nodesetval->length (); i++) {
+            var node = xpo->nodesetval->item (i);
+            try {
+                var item = yield factory.create (this, node);
+                if (item != null) {
+                    this.add_child_item (item);
+                }
+            } catch (VideoItemError error) {
+                warning ("Error creating video item: %s",
+                         error.message);
+            }
         }
 
-        return ret;
+        this.updated ();
+
+        return this.child_count > 0;
     }
 
-    public void update () {
-        var message = new Soup.Message ("GET",
-            "http://www.zdf.de/ZDFmediathek/content/%u?view=rss".printf(
-                                                            zdf_content_id)); 
-        if (last_modified != null) {
-            debug ("Requesting change since %s",
-                   last_modified.to_string(DateFormat.HTTP));
-            message.request_headers.append("If-Modified-Since", 
-                   last_modified.to_string(DateFormat.HTTP));
+    private Message get_update_message () {
+        var message = new Soup.Message ("GET", this.feed_uri);
+        if (this.last_modified != null) {
+            var datestring = this.last_modified.to_string (DateFormat.HTTP);
+
+            debug ("Requesting change since %s", datestring);
+            message.request_headers.append("If-Modified-Since", datestring);
         }
 
-        ((RootContainer) this.parent).session.queue_message (message,
-                                                             on_feed_got);
+        return message;
+    }
+
+    public async void update () {
+        var message = this.get_update_message ();
+        yield SoupUtils.queue_message (RootContainer.get_default_session (),
+                                       message);
+
+        switch (message.status_code) {
+            case 304:
+                debug ("Feed at %s did not change, nothing to do.",
+                       message.uri.to_string (false));
+                break;
+            case 200:
+                var success = yield this.parse_response (message);
+                if (success) {
+                    var date = message.response_headers.get_one ("Date");
+
+                    this.last_modified = new Soup.Date.from_string (date);
+                }
+                break;
+            default:
+                warning ("Unexpected response %u for %s: %s",
+                         message.status_code,
+                         message.uri.to_string (false),
+                         Soup.status_get_phrase (message.status_code));
+                break;
+        }
     }
 
     public RssContainer (MediaContainer parent, uint id) {
         base ("GroupId:%u".printf(id),
-             parent, 
-             "ZDF Mediathek RSS feed %u".printf (id));
+              parent, 
+              "ZDF Mediathek RSS feed %u".printf (id));
 
-        this.zdf_content_id = id;
-        update ();
+        this.content_id = id;
+        this.feed_uri = uri_template.printf (id);
+        this.update ();
     }
 }
diff --git a/src/plugins/mediathek/rygel-mediathek-soup-utils.vala b/src/plugins/mediathek/rygel-mediathek-soup-utils.vala
new file mode 100644
index 0000000..08ad438
--- /dev/null
+++ b/src/plugins/mediathek/rygel-mediathek-soup-utils.vala
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 Jens Georg
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+internal class Rygel.Mediathek.SoupUtils : Object {
+    public static async void queue_message (Soup.Session session,
+                                            Soup.Message message) {
+        SourceFunc asyc_callback  = queue_message.callback;
+
+        session.queue_message (message, () => { asyc_callback (); });
+        yield;
+    }
+}
diff --git a/src/plugins/mediathek/rygel-mediathek-video-item-factory.vala b/src/plugins/mediathek/rygel-mediathek-video-item-factory.vala
new file mode 100644
index 0000000..d5e4a73
--- /dev/null
+++ b/src/plugins/mediathek/rygel-mediathek-video-item-factory.vala
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2011 Jens Georg
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+internal errordomain Rygel.Mediathek.VideoItemError {
+    XML_PARSE_ERROR,
+    NETWORK_ERROR
+}
+
+internal class Rygel.Mediathek.VideoItemFactory : Object {
+    private static VideoItemFactory instance;
+    private AsxPlaylistParser playlist_parser;
+
+    public static VideoItemFactory get_default () {
+        if (instance == null) {
+            instance = new VideoItemFactory ();
+        }
+
+        return instance;
+    }
+
+    public async VideoItem? create (MediaContainer parent,
+                                    Xml.Node      *xml_item)
+                                    throws VideoItemError {
+        string title;
+        string playlist_url;
+        this.extract_data_from_xml (xml_item,
+                                    out title,
+                                    out playlist_url);
+
+        var resolved_uris = yield playlist_parser.parse (playlist_url);
+
+        if (resolved_uris == null || resolved_uris.size == 0) {
+            return null;
+        }
+
+        var id = Checksum.compute_for_string (ChecksumType.MD5, title);
+        var item = new VideoItem (id, parent, title);
+
+        item.mime_type = "video/x-ms-wmv";
+        item.author = "ZDF - Second German TV Channel Streams";
+
+        foreach (var uri in resolved_uris) {
+            item.add_uri (uri);
+        }
+
+        return item;
+    }
+
+    private VideoItemFactory () {
+        playlist_parser = new AsxPlaylistParser
+                                        (RootContainer.get_default_session ());
+    }
+
+    private bool namespace_ok (Xml.Node* node) {
+        return node->ns != null && node->ns->prefix == "media";
+    }
+
+    private void extract_data_from_xml (Xml.Node   *item,
+                                        out string  title,
+                                        out string  playlist_url)
+                                        throws VideoItemError {
+        var title_node = XMLUtils.get_element (item, "title");
+        var group = XMLUtils.get_element (item, "group");
+
+        if (title_node == null) {
+            throw new VideoItemError.XML_PARSE_ERROR ("No 'title' element");
+        }
+
+        if (group == null) {
+            throw new VideoItemError.XML_PARSE_ERROR ("No 'group' element");
+        }
+
+        if (!namespace_ok (group)) {
+            throw new VideoItemError.XML_PARSE_ERROR ("Invalid namespace");
+        }
+
+        var content = XMLUtils.get_element (group, "content");
+        if (content == null) {
+            throw new VideoItemError.XML_PARSE_ERROR
+                                        ("'group' has no 'content' element");
+        }
+
+        // content points to the first content subnode now
+        while (content != null) {
+            var url_attribute = content->has_prop ("url");
+            if (url_attribute != null && namespace_ok (content)) {
+                
+                unowned string url = url_attribute->children->content;
+                if (url.has_suffix (".asx")) {
+                    playlist_url = url;
+
+                    break;
+                }
+
+            }
+            content = content->next;
+        }
+
+        if (playlist_url == null) {
+            throw new VideoItemError.XML_PARSE_ERROR ("No URL found");
+        }
+
+        title = title_node->get_content ();
+    }
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]