Author: tack
Date: Mon Oct 17 02:29:37 2005
New Revision: 874

Added:
   trunk/WIP/epg2/
   trunk/WIP/epg2/doc/
   trunk/WIP/epg2/doc/README
   trunk/WIP/epg2/doc/TODO
   trunk/WIP/epg2/setup.py
   trunk/WIP/epg2/src/
   trunk/WIP/epg2/src/__init__.py
   trunk/WIP/epg2/src/channel.py
   trunk/WIP/epg2/src/client.py
   trunk/WIP/epg2/src/program.py
   trunk/WIP/epg2/src/server.py
   trunk/WIP/epg2/src/source_xmltv.py
   trunk/WIP/epg2/src/source_zap2it.py
   trunk/WIP/epg2/test/
   trunk/WIP/epg2/test/epgclient.py
   trunk/WIP/epg2/test/epgserver.py

Log:
Initial import of epg rewrite


Added: trunk/WIP/epg2/doc/README
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/doc/README   Mon Oct 17 02:29:37 2005
@@ -0,0 +1,51 @@
+This is my WIP rewrite of kaa.epg.  It uses kaa.base.db for storage, so it
+benefits from the keyword support in kaa.base.db.  Here's a breakdown of
+improvements and other differences:
+
+  * Split into client/server (GuideClient/GuideServer) and uses
+    kaa.base.ipc for communication.  
+  * Supports fast keyword search with relevancy sorting (search 30000
+    programs in 0.01 seconds)
+  * Use libxml2 for xml parsing.  Updating from a 17MB xmltv file takes
+    95 seconds instead of 74 minutes.  Seriously.
+  * Implement zap2it datadirect backend. North America users no longer need
+    xmltv.
+  * The API is a bit different.  It avoids __getitem__ which is a clever
+    interface but perhaps a bit too clever.
+  * Updating from backend is asynchronous.  Downloading / parsing is done
+    in a thread, and the database is updated in steps within the main loop.
+  * GuideClient can used in the same process as GuideServer and will avoid
+    the unneeded IPC calls.
+
+The main interface is via the get_channel() and search() methods in
+GuideClient.  get_channel() returns a Channel object for the given channel
+number.  search() returns a list of Program objects that match the query
+given.  Some examples:
+
+
+    # Connect to the guide server (unix socket 'epg')
+    guide = epg2.GuideClient("epg")
+
+    # get Channel object for channe 515
+    ch = guide.get_channel(515)
+
+    # get Program objects playing at the current time (list of 1) for
+    # channel 515
+    program  = ch.get_programs()[0]
+
+    now = time.time()
+
+    # Get Program objects for channel 515 playing in the next 3 hours
+    programs = ch.get_programs((now, now + 3*60*60))
+
+    # Get all programs in the next 12 hours with keywords 'Simpsons'
+    programs = guide.search(keywords = "Simpsons", 
+                            time = (now, now + 12*60*60))
+
+    # Get all programs in the next 3 hours between channels 30 and 40.
+    programs = guide.search(time = (now, now + 3*60*60), channels = 
+                            (guide.get_channel(30), guide.get_channel(40))
+    
+
+See test/ for examples.
+

Added: trunk/WIP/epg2/doc/TODO
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/doc/TODO     Mon Oct 17 02:29:37 2005
@@ -0,0 +1,7 @@
+- Import more data from sources (like ratings, advisories, actor names, etc.)
+- Merge program data on update, instead of wiping existing data.
+- Assign ids to unique programs regardless of air times (this is already
+  given by zap2it, but not sure about other listing providers -- may need to
+  code custom heuristics).
+- Finish API :)
+

