Author: dmeyer
Date: Sat Feb 24 22:52:01 2007
New Revision: 2522
Modified:
trunk/epg/src/server.py
trunk/epg/src/sources/config_epgdata.cxml
trunk/epg/src/sources/config_xmltv.cxml
trunk/epg/src/sources/epgdata.py
trunk/epg/src/sources/xmltv.py
trunk/epg/src/sources/zap2it.py
Log:
cosmetic updates
Modified: trunk/epg/src/server.py
==============================================================================
--- trunk/epg/src/server.py (original)
+++ trunk/epg/src/server.py Sat Feb 24 22:52:01 2007
@@ -5,7 +5,7 @@
# $Id$
# -----------------------------------------------------------------------------
# kaa.epg - EPG Database
-# Copyright (C) 2004-2006 Jason Tackaberry, Dirk Meyer, Rob Shortt
+# Copyright (C) 2004-2007 Jason Tackaberry, Dirk Meyer, Rob Shortt
#
# First Edition: Jason Tackaberry <[EMAIL PROTECTED]>
#
@@ -76,9 +76,11 @@
self._clients = []
self._db = db
- self._setup_internal_variables()
self._rpc_server = []
+ # initial sync
+ self.sync()
+
# start unix socket rpc connection
s = kaa.rpc.Server('epg')
s.signals['client_connected'].connect(self.client_connected)
@@ -86,6 +88,49 @@
self._rpc_server.append(s)
+ def sync(self, result=False):
+ """
+ Sync database. The guide may changed by source, commit changes to
+ database and notify clients. Load some basic settings from the db.
+ The result parameter is not used but given by the InProgress callback
+ when this function is called after an update.
+ """
+ log.info('commit database changes')
+ self._db.commit()
+
+ # Load some basic information from the db.
+ 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._db._db_query(q)
+ if len(res):
+ self._max_program_length = res[0][0]
+
+ res = self._db._db_query("SELECT count(*) FROM objects_program")
+ if len(res):
+ self._num_programs = res[0][0]
+
+ self._tuner_ids = []
+ channels = self._db.query(type = "channel")
+ for c in channels:
+ for t in c["tuner_id"]:
+ if t in self._tuner_ids:
+ log.warning('loading channel %s with tuner_id %s '+\
+ 'allready claimed by another channel',
+ c["name"], t)
+ else:
+ self._tuner_ids.append(t)
+
+ # get channel list to be passed to a client on connect / update
+ self._channel_list = [ (r['id'], r['tuner_id'], r['name'],
r['long_name']) \
+ for r in self._db.query(type="channel") ]
+
+ info = self._channel_list, self._max_program_length, self._num_programs
+ for client in self._clients:
+ log.info('update client %s', client)
+ client.rpc('guide.update', info)
+
+
# -------------------------------------------------------------------------
# kaa.rpc interface called by kaa.epg.Client
# -------------------------------------------------------------------------
@@ -115,7 +160,11 @@
if not sources.has_key(backend):
raise ValueError, "No such update backend '%s'" % backend
log.info('update backend %s', backend)
- return sources[backend].update(self, *args, **kwargs)
+ result = sources[backend].update(self, *args, **kwargs)
+ if isinstance(result, kaa.notifier.InProgress):
+ # sync when guide is updated
+ result.connect(self.sync)
+ return result
@kaa.rpc.expose('guide.query')
@@ -154,20 +203,6 @@
# functions called by source_* modules
# -------------------------------------------------------------------------
- @kaa.notifier.execute_in_mainloop()
- def guide_changed(self):
- """
- Guide changed by source, commit changes to database and notify clients.
- """
- log.info('commit database changes')
- self._db.commit()
- self._setup_internal_variables()
- info = self._channel_list, self._max_program_length, self._num_programs
- for client in self._clients:
- log.info('update client %s', client)
- client.rpc('guide.update', info)
-
-
def add_channel(self, tuner_id, name, long_name):
"""
This method requires at least one of tuner_id, name,
@@ -297,37 +332,3 @@
if stop - start > self._max_program_length:
self._max_program_length = stop = start
return o["id"]
-
-
- # -------------------------------------------------------------------------
- # internal functions
- # -------------------------------------------------------------------------
-
- def _setup_internal_variables(self):
- """
- Load some basic information from the db.
- """
- 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._db._db_query(q)
- if len(res):
- self._max_program_length = res[0][0]
-
- res = self._db._db_query("SELECT count(*) FROM objects_program")
- if len(res):
- self._num_programs = res[0][0]
-
- self._tuner_ids = []
- channels = self._db.query(type = "channel")
- for c in channels:
- for t in c["tuner_id"]:
- if t in self._tuner_ids:
- log.warning('loading channel %s with tuner_id %s '+\
- 'allready claimed by another channel',
- c["name"], t)
- else:
- self._tuner_ids.append(t)
-
- # get channel list to be passed to a client on connect / update
- self._channel_list = [ (r['id'], r['tuner_id'], r['name'],
r['long_name']) \
- for r in self._db.query(type="channel") ]
Modified: trunk/epg/src/sources/config_epgdata.cxml
==============================================================================
--- trunk/epg/src/sources/config_epgdata.cxml (original)
+++ trunk/epg/src/sources/config_epgdata.cxml Sat Feb 24 22:52:01 2007
@@ -1,9 +1,11 @@
<?xml version="1.0"?>
<config>
<desc lang="en">
- epgdata settings
-
- MORE DESCRIPTION!
+ epgdata.com settings
+ A epgdata.com subscriptions is needed. Right now the service is in test
+ mode and it is possible to request a test key. It is unknown how long
+ such a test key will work and if it will be possible after that to use
+ the service.
</desc>
<var name="activate" default="False">
<desc lang="en">Get epg data from epgdata.com</desc>
Modified: trunk/epg/src/sources/config_xmltv.cxml
==============================================================================
--- trunk/epg/src/sources/config_xmltv.cxml (original)
+++ trunk/epg/src/sources/config_xmltv.cxml Sat Feb 24 22:52:01 2007
@@ -6,7 +6,7 @@
You can use a xmltv rabber to populate the epg database. To activate
the
xmltv grabber you need to set 'activate' to True and specify a
data_file
which already contains the current listing or define a grabber to
fetch the
- listings. Optionally you can define arguments for that grabber and the
+ listings. Optionally you can define arguments for that grabber and the
location of a sort program to sort the data after the grabber has
finished.
</desc>
<var name="activate" default="False">
@@ -15,11 +15,11 @@
<var name="data_file" type="str">
<desc lang="en">Location of XMLTV data file.</desc>
</var>
- <var name="grabber" default="xmltv">
+ <var name="grabber" type="str">
<desc lang="en">
If you want to run an XMLTV grabber to fetch your listings set
this to
the full path of your grabber program plus any additional custom
- arguments. If no path is specified it will use the default search
path
+ arguments. If no path is specified it will use the default search
path
($PATH).
</desc>
</var>
Modified: trunk/epg/src/sources/epgdata.py
==============================================================================
--- trunk/epg/src/sources/epgdata.py (original)
+++ trunk/epg/src/sources/epgdata.py Sat Feb 24 22:52:01 2007
@@ -2,10 +2,10 @@
# -----------------------------------------------------------------------------
# source_epgdata.py - get epg data from www.epgdata.com
# -----------------------------------------------------------------------------
-# $Id:
+# $Id:
# -----------------------------------------------------------------------------
# kaa.epg - EPG Database
-# Copyright (C) 2004-2006 Jason Tackaberry, Dirk Meyer, Rob Shortt
+# Copyright (C) 2007 Jason Tackaberry, Dirk Meyer, Rob Shortt
#
# First Edition: Tanja Kotthaus <[EMAIL PROTECTED]>
#
@@ -27,15 +27,16 @@
#
# -----------------------------------------------------------------------------
+__all__ = [ 'config', 'update' ]
+
# python imports
-import sys
import os
import time
import glob
import logging
# kaa imports
-from kaa import xml, TEMP
+from kaa import TEMP
import kaa.notifier
import kaa.xml
@@ -67,7 +68,7 @@
'd36':'director',
'd37':'actor',
'd40':'icon'
-}
+}
# the meaning of the tags that are used in the channel*.xml files can be found
# in the header of each channe*.xml file.
@@ -83,12 +84,12 @@
'g1':'name', # genre
'ca0':'id', # category_id
'ca2':'name' # category
-}
+}
def timestr2secs_utc(timestr):
"""
Convert the timestring to UTC (=GMT) seconds.
-
+
The time format in the epddata is:
'2002-09-08 00:00:00'
The timezone is german localtime, which is CET or CEST.
@@ -96,20 +97,20 @@
secs = time.mktime(time.strptime(timestr, '%Y-%m-%d %H:%M:%S'))
return secs
-
-
-def parse_data(info):
+
+
+def parse_data(info):
""" parse the info from the xml
-
+
The current node can be either from a channel or a program.
Subelements of the form <ch?> are for channels whereas <d?> are for
programs.
- See CH_MAPPING and PROG_MAPPING for a list of subelements that are
- processed and their meaning.
+ See CH_MAPPING and PROG_MAPPING for a list of subelements that are
+ processed and their meaning.
First all subelements of the node are read to a dictionary called attr
and then the info in the dictionary is processed further depending on what
kind of node we have.
"""
-
+
attr= {}
flag = None
# child is a <data> element and its children are containing the infos
@@ -119,13 +120,13 @@
flag = 'channel'
# let's process it
attr[CH_MAPPING[child.name]] = child.content
-
+
if child.name in META_MAPPING.keys():
# this is meta info
flag = 'meta'
# let's process it
attr[META_MAPPING[child.name]] = child.content
-
+
if child.name in PROG_MAPPING.keys():
# this is program info
flag = 'programme'
@@ -139,9 +140,9 @@
# if it is '1999-2004', take the first year
date = date.split('-')[0]
if not len(date)==4:
- # format unknown, ignore
+ # format unknown, ignore
continue
- else:
+ else:
fmt = '%Y'
attr['date'] = int(time.mktime(time.strptime(date, fmt)))
elif child.name=='d10' or child.name=='d25': # genre and category
@@ -151,18 +152,18 @@
except KeyError:
pass
else:
- attr[PROG_MAPPING[child.name]] = content
- else:
+ attr[PROG_MAPPING[child.name]] = content
+ else:
# process all other known elements
attr[PROG_MAPPING[child.name]] = child.content
-
+
if flag =='channel':
# create db_id
- db_id = info.epg.add_channel(tuner_id=attr['tvchannel_dvb'],
- name=attr['tvchannel_short'],
- long_name=attr['tvchannel_name'])
+ db_id = info.add_channel(tuner_id=attr['tvchannel_dvb'],
+ name=attr['tvchannel_short'],
+ long_name=attr['tvchannel_name'])
# and fill the channel_id_to_db_id dictionary
- info.channel_id_to_db_id[attr['tvchannel_id']] = db_id
+ info.channel_id_to_db_id[attr['tvchannel_id']] = db_id
if flag == 'meta':
info.meta_id_to_meta_name[attr['id']]=attr['name']
@@ -176,12 +177,12 @@
# translate channel_id to db_id
db_id = info.channel_id_to_db_id[attr.pop('channel_id')]
# fill this program to the database
- info.epg.add_program(db_id, start, stop, title, **attr)
+ info.add_program(db_id, start, stop, title, **attr)
#####
# this functions form the interface to freevo
-#####
+#####
class UpdateInfo:
"""
@@ -191,74 +192,74 @@
@kaa.notifier.execute_in_thread('epg')
-def _parse_xml(epg):
+def _parse_xml():
"""
Thread to parse the xml file. It will also call the grabber if needed.
"""
-
+
# create a tempdir as working area
tempdir = os.path.join(TEMP, 'epgdata')
if not os.path.isdir(tempdir):
os.mkdir(tempdir)
# and clear it if needed
- for i in glob.glob(os.path.join(tempdir,'*')):
- os.remove(i)
-
+ for i in glob.glob(os.path.join(tempdir,'*')):
+ os.remove(i)
+
# temp file
tmpfile = os.path.join(tempdir,'temp.zip')
# logfile
logfile = os.path.join(TEMP,'epgdata.log')
-
+
# empty list for the xml docs
docs = []
# count of the nodes that have to be parsed
nodes = 0
-
-
+
+
# create download adresse for meta data
- addresse = 'http://www.epgdata.com/index.php'
- addresse+= '?action=sendInclude&iLang=de&iOEM=xml&iCountry=de'
- addresse+= '&pin=%s' % config.pin
- addresse+= '&dataType=xml'
+ address = 'http://www.epgdata.com/index.php'
+ address+= '?action=sendInclude&iLang=de&iOEM=xml&iCountry=de'
+ address+= '&pin=%s' % config.pin
+ address+= '&dataType=xml'
+
-
# remove old file if needed
try:
os.remove(tmpfile)
except OSError:
- pass
- # download the meta data file
+ pass
+ # download the meta data file
log.info ('Downloading meta data')
- exit = os.system('wget -N -O %s "%s" >>%s 2>>%s'
- %(tmpfile, addresse, logfile, logfile))
+ exit = os.system('wget -N -O %s "%s" >>%s 2>>%s'
+ %(tmpfile, address, logfile, logfile))
if not os.path.exists(tmpfile) or exit:
log.error('Cannot get file from epgdata.com, see %s' %logfile)
return
- # and unzip the zip file
+ # and unzip the zip file
log.info('Unzipping data for meta data')
- exit = os.system('unzip -uo -d %s %s >>%s 2>>%s'
+ exit = os.system('unzip -uo -d %s %s >>%s 2>>%s'
%(tempdir, tmpfile, logfile, logfile))
if exit:
log.error('Cannot unzip the downloaded file, see %s' %logfile)
return
-
- # list of channel info xml files
- chfiles = glob.glob(os.path.join(tempdir,'channel*.xml'))
+
+ # list of channel info xml files
+ chfiles = glob.glob(os.path.join(tempdir,'channel*.xml'))
if len(chfiles)==0:
log.error('no channel xml files for parsing')
- return
-
- # parse this files
+ return
+
+ # parse this files
for xmlfile in chfiles:
try:
doc = kaa.xml.Document(xmlfile, 'channel')
except:
log.warning('error while parsing %s' %xmlfile)
continue
- docs.append(doc)
- nodes = nodes + len(doc.children)
-
-
+ docs.append(doc)
+ nodes = nodes + len(doc.children)
+
+
#parse the meta files
try:
# the genre file
@@ -268,8 +269,8 @@
log.warning('error while parsing %s' %xmlfile)
else:
# add the files to the list
- docs.append(doc)
- nodes = nodes + len(doc.children)
+ docs.append(doc)
+ nodes = nodes + len(doc.children)
try:
# the category file
xmlfile = os.path.join(tempdir, 'category.xml')
@@ -278,71 +279,69 @@
log.warning('error while parsing %s' %xmlfile)
else:
# add the files to the list
- docs.append(doc)
- nodes = nodes + len(doc.children)
-
-
- # create download adresse for programm files
- addresse = 'http://www.epgdata.com/index.php'
- addresse+= '?action=sendPackage&iLang=de&iOEM=xml&iCountry=de'
- addresse+= '&pin=%s' % config.pin
- addresse+= '&dayOffset=%s&dataType=xml'
-
- # get the file for each day
+ docs.append(doc)
+ nodes = nodes + len(doc.children)
+
+
+ # create download adresse for programm files
+ address = 'http://www.epgdata.com/index.php'
+ address+= '?action=sendPackage&iLang=de&iOEM=xml&iCountry=de'
+ address+= '&pin=%s' % config.pin
+ address+= '&dayOffset=%s&dataType=xml'
+
+ # get the file for each day
for i in range(0, int(config.days)):
# remove old file if needed
try:
os.remove(tmpfile)
except OSError:
- pass
- # download the zip file
+ pass
+ # download the zip file
log.info('Getting data for day %s' %(i+1))
- exit = os.system('wget -N -O %s "%s" >>%s 2>>%s'
- %(tmpfile, addresse %i, logfile, logfile))
+ exit = os.system('wget -N -O %s "%s" >>%s 2>>%s'
+ %(tmpfile, address %i, logfile, logfile))
if not os.path.exists(tmpfile) or exit:
log.error('Cannot get file from epgdata.com, see %s' %logfile)
return
- # and unzip the zip file
+ # and unzip the zip file
log.info('Unzipping data for day %s' %(i+1))
- exit = os.system('unzip -uo -d %s %s >>%s 2>>%s'
+ exit = os.system('unzip -uo -d %s %s >>%s 2>>%s'
%(tempdir, tmpfile, logfile, logfile))
if exit:
log.error('Cannot unzip the downloaded file, see %s' %logfile)
return
-
-
- # list of program xml files that must be parsed
- progfiles = glob.glob(os.path.join(tempdir,'*de_q[a-z].xml'))
+
+
+ # list of program xml files that must be parsed
+ progfiles = glob.glob(os.path.join(tempdir,'*de_q[a-z].xml'))
if len(progfiles)==0:
log.warning('no progam xml files for parsing')
-
- # parse the progam xml files
+
+ # parse the progam xml files
for xmlfile in progfiles:
try:
doc = kaa.xml.Document(xmlfile, 'pack')
except:
log.warning('error while parsing %s' %xmlfile)
continue
- # add the files to the list
- docs.append(doc)
- nodes = nodes + len(doc.children)
-
- log.info('There are %s files to parse with in total %s nodes'
+ # add the files to the list
+ docs.append(doc)
+ nodes = nodes + len(doc.children)
+
+ log.info('There are %s files to parse with in total %s nodes'
%(len(docs), nodes))
-
-
+
+
# put the informations in the UpdateInfo object.
info = UpdateInfo()
- info.epg = epg
- info.pin = config.pin
info.channel_id_to_db_id = {}
info.meta_id_to_meta_name = {}
- info.docs =docs
+ info.docs = docs
info.doc = info.docs.pop(0)
info.node = info.doc.first
info.total = nodes
info.progress_step = info.total / 100
-
+
return info
@@ -359,12 +358,14 @@
# it always returns an InProgress object that needs to be yielded.
# When yield returns we need to call the InProgress object to get
# the result. If the result is None, the thread run into an error.
- info = _parse_xml(epg)
+ info = _parse_xml()
yield info
info = info()
if not info:
yield False
+ info.add_program = epg.add_program
+ info.add_channel = epg.add_channel
t0 = time.time()
while info.node or len(info.docs) > 0:
while info.node:
@@ -381,5 +382,4 @@
info.doc = info.docs.pop(0)
# and start with its first node
info.node = info.doc.first
- epg.guide_changed()
yield True
Modified: trunk/epg/src/sources/xmltv.py
==============================================================================
--- trunk/epg/src/sources/xmltv.py (original)
+++ trunk/epg/src/sources/xmltv.py Sat Feb 24 22:52:01 2007
@@ -5,7 +5,7 @@
# $Id$
# -----------------------------------------------------------------------------
# kaa.epg - EPG Database
-# Copyright (C) 2004-2006 Jason Tackaberry, Dirk Meyer, Rob Shortt
+# Copyright (C) 2004-2007 Jason Tackaberry, Dirk Meyer, Rob Shortt
#
# First Edition: Jason Tackaberry <[EMAIL PROTECTED]>
#
@@ -130,7 +130,7 @@
# for the used grabber somehow.
name = display or station
- db_id = info.epg.add_channel(tuner_id=channel, name=station,
long_name=name)
+ db_id = info.add_channel(tuner_id=channel, name=station, long_name=name)
info.channel_id_to_db_id[channel_id] = [db_id, None]
@@ -176,12 +176,12 @@
# user to run tv_sort to fix this. And IIRC tv_sort also takes care of
# this problem.
last_start, last_title, last_attr = last_prog
- info.epg.add_program(db_id, last_start, start, last_title, **last_attr)
+ info.add_program(db_id, last_start, start, last_title, **last_attr)
if not info.node.getattr("stop"):
info.channel_id_to_db_id[channel_id][1] = (start, title, attr)
else:
stop = timestr2secs_utc(info.node.getattr("stop"))
- info.epg.add_program(db_id, start, stop, title, **attr)
+ info.add_program(db_id, start, stop, title, **attr)
class UpdateInfo:
@@ -192,7 +192,7 @@
@kaa.notifier.execute_in_thread('epg')
-def _parse_xml(epg):
+def _parse_xml():
"""
Thread to parse the xml file. It will also call the grabber if needed.
"""
@@ -245,7 +245,6 @@
info.node = doc.first
info.channel_id_to_db_id = channel_id_to_db_id
info.total = nprograms
- info.epg = epg
info.progress_step = info.total / 100
return info
@@ -263,12 +262,14 @@
# it always returns an InProgress object that needs to be yielded.
# When yield returns we need to call the InProgress object to get
# the result. If the result is None, the thread run into an error.
- info = _parse_xml(epg)
+ info = _parse_xml()
yield info
info = info()
if not info:
yield False
+ info.add_program = epg.add_program
+ info.add_channel = epg.add_channel
t0 = time.time()
while info.node:
if info.node.name == "channel":
@@ -282,5 +283,4 @@
yield kaa.notifier.YieldContinue
t0 = time.time()
- info.epg.guide_changed()
yield True
Modified: trunk/epg/src/sources/zap2it.py
==============================================================================
--- trunk/epg/src/sources/zap2it.py (original)
+++ trunk/epg/src/sources/zap2it.py Sat Feb 24 22:52:01 2007
@@ -5,7 +5,7 @@
# $Id$
# -----------------------------------------------------------------------------
# kaa.epg - EPG Database
-# Copyright (C) 2004-2006 Jason Tackaberry, Dirk Meyer, Rob Shortt
+# Copyright (C) 2004-2007 Jason Tackaberry, Dirk Meyer, Rob Shortt
#
# First Edition: Jason Tackaberry <[EMAIL PROTECTED]>
#
@@ -176,9 +176,9 @@
return
channel = int(node.prop("channel"))
- db_id = info.epg.add_channel(tuner_id=channel,
- name=info.stations_by_id[id]["station"],
- long_name=info.stations_by_id[id]["name"])
+ db_id = info.add_channel(tuner_id=channel,
+ name=info.stations_by_id[id]["station"],
+ long_name=info.stations_by_id[id]["name"])
info.stations_by_id[id]["db_id"] = db_id
@@ -198,8 +198,8 @@
d["stop"] = d["start"] + duration_secs
d["rating"] = str_to_unicode(node.prop("tvRating"))
- info.epg.add_program(info.stations_by_id[d["station_id"]]["db_id"],
d["start"],
- d["stop"], d.get("title"), desc=d.get("desc"))
+ info.add_program(info.stations_by_id[d["station_id"]]["db_id"], d["start"],
+ d["stop"], d.get("title"), desc=d.get("desc"))
def parse_program(node, info):
@@ -235,8 +235,9 @@
pass
@kaa.notifier.execute_in_thread('epg')
-def _parse_xml(epg, username, passwd, start, stop):
- filename = request(username, passwd, ZAP2IT_HOST, ZAP2IT_URI, start, stop)
+def _parse_xml(start, stop):
+ filename = request(str(config.username), str(config.password),
+ ZAP2IT_HOST, ZAP2IT_URI, start, stop)
if not filename:
return
doc = libxml2.parseFile(filename)
@@ -259,7 +260,6 @@
info.total = nprograms
info.schedules_by_id = {}
info.stations_by_id = stations_by_id
- info.epg = epg
info.progress_step = info.total / 100
info.t0 = time.time()
@@ -278,12 +278,14 @@
# it always returns an InProgress object that needs to be yielded.
# When yield returns we need to call the InProgress object to get
# the result. If the result is None, the thread run into an error.
- info = _parse_xml(epg, str(config.username), str(config.password), start,
stop)
+ info = _parse_xml(start, stop)
yield info
info = info()
if not info:
yield False
+ info.add_program = epg.add_program
+ info.add_channel = epg.add_channel
t0 = time.time()
while info.node or info.roots:
if not info.node:
@@ -301,7 +303,6 @@
os.unlink(info.doc.name)
info.doc.freeDoc()
- info.epg.guide_changed()
- log.info('Processed %d programs in %.02f seconds', info.epg._num_programs,
+ log.info('Processed %d programs in %.02f seconds', epg._num_programs,
time.time() - info.t0)
yield True
-------------------------------------------------------------------------
Take Surveys. Earn Cash. Influence the Future of IT
Join SourceForge.net's Techsay panel and you'll get the chance to share your
opinions on IT & business topics through brief surveys-and earn cash
http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV
_______________________________________________
Freevo-cvslog mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/freevo-cvslog