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