Added: trunk/WIP/epg2/setup.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/setup.py     Mon Oct 17 02:29:37 2005
@@ -0,0 +1,45 @@
+# -*- coding: iso-8859-1 -*-
+# -----------------------------------------------------------------------------
+# setup.py - setup script for kaa.epg2
+# -----------------------------------------------------------------------------
+# $Id: setup.py 465 2005-07-07 13:37:56Z dischi $
+#
+# -----------------------------------------------------------------------------
+# kaa-epg - Python EPG module (take 2)
+# Copyright (C) 2005 Dirk Meyer, Rob Shortt, Jason Tackaberry, et al.
+#
+# First Edition: Jason Tackaberry <[EMAIL PROTECTED]>
+# Maintainer:    Dirk Meyer <[EMAIL PROTECTED]>
+#                Jason Tackaberry <[EMAIL PROTECTED]>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MER-
+# CHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# -----------------------------------------------------------------------------
+
+# python imports
+import sys
+
+try:
+    # kaa base imports
+    from kaa.base.distribution import Extension, setup
+except ImportError:
+    print 'kaa.base not installed'
+    sys.exit(1)
+    
+
+setup(module       = 'epg2',
+      version      = '0.1',
+      description  = "Python EPG module",
+      )

Added: trunk/WIP/epg2/src/__init__.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/src/__init__.py      Mon Oct 17 02:29:37 2005
@@ -0,0 +1,2 @@
+from client import *
+from server import *

Added: trunk/WIP/epg2/src/channel.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/src/channel.py       Mon Oct 17 02:29:37 2005
@@ -0,0 +1,21 @@
+from kaa.base.utils import str_to_unicode
+import weakref, time
+
+class Channel(object):
+    def __init__(self, channel, station, name, epg):
+        self.id = None
+        self.channel = channel
+        self.station = station
+        self.name = name
+
+        self._epg = weakref.ref(epg)
+
+    def get_epg(self):
+        return self._epg()
+
+    def get_programs(self, t = None):
+        if not t:
+            t = time.time()
+
+        return self.get_epg().search(time = t, channel = self)
+

Added: trunk/WIP/epg2/src/client.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/src/client.py        Mon Oct 17 02:29:37 2005
@@ -0,0 +1,116 @@
+import libxml2, sys, time, os, weakref, cPickle
+from kaa.base import ipc, db
+from kaa.notifier import Signal
+from server import *
+from channel import *
+from program import *
+
+__all__ = ['GuideClient']
+
+
+class GuideClient(object):
+    def __init__(self, server_or_socket, auth_secret = None):
+        if type(server_or_socket) == GuideServer:
+            self._server = server_or_socket
+            self._ipc = None
+        else:
+            self._ipc = ipc.IPCClient(server_or_socket, auth_secret = 
auth_secret)
+            self._server = self._ipc.get_object("guide")
+
+        self.signals = {
+            "updated": Signal(),
+            "update_progress": Signal()
+        }
+    
+        self._load()
+        self._server.signals["updated"].connect(self._updated)
+        
self._server.signals["update_progress"].connect(self.signals["update_progress"].emit)
+
+    def _updated(self):
+        self._load()
+        self.signals["updated"].emit()
+
+
+    def _load(self):
+        self._channels_by_number = {}
+        self._channels_by_id = {}
+        self._channels_list = []
+        data = self._server.query(type="channel", __ipc_copy_result = True)
+        for row in db.iter_raw_data(data, ("id", "channel", "station", 
"name")):
+            id, channel, station, name = row
+            chan = Channel(channel, station, name, self)
+            chan.id = id
+            self._channels_by_number[channel] = chan
+            self._channels_by_id[id] = chan
+            self._channels_list.append(chan)
+
+        self._max_program_length = self._server.get_max_program_length()
+        self._num_programs = self._server.get_num_programs()
+
+
+    def _program_rows_to_objects(self, query_data):
+        cols = "parent_id", "start", "stop", "title", "desc"#, "ratings"
+        results = []
+        for row in db.iter_raw_data(query_data, cols):
+            if row[0] not in self._channels_by_id:
+                continue
+            channel = self._channels_by_id[row[0]]
+            program = Program(channel, row[1], row[2], row[3], row[4])
+            results.append(program)
+        return results
+
+
+    def search(self, **kwargs):
+        if "channel" in kwargs:
+            ch = kwargs["channel"]
+            if type(ch) == Channel:
+                kwargs["channel"] = ch.id
+            elif type(ch) == tuple and len(ch) == 2:
+                kwargs["channel"] = db.QExpr("range", (ch[0].id, ch[1].id))
+            else:
+                raise ValueError, "channel must be Channel object or tuple of 
2 Channel objects"
+
+        if "time" in kwargs:
+            if type(kwargs["time"]) in (int, float, long):
+                # Find all programs currently playing at the given time.  We
+                # add 1 second as a heuristic to prevent duplicates if the
+                # given time occurs on a boundary between 2 programs.
+                start, stop = kwargs["time"] + 1, kwargs["time"] + 1
+            else:
+                start, stop = kwargs["time"]
+
+            max = self.get_max_program_length()
+            kwargs["start"] = db.QExpr("range", (int(start) - max, int(stop)))
+            kwargs["stop"] = db.QExpr(">=", int(start))
+            del kwargs["time"]
+
+        kwargs["type"] = "program"
+        data = self._server.query(__ipc_copy_result = True, **kwargs)
+        if not data[1]:
+            return []
+        return self._program_rows_to_objects(data)
+
+
+    def get_channel(self, key):
+        if key not in self._channels_by_number:
+            return None
+        return self._channels_by_number[key]
+
+    def get_channel_by_id(self, id):
+        if id not in self._channels_by_id:
+            return None
+        return self._channels_by_id[id]
+
+    def get_max_program_length(self):
+        return self._max_program_length
+
+    def get_num_programs(self):
+        return self._num_programs
+
+    def get_channels(self):
+        return self._channels_list
+
+    def update(self, *args, **kwargs):
+        # updated signal will fire when this call completes.
+        kwargs["__ipc_oneway"] = True
+        self._server.update(*args, **kwargs)

Added: trunk/WIP/epg2/src/program.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/src/program.py       Mon Oct 17 02:29:37 2005
@@ -0,0 +1,11 @@
+from kaa.base.utils import str_to_unicode
+from channel import *
+
+class Program(object):
+    def __init__(self, channel, start, stop, title, desc):
+        assert(type(channel) == Channel)
+        self.channel = channel
+        self.start = start
+        self.stop = stop
+        self.title = title
+        self.desc = desc

Added: trunk/WIP/epg2/src/server.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/src/server.py        Mon Oct 17 02:29:37 2005
@@ -0,0 +1,143 @@
+import libxml2, sys, time, os, weakref
+from kaa.base.db import *
+from kaa.base import ipc
+from kaa.notifier import Signal
+
+__all__ = ['GuideServer']
+
+# TODO: merge updates when processing instead of wipe.
+
+class GuideServer(object):
+    def __init__(self, socket, dbfile = None, auth_secret = None):
+        if not dbfile:
+            dbfile = "epgdb.sqlite"
+
+        db = Database(dbfile)
+        db.register_object_type_attrs("channel",
+            name = (unicode, ATTR_SEARCHABLE),
+            station = (unicode, ATTR_SEARCHABLE),
+            channel = (int, ATTR_SEARCHABLE),
+            channel_id = (unicode, ATTR_SIMPLE)
+        )
+        db.register_object_type_attrs("program", 
+            [ ("start", "stop") ],
+            title = (unicode, ATTR_KEYWORDS),
+            desc = (unicode, ATTR_KEYWORDS),
+            date = (int, ATTR_SEARCHABLE),
+            start = (int, ATTR_SEARCHABLE),
+            stop = (int, ATTR_SEARCHABLE),
+            ratings = (dict, ATTR_SIMPLE)
+        )
+
+        self.signals = {
+            "updated": Signal(),
+            "update_progress": Signal()
+        }
+
+        self._db = db
+        self._load()
+        
+        self._ipc = ipc.IPCServer(socket, auth_secret = auth_secret)
+        self._ipc.signals["client_closed"].connect_weak(self._client_closed)
+        self._ipc.register_object(self, "guide")
+
+    def _load(self):
+        self._max_program_length = self._num_programs = 0
+        q = "SELECT stop-start AS length FROM objects_program ORDER BY length 
DESC LIMIT 1"
+        res = self.get_db()._db_query(q)
+        if len(res):
+            self._max_program_length = res[0][0]
+
+        res = self.get_db()._db_query("SELECT count(*) FROM objects_program")
+        if len(res):
+            self._num_programs = res[0][0]
+
+
+    def _client_closed(self, client):
+        for signal in self.signals.values():
+            for callback in signal:
+                if ipc.get_ipc_from_proxy(callback) == client:
+                    signal.disconnect(callback)
+
+
+    def update(self, backend, *args, **kwargs):
+        try:
+            exec('import source_%s as backend' % backend)
+        except ImportError:
+            raise ValueError, "No such update backend '%s'" % backend
+
+        self._wipe()
+        self.signals["update_progress"].connect_weak(self._update_progress, 
time.time())
+        backend.update(self, *args, **kwargs)
+
+
+    def _update_progress(self, cur, total, update_start_time):
+        if total <= 0:
+            # Processing something, but don't yet know how much
+            n = 0
+        else:
+            n = int((cur / float(total)) * 50)
+
+        # Temporary: output progress status to stdout.
+        sys.stdout.write("|%51s| %d / %d\r" % (("="*n + ">").ljust(51), cur, 
total))
+        sys.stdout.flush()
+
+        if cur == total:
+            self._db.commit()
+            self.signals["updated"].emit()
+            self.signals["update_progress"].disconnect(self._update_progress)
+            print "\nProcessed in %.02f seconds." % 
(time.time()-update_start_time)
+
+
+    def _wipe(self):
+        t0=time.time()
+        self._db.delete_by_query()
+        self._channel_id_to_db_id = {}
+
+
+    def _add_channel_to_db(self, id, channel, station, name):
+        o = self._db.add_object("channel", 
+                                channel = channel,
+                                station = station,
+                                name = name,
+                                channel_id = id)
+        return o["id"]
+
+
+    def _add_program_to_db(self, channel_id, start, stop, title, desc):
+        o = self._db.add_object("program", 
+                                parent = ("channel", channel_id),
+                                start = start,
+                                stop = stop, 
+                                title = title, 
+                                desc = desc, ratings = 42)
+        if stop - start > self._max_program_length:
+            self._max_program_length = stop = start
+        return o["id"]
+
+
+    def query(self, **kwargs):
+        if "channel" in kwargs:
+            if type(kwargs["channel"]) in (list, tuple):
+                kwargs["parent"] = [("channel", x) for x in kwargs["channel"]]
+            else:
+                kwargs["parent"] = "channel", kwargs["channel"]
+            del kwargs["channel"]
+
+        for key in kwargs.copy():
+            if key.startswith("__ipc_"):
+                del kwargs[key]
+
+        res = self._db.query_raw(**kwargs)
+        return res
+
+
+    def get_db(self):
+        return self._db
+
+
+    def get_max_program_length(self):
+        return self._max_program_length
+
+    def get_num_programs(self):
+        return self._num_programs

Added: trunk/WIP/epg2/src/source_xmltv.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/src/source_xmltv.py  Mon Oct 17 02:29:37 2005
@@ -0,0 +1,162 @@
+import libxml2, sys, time, os, calendar
+from kaa.base.utils import str_to_unicode
+import kaa.notifier
+
+def timestr2secs_utc(timestr):
+    """
+    Convert a timestring to UTC (=GMT) seconds.
+
+    The format is either one of these two:
+    '20020702100000 CDT'
+    '200209080000 +0100'
+    """
+    # This is either something like 'EDT', or '+1'
+    try:
+        tval, tz = timestr.split()
+    except ValueError:
+        tval = timestr
+        tz   = str(-time.timezone/3600)
+
+    if tz == 'CET':
+        tz='+1'
+
+    # Is it the '+1' format?
+    if tz[0] == '+' or tz[0] == '-':
+        tmTuple = ( int(tval[0:4]), int(tval[4:6]), int(tval[6:8]),
+                    int(tval[8:10]), int(tval[10:12]), 0, -1, -1, -1 )
+        secs = calendar.timegm( tmTuple )
+
+        adj_neg = int(tz) >= 0
+        try:
+            min = int(tz[3:5])
+        except ValueError:
+            # sometimes the mins are missing :-(
+            min = 0
+        adj_secs = int(tz[1:3])*3600+ min*60
+
+        if adj_neg:
+            secs -= adj_secs
+        else:
+            secs += adj_secs
+    else:
+        # No, use the regular conversion
+
+        ## WARNING! BUG HERE!
+        # The line below is incorrect; the strptime.strptime function doesn't
+        # handle time zones. There is no obvious function that does. Therefore
+        # this bug is left in for someone else to solve.
+
+        try:
+            secs = time.mktime(strptime.strptime(timestr, xmltv.date_format))
+        except ValueError:
+            timestr = timestr.replace('EST', '')
+            secs = time.mktime(strptime.strptime(timestr, xmltv.date_format))
+    return secs
+
+
+
+def parse_channel(epg, channel_id_to_db_id, node):
+    channel_id = str_to_unicode(node.prop("id"))
+    channel = station = name = None
+
+    child = node.children
+    while child:
+        # This logic expects that the first display-name that appears
+        # after an all-numeric and an all-alpha display-name is going
+        # to be the descriptive station name.  XXX: check if this holds
+        # for all xmltv source.
+        if child.name == "display-name":
+            if not channel and child.content.isdigit():
+                channel = int(child.content)
+            elif not station and child.content.isalpha():
+                station = str_to_unicode(child.content)
+            elif channel and station and not name:
+                name = str_to_unicode(child.content)
+        child = child.get_next()
+
+    id = epg._add_channel_to_db(channel_id, channel, station, name)
+    channel_id_to_db_id[channel_id] = id
+
+
+def parse_programme(epg, channel_id_to_db_id, node):
+    channel_id = str_to_unicode(node.prop("channel"))
+    if channel_id not in channel_id_to_db_id:
+        print "UNKNOWN CHANNEL", channel_id
+        return
+
+    start = timestr2secs_utc(node.prop("start"))
+    stop = timestr2secs_utc(node.prop("stop"))
+    title = date = desc = None
+
+    child = node.children
+    while child:
+        if child.name == "title":
+            title = str_to_unicode(child.content)
+        elif child.name == "desc":
+            desc = str_to_unicode(child.content)
+        elif child.name == "date":
+            fmt = "%Y%m%d"
+            if len(child.content) == 4:
+                fmt = "%Y"
+            date = time.mktime(time.strptime(child.content, fmt))
+        child = child.get_next()
+
+    if not title:
+        return
+
+    epg._add_program_to_db(channel_id_to_db_id[channel_id], start, stop, 
title, desc)
+ 
+
+class UpdateInfo:
+    pass
+
+def _update_parse_xml_thread(epg, xmltv_file):
+    doc = libxml2.parseFile(xmltv_file)
+    channel_id_to_db_id = {}
+    nprograms = 0
+    child = doc.children.get_next().children
+    while child:
+        if child.name == "programme":
+            nprograms += 1
+        child = child.get_next()
+
+    info = UpdateInfo()
+    info.doc = doc
+    info.node = doc.children.get_next().children
+    info.channel_id_to_db_id = channel_id_to_db_id
+    info.total = nprograms
+    info.cur = 0
+    info.epg = epg
+    info.progress_step = info.total / 100
+
+    timer = kaa.notifier.Timer(_update_process_step, info)
+    timer.set_prevent_recursion()
+    timer.start(0)
+    
+
+def _update_process_step(info):
+    t0 = time.time()
+    while info.node:
+        if info.node.name == "channel":
+            parse_channel(info.epg, info.channel_id_to_db_id, info.node)
+        elif info.node.name == "programme":
+            parse_programme(info.epg, info.channel_id_to_db_id, info.node)
+            info.cur +=1
+            if info.cur % info.progress_step == 0:
+                info.epg.signals["update_progress"].emit(info.cur, info.total)
+
+        info.node = info.node.get_next()
+        if time.time() - t0 > 0.1:
+            break
+
+    if not info.node:
+        info.epg.signals["update_progress"].emit(info.cur, info.total)
+        info.doc.freeDoc()
+        return False
+
+    return True
+    
+
+def update(epg, xmltv_file):
+    thread = kaa.notifier.Thread(_update_parse_xml_thread, epg, xmltv_file)
+    thread.start()

Added: trunk/WIP/epg2/src/source_zap2it.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/src/source_zap2it.py Mon Oct 17 02:29:37 2005
@@ -0,0 +1,247 @@
+import libxml2
+import md5, time, httplib, gzip, calendar
+from StringIO import StringIO
+from kaa.base.utils import str_to_unicode
+import kaa
+
+ZAP2IT_HOST = "datadirect.webservices.zap2it.com:80"
+ZAP2IT_URI = "/tvlistings/xtvdService"
+
+
+def H(m):
+    return md5.md5(m).hexdigest()
+
+soap_download_request = \
+'''<?xml version="1.0" encoding="utf-8"?>
+<SOAP-ENV:Envelope
+     xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/";
+     xmlns:xsd="http://www.w3.org/2001/XMLSchema";
+     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+     xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/";>
+<SOAP-ENV:Body>
+  <tms:download xmlns:tms="urn:TMSWebServices">
+    <startTime xsi:type="tms:dateTime">%s</startTime>
+    <endTime xsi:type="tms:dateTime">%s</endTime>
+  </tms:download>
+</SOAP-ENV:Body>
+</SOAP-ENV:Envelope>'''
+
+def get_auth_digest_response_header(username, passwd, uri, auth):
+    auth = auth[auth.find("Digest") + len("Digest "):].strip()
+    vals = [ x.split("=", 1) for x in auth.split(", ") ]
+    vals = [ (k.strip(), v.strip().replace('"', '')) for k, v in vals ]
+    params = dict(vals)
+
+    if None in [ params.get(x) for x in ("nonce", "qop", "realm") ]:
+        return None
+
+    nc = "00000001"
+    cnonce = md5.md5("%s:%s:%s:%s" % (nc, params["nonce"], time.ctime(),
+                                      
open("/dev/urandom").read(8))).hexdigest()
+
+    A1 = "%s:%s:%s" % (username, params["realm"], passwd)
+    A2 = "%s:%s" % ("POST", uri)
+    response = "%s:%s:%s:%s:%s:%s" % (H(A1), params["nonce"], nc, cnonce, 
+                                      params["qop"], H(A2))
+
+    response = md5.md5(response).hexdigest()
+
+    hdr = ('Digest username="%s", realm="%s", qop="%s", algorithm="MD5", ' + 
+          'uri="%s", nonce="%s", nc="%s", cnonce="%s", response="%s"') % \
+          (username, params["realm"], params["qop"], uri, params["nonce"],
+           nc, cnonce, response)
+    if "opaque" in params:
+        hdr += ', opaque="%s"' % params["opaque"]
+    return hdr
+
+
+
+def request(username, passwd, host, uri, start, stop, auth = None):
+    conn = httplib.HTTPConnection(host)
+    start_str = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(start))
+    stop_str = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(stop))
+    soap_request = soap_download_request % (start_str, stop_str)
+
+    headers = {
+        "Accept-Encoding": "gzip",
+        "Host": host,
+        "User-Agent": "kaa.epg/0.0.1",
+        "Content-Length": "%d" % len(soap_request),
+        "Content-Type": "text/xml; charset=utf-8",
+        "SOAPAction": "urn:TMSWebServices:xtvdWebService#download"
+    }
+    if auth:
+        headers["Authorization"] = auth
+    else:
+        # FIXME: find a better way to convey this.
+        print "Connecting to zap2it ..."
+
+    conn.request("POST", uri, None, headers)
+    conn.send(soap_request)
+    response = conn.getresponse()
+    if response.status == 401 and auth:
+        # Failed authentication.
+        raise ValueError, "zap2it authentication failed; bad username or 
password?"
+
+    if not auth and response.getheader("www-authenticate"):
+        header = response.getheader("www-authenticate")
+        auth  = get_auth_digest_response_header(username, passwd, uri, header)
+        return request(username, passwd, host, uri, start, stop, auth)
+
+    print "Downloading guide update ..."
+    data = response.read()
+    data = gzip.GzipFile(fileobj = StringIO(data)).read()
+    #open("guide.xml", "w").write(data)
+    conn.close()
+    return data
+
+
+def iternode(node):
+    child = node.children
+    while child:
+        yield child
+        child = child.get_next()
+
+
+def parse_station(node, info):
+    id = node.prop("id")
+    d = {}
+    for child in iternode(node):
+        if child.name == "callSign":
+            d["station"] = str_to_unicode(child.content)
+        elif child.name == "name":
+            d["name"] = str_to_unicode(child.content)
+    info.stations_by_id[id] = d
+
+
+def parse_map(node, info):
+    id = node.prop("station")
+    if id not in info.stations_by_id:
+        # Shouldn't happen.
+        return
+
+    channel = int(node.prop("channel"))
+    db_id = info.epg._add_channel_to_db(id, channel, 
info.stations_by_id[id]["station"],
+                                        info.stations_by_id[id]["name"])
+    info.stations_by_id[id]["db_id"] = db_id
+
+
+
+def parse_schedule(node, info):
+    program_id = node.prop("program")
+    if program_id not in info.schedules_by_id:
+        return
+    d = info.schedules_by_id[program_id]      
+    d["station_id"] = node.prop("station")
+    t = time.strptime(node.prop("time")[:-1], "%Y-%m-%dT%H:%M:%S")
+    d["start"] = int(calendar.timegm(t))
+    duration = node.prop("duration")
+
+    # Assumes duration is in the form PT00H00M
+    duration_secs = (int(duration[2:4])*60 + int(duration[5:7]))*60
+    d["stop"] = d["start"] + duration_secs
+    d["rating"] = str_to_unicode(node.prop("tvRating"))
+
+    info.epg._add_program_to_db(info.stations_by_id[d["station_id"]]["db_id"], 
d["start"],
+                           d["stop"], d.get("title"), d.get("desc"))
+
+
+def parse_program(node, info):
+    program_id = node.prop("id")
+    d = {}
+    for child in iternode(node):
+        if child.name == "title":
+            d["title"] = str_to_unicode(child.content)
+        elif child.name == "description":
+            d["desc"] = str_to_unicode(child.content)
+
+    info.schedules_by_id[program_id] = d
+
+
+def find_roots(node, roots = {}):
+    for child in iternode(node):
+        if child.name == "stations":
+            roots["stations"] = child
+        elif child.name == "lineup":
+            roots["lineup"] = child
+        elif child.name == "schedules":
+            roots["schedules"] = child
+        elif child.name == "programs":
+            roots["programs"] = child
+        elif child.name == "productionCrew":
+            roots["crew"] = child
+        elif child.children:
+            find_roots(child, roots)
+        if len(roots) == 5:
+            return
+     
+class UpdateInfo:
+    pass
+         
+def _update_parse_xml_thread(epg, username, passwd, start, stop):
+    data = request(username, passwd, ZAP2IT_HOST, ZAP2IT_URI, start, stop)
+    doc = libxml2.parseMemory(data, len(data))
+
+    stations_by_id = {}
+    roots = {}
+
+    find_roots(doc, roots)
+    nprograms = 0
+    for child in iternode(roots["schedules"]):
+        if child.name == "schedule":
+            nprograms += 1
+
+    info = UpdateInfo()
+    info.doc = doc
+    info.roots = [roots["stations"], roots["lineup"], roots["programs"], 
roots["schedules"]]
+    info.node_names = ["station", "map", "program", "schedule"]
+    info.node = None
+    info.total = nprograms
+    info.cur = 0
+    info.schedules_by_id = {}
+    info.stations_by_id = stations_by_id
+    info.epg = epg
+    info.progress_step = info.total / 100
+   
+    timer = kaa.notifier.Timer(_update_process_step, info)
+    timer.set_prevent_recursion()
+    timer.start(0)
+     
+    
+def _update_process_step(info):
+    t0=time.time()
+    if not info.node and info.roots:
+        info.node = info.roots.pop(0).children
+        info.cur_node_name = info.node_names.pop(0)
+
+    while info.node:
+        if info.node.name == info.cur_node_name:
+            globals()["parse_%s" % info.cur_node_name](info.node, info)
+        if info.node.name == "schedule":
+            info.cur += 1
+            if info.cur % info.progress_step == 0:
+                info.epg.signals["update_progress"].emit(info.cur, info.total)
+
+        info.node = info.node.get_next()
+        if time.time() - t0 > 0.1:
+            break
+
+    if not info.node and not info.roots:
+        info.epg.signals["update_progress"].emit(info.total, info.total)
+        info.doc.freeDoc()
+        return False
+
+    return True
+
+
+def update(epg, username, passwd, start = None, stop = None):
+    if not start:
+        # If start isn't specified, choose current time (rounded down to the 
+        # nearest hour).
+        start = int(time.time()) / 3600 * 3600
+    if not stop:
+        # If stop isn't specified, use 24 hours after start.
+        stop = start + (24 * 60 * 60)
+
+    thread = kaa.notifier.Thread(_update_parse_xml_thread, epg, username, 
passwd, start, stop)
+    thread.start()

Added: trunk/WIP/epg2/test/epgclient.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/test/epgclient.py    Mon Oct 17 02:29:37 2005
@@ -0,0 +1,56 @@
+# Test EPG client.  EPG server must be running.
+#
+# With no arguments, this will query the EPG server for all programs currently
+# running.  With arguments, this will query all programs that match against 
+# the keywords supplied as arguments.
+
+import os, time, sys, textwrap
+import kaa
+from kaa.epg2 import *
+
+def update_progress(cur, total):
+    n = 0
+    if total > 0:
+        n = int((cur / float(total)) * 50)
+    sys.stdout.write("|%51s| %d / %d\r" % (("="*n + ">").ljust(51), cur, 
total))
+    sys.stdout.flush()
+    if cur == total:
+        print
+
+guide = GuideClient("epg")
+guide.signals["update_progress"].connect(update_progress)
+
+# Initial import
+if guide.get_num_programs() == 0:
+    # update() is asynchronous so we enter kaa.main() and exit it
+    # once the update is finished.
+    guide.signals["updated"].connect(sys.exit)
+
+    # xmltv backend: specify path to XML file:
+    guide.update("xmltv", os.path.expanduser("~/.freevo/TV.xml"))
+
+    # zap2it backend, specify username/passwd and optional start/stop time 
(GMT)
+    # guide.update("zap2it", username="uname", passwd="passwd")
+    kaa.main()
+
+t0 = time.time()
+if len(sys.argv) > 1:
+    keywords = " ".join(sys.argv[1:])
+    print "Results for '%s':" % keywords
+    programs = guide.search(keywords = keywords)
+    # Sort by start time
+    programs.sort(lambda a, b: cmp(a.start, b.start))
+else:
+    print "All programs currently playing:"
+    programs = guide.search(time = time.time())
+    # Sort by channel
+    programs.sort(lambda a, b: cmp(a.channel.channel, b.channel.channel))
+t1 = time.time()
+
+for program in programs:
+    start_time = time.strftime("%a %H:%M", time.localtime(program.start))
+    print "  %s (%s): %s" % (program.channel.channel, start_time, 
program.title)
+    if program.desc:
+        print "\t* " + "\n\t  ".join(textwrap.wrap(program.desc, 60))
+print "- Queried %d programs; %s results; %.04f seconds" % \
+      (guide.get_num_programs(), len(programs), t1-t0)

Added: trunk/WIP/epg2/test/epgserver.py
==============================================================================
--- (empty file)
+++ trunk/WIP/epg2/test/epgserver.py    Mon Oct 17 02:29:37 2005
@@ -0,0 +1,5 @@
+from kaa.epg2 import *
+import kaa
+
+guide = GuideServer("epg")
+kaa.main()


-------------------------------------------------------
This SF.Net email is sponsored by:
Power Architecture Resource Center: Free content, downloads, discussions,
and more. http://solutions.newsforge.com/ibmarch.tmpl
_______________________________________________
Freevo-cvslog mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/freevo-cvslog

Reply via email to