Commits

Guido Draheim committed 4fb09b5 Merge

Merge with dvbcronrecording-0.4.19

Comments (0)

Files changed (17)

 9e2c68304789bef08fdd71ffb1455ee771383c9e dvbcronrecording-0.4.16
 395c51bb705e027352eb40dda3c2ebdc5409a74e dvbcronrecording-0.4.17
 4f6036dd024e926e9f5c8e84d58c76e82b0c0b8e dvbcronrecording-0.4.18
+44e6cab2debff4b69d6e0b2b064ccca61292952d dvbcronrecording-0.4.19

src/dvbcronrecording/core.py

             page = m.group(1)
             if page and page.startswith("channel"): return False
             if page and page.startswith("file"): return False
+            if page and page.startswith("programguide"): return False
             req.args['page'] = page 
             return True
         return False
         if page in [ 'delete' ]:
             req.perm.assert_permission(LIST_DELETE)
             message = self._recordings_delete(req)
+        if page in [ 'deleteold' ]:
+            req.perm.assert_permission(LIST_DELETE)
+            message = self._recordings_delete_old(req)
         
         # database commit and return page content
         commit()
         q = q.filter_by(id = req.args.get("id"))
         q.delete()
 
+    """
+      Deletes entry from recordings that have a single day for
+      recording in last weeks (older in this month or being in last month) 
+    """
+    def _recordings_delete_old(self, req):
+        deleted = []
+        make = re.compile("^(\d+)[.](\d+).*")
+        today = datetime.datetime.now()
+        this_month = today.month
+        last_month = this_month - 1
+        if last_month == 0: last_month = 12
+        prev_month = last_month - 1
+        if prev_month == 0: prev_month = 12
+        session = db_cnx(self.env) 
+        q = session.query(RecordingList).filter(RecordingList.onlydate.like("%.%"))
+        for item in q.all():
+            m = make.match(item.onlydate)
+            if m:
+                day = int(m.group(1))
+                month = int(m.group(2))
+                if month == this_month and day < today.day:
+                    deleted += [ item.id ]
+                    session.delete(item)
+                if month == last_month or month == prev_month:
+                    deleted += [ item.id ]
+                    session.delete(item)
+        # commit is done in parent
+        return "[%s]" % (",".join([ str(item) for item in deleted]))
 
     # ---------------------------------------------------------------
     # old-style format

src/dvbcronrecording/db/db5.py

+# http://trac-hacks.org/wiki/TracSqlAlchemyBridgeIntegration
+
+from sqlalchemy import (
+    # MetaData,
+    # Boolean,
+    Column,
+    DateTime,
+    # ForeignKey,
+    Integer,
+    String,
+    # Unicode,
+    )
+#from sqlalchemy.orm import relationship, backref
+#from sqlalchemy.orm.exc import NoResultFound
+
+from sqlalchemy.ext.declarative import declarative_base
+RecordingModel = declarative_base()
+
+class RecordingList(RecordingModel):
+    __tablename__ = "recording_list"
+    id = Column('id', Integer, primary_key=True)
+    channelname = Column('channelname', String)
+    newtime = Column('newtime', String)
+    endtime = Column('endtime', String)
+    extratime = Column('extratime', String)
+    onlydate = Column('onlydate', String)
+    title = Column('title', String)
+    status = Column('status', String)
+    weekday = Column('weekday', Integer)
+    priority = Column('priority', Integer)
+
+class RecordingChannels(RecordingModel):
+    __tablename__ = 'recording_channels'
+    id = Column('id', Integer, primary_key=True)
+    channelname = Column('channelname', String)
+    adapter = Column('adapter', String)
+    title = Column('title', String)
+
+class RecordingChannelsConf(RecordingModel):
+    __tablename__ = 'recording_channelsconf'
+    adapter = Column('adapter', String, primary_key = True)
+    title = Column('title', String, primary_key = True)
+    frequency = Column('frequency', Integer)
+    polarity = Column('polarity', String)
+    source = Column('source', String)
+    symbolrate = Column('symbolrate', Integer)
+    vpid = Column('vpid', Integer)
+    apid = Column('apid', Integer)
+    tpid = Column('tpid', Integer)
+
+class RecordingTuning(RecordingModel):
+    __tablename__ = 'recording_tuning'
+    id = Column("id", Integer, primary_key=True)
+    adapter = Column('adapter', String)
+    satellite = Column('satellite', String)
+    transponder = Column('transponder', String)
+    scansettings = Column('scansettings', String)
+    scansourcefile = Column('scansourcefile', String)
+    channelsconf = Column('channelsconf', String)
+
+class RecordingChanges(RecordingModel):
+    __tablename__ = 'recording_changes'
+    id = Column("id", Integer, primary_key=True)
+    tablename = Column("tablename", String)
+    changed = Column("changed", String)
+    username = Column("username", String)
+    modified = Column("modified", DateTime)
+
+class ProgramguideChannels(RecordingModel):
+    __tablename__ = 'recording_programguide'
+    id = Column("id", Integer, primary_key=True)
+    programguide = Column("programguide", String)
+    programchannel = Column("programchannel", String)
+    channelname = Column("channelname", String)
+    extratime = Column("extratime", String)
+    extrapercent = Column("extrapercent", String)
+    priority = Column("priority", Integer)
+
+metadata = RecordingModel.metadata
+
+from tsab import engine
+
+def create_all(env, cursor):
+    metadata.create_all(bind=engine(env))
+
+def upgrade(env, cursor):
+    import schemachange
+    try:
+        for sql in schemachange.migrate_AtoB(old_metadata(), metadata, env, excludeTables = None):
+            print sql
+    except Exception, e:
+        import traceback
+        traceback.print_exc(e)
+        raise
+
+def downgrade(env, cursor):
+    import schemachange
+    for sql in schemachange.migrate_BtoA(old_metadata(), metadata, env, excludeTables = None):
+        print sql
+
+def old_metadata():
+    import db4
+    return db4.metadata

src/dvbcronrecording/db/schema.py

 
-import dvbcronrecording.db.db4 as _db 
+import dvbcronrecording.db.db5 as _db
 
 metadata = _db.metadata
 tables = metadata.sorted_tables # support old aff() detection
 RecordingChannelsConf = _db.RecordingChannelsConf
 RecordingList = _db.RecordingList
 RecordingChanges = _db.RecordingChanges
+ProgramguideChannels = _db.ProgramguideChannels
 
 pass

src/dvbcronrecording/db/schemachange.py

+# NOTE: I just came across this, a migration tool from the author of sqlalchemy
+# https://bitbucket.org/zzzeek/alembic
+
+
 import schemadiff
-import tsab
+import tsab2 as tsab
+import sys
+
+import sqlalchemy
 
 def alter_sql_AtoB(metadataA, metadataB, env, excludeTables = None):
     """ given a SchemaDiff instance, generate the corresponding
     changer = Changer(diff, env)
     for change in changer.migrateBtoA():
             yield change
-7            
-class DummyStatement:
-    def supports_execution(self):
-        return False
+
+import sqlalchemy.sql.expression
+class SchemaChangeElement(sqlalchemy.sql.expression.ClauseElement):
+    __visit_name__ = "schema_change"
     
 class Changer:
     def __init__(self, schemadiff, env):
         self._engine = None
         self._session = None
     def engine(self):
+        print >> sys.stderr, "ENGINE"
         if self._engine is None:
             self._engine = tsab.engine(self.env)
         return self._engine
                 print "unkonwn dialect", self.dialect()
         return self._ddl_dialect
     def ddl_compiler(self):
+        """ we do all DDL compilation ourselves. But we take advantage of
+            the ddl.get_column_specification(coldef) sql conversions. """
         if self._ddl_compiler is None:
-            statement = DummyStatement()
+            statement = SchemaChangeElement()
             if self.dialect() in [ "sqlite" ]:
                 # import sqlalchemy.dialects.sqlite.base
                 dialect = self.ddl_dialect()
                 import sqlalchemy.dialects.sqlite.base
-                q = sqlalchemy.dialects.sqlite.base.SQLiteDDLCompiler(dialect, statement)
+                class SQLiteDDL(sqlalchemy.dialects.sqlite.base.SQLiteDDLCompiler):
+                    def visit_schema_change(self, s):
+                        """ SQLAlchemy 0.7 compiles immediately on __init__ """
+                        pass
+                q = SQLiteDDL(dialect, statement)
                 # q = dialect.ddl_compiler(dialect, statement)
                 self._ddl_compiler = q
             else:

src/dvbcronrecording/db/version.py

-number = 4
+number = 5

src/dvbcronrecording/dvbcronrecording.de.po

 msgid "channelname"
 msgstr "Senderkennung"
 
+msgid "program guide"
+msgstr "Programmzeitschrift"
+
 msgid "recorder list"
 msgstr "Recorder-Liste"
 
 
 msgid " previous moved after next recording - swapped around,"
 msgstr " vorige verschoben hinter nächste Aufname - vertauscht,"
+
+msgid "delete old singular recording times"
+msgstr " [Lösche veraltete Einzelaufnahmezeiten] "

src/dvbcronrecording/htdocs/css/dvbcronrecording.css

         padding: 2px 1em;
        border: 1px outset gray; background-color: #EEE;
 }
+
+.programguide table { padding: 0 }

src/dvbcronrecording/htdocs/css/programguide.css

+.programguide .new { font-weight: bold; color: #040; }
+.programguide th { text-align: left; border-bottom: 1px dotted black; font-size: small; font-weight: bold; }
+.programguide .id { width: 2em; text-align: right; padding-right: 0.5em; font-size: small; }
+.programguide .programchannel { width: 10em; }
+.programguide .channelname { width: 5em; }
+.programguide .extratime { width: 5em; }
+.programguide .extrapercent { width: 5em; }
+.programguide .delaction { font-size: xx-small; padding: 0; border-spacing: 0;  }
+
+.programguide .programchannel input { width: 10em; }
+.programguide .channelname input { width: 5em; }
+.programguide .extratime input { width: 5em; text-align: right; }
+.programguide .extrapercent input { width: 5em; text-align: right; }
+.programguide .action input { width: 10em; }
+

src/dvbcronrecording/htdocs/css/recordinglist.css

                 background-color: #DDD; padding: 1px 14px; }
 .actionform div { display: inline } /* Trac specific */
 
+.recordinglist .deleteold { padding : 2em; }
+
 .recordinglist .new { font-weight: bold; color: #040; }
 .recordinglist th { text-align: left; border-bottom: 1px dotted black; font-size: small; font-weight: bold; }
 .recordinglist .id { width: 2em; text-align: right; padding-right: 0.5em; font-size: small; }

src/dvbcronrecording/htdocs/js/dvbcronrecording_tvspielfilm.user.js

+// ==UserScript== 
+// @name          DvbCronRecording TvSpielfilm
+// @description   Save entries on the program guide to the recording list
+// @namespace     http://bitbucket.org/2011/dvbcronrecording
+// @source        http://SERVER/recording/programguide
+// @version       0.1.1.0
+// @creator       Guido Draheim <guidod-2011-@gmx.de>
+// @license       http://creativecommons.org/licenses/by-nc-sa/2.0/de/
+// @include       http://www.tvspielfilm.de/sendung/*
+// @include       http://tvspielfilm.de/sendung/*
+// @include       http://www.tvspielfilm.de/tv-programm/sendung/*
+// @include       http://tvspielfilm.de/tv-programm/sendung/*
+              http://www.tvspielfilm.de/tv-programm/sendung/lets-dance,107029138385.html
+// @require       http://SERVER/chrome/common/js/jquery.js
+// ==/UserScript==
+
+var _url = "http://SERVER/recording/programguide/save/tvspielfilm";
+
+function url_data(data) {
+	var text = "";
+	for (var elem in data) {
+		if (text == "") text += "?"; else text += "&";
+		text += encodeURIComponent(elem) + "=" + encodeURIComponent(data[elem]);
+	}
+	return text;
+}
+
+function parentsUntil(node, selector) {
+	var N = $(node);
+	if (N && $(N).is(selector)) return N; else N = $(N).parent();
+	if (N && $(N).is(selector)) return N; else N = $(N).parent();
+	if (N && $(N).is(selector)) return N; else N = $(N).parent();
+	if (N && $(N).is(selector)) return N; else N = $(N).parent();
+	if (N && $(N).is(selector)) return N; else N = $(N).parent();
+	if (N && $(N).is(selector)) return N; else N = $(N).parent();
+	return node;
+}
+
+function save_error(response) {
+	GM_log("ERROR "+response.statusText);
+    $("#dvbcronrecording").css("border", "2px solid red")
+    var value = $(".error", response.responseText).text();
+    $("#dvbcronrecording").text("! "+response.statusText+" ! "+value);
+}
+
+function save_success(response) {
+	GM_log("STORED "+response.statusText);
+    $("#dvbcronrecording").css("border", "1px solid green");
+    $("#dvbcronrecording").css("color", "green");
+    var value = $(".error", response.responseText).text();
+    if (value.match("OOPS")) {
+    	$("#dvbcronrecording").css("border", "2px solid red");
+    	$("#dvbcronrecording").css("color", "red");
+    }
+    $("#dvbcronrecording").text("| "+response.statusText+" | "+value);
+}
+
+
+function save_send(date, time, channel, title) {
+    var data = { op: "insert",
+    		date: date,
+    		time: time,
+    		channel: channel,
+    		title: title };
+    GM_log("SEND "+url_data(data));
+    GM_xmlhttpRequest({
+    	method: "GET", url : _url + url_data(data),
+    	headers: { 
+    		"Accept" : "text/html,text/xml" },
+    	onload: save_success, onerror: save_error }); 
+}
+
+
+function clicked_save(event) {
+	var target = event.target;
+	var film_info = parentsUntil(target, ".film-info");
+	var film_items = $("li", film_info);
+	var date = $(film_items.get(0)).text();
+	var time = $(film_items.get(1)).text();
+	var channel = $(film_items.get(2)).text();
+	var title = $($("h1").get(0)).text();
+	// alert("DATE:"+date+" TIME:"+time+" CHANNEL:"+channel+" TITLE:"+title);
+	save_send(date, time, channel, title);
+}
+
+function register()
+{
+	GM_log("register dvbcronrecording")
+	film_info = $(".film-info");
+	if (film_info.length == 1) {
+		var mark = $("ul", film_info);
+		if (mark.length != 1) {
+			alert("mark = " + mark.length);
+		}
+		var div = $(document.createElement("li")).attr("id","dvbcronrecording").css("color", "red").css("font-weight", "bold");
+		div.text("SAVE");
+		div.click(clicked_save);
+		div.appendTo(mark);
+	}
+}
+
+$(window).load(register);
+GM_log("ready dvbcronrecording");
+

src/dvbcronrecording/programguide.py

+# -*- coding: utf-8 -*-
+import pkg
+import re
+import os.path
+import sys
+import datetime
+
+from trac.core import Component, implements
+# from trac.db import *
+#from trac.wiki import wiki_to_html, wiki_to_oneliner
+from trac.web.chrome import add_stylesheet, add_script
+#from trac.util import Markup, format_datetime
+
+from trac.web import IRequestHandler
+from trac.perm import IPermissionRequestor
+from trac.web.chrome import  ITemplateProvider # INavigationContributor,
+from trac.util.presentation import Paginator
+from translate import Translate #@UnresolvedImport
+
+from dvbcronrecording.db.session import db_cnx, commit
+from dvbcronrecording.db.schema import RecordingList #@UnresolvedImport
+from dvbcronrecording.db.schema import RecordingChanges #@UnresolvedImport
+
+def intnull(value, default=None):
+    if value is None: return default
+    try: return int(value)
+    except Exception: return default
+def lookup(lists, entry, default=None):
+    if entry is None: return default
+    idx = intnull(entry)
+    if idx is None or idx >= len(lists): return default
+    return lists[idx]
+def ustr(text):
+    if type(text) is unicode: return text
+    if type(text) is str: return unicode(text)
+    return unicode(str(text))
+
+PACKAGE = u'dvbcronrecording'
+NAV = 'recordings'
+URL = 'recording'
+SUBURL = 'programguide'
+
+LIST_APPEND = 'DVBREC_LIST_APPEND'
+
+"""
+  here we are.
+"""
+class DvbCronRecordingProgramguidePlugin(Component):
+
+    #
+    # Public methods
+    #
+
+    implements(IPermissionRequestor, ITemplateProvider, IRequestHandler)
+    
+    programguide_list = [ "tvspielfilm" ]
+
+    # IPermissionRequestor methods
+
+    """
+      Returns list of permitions privided by this plugin.
+    """
+    def get_permission_actions(self):
+        return [ LIST_APPEND ]
+
+    # ITemplateProvider methods
+
+    """
+      Returns additional path where stylesheets are placed.
+    """
+    def get_htdocs_dirs(self):
+        return [(PACKAGE, pkg.resource_filename(__name__, 'htdocs'))]
+
+    """
+      Returns additional path where templates are placed.
+    """
+    def get_templates_dirs(self):
+        return [pkg.resource_filename(__name__, 'templates')]
+
+    # IRequestHandler methods
+
+    """
+      Determines if request should be handled by this plugin.
+    """
+    def match_request(self, req):
+        m = re.match(r"/%s/%ss?(?:/(.*))?$" % (URL, SUBURL), req.path_info)
+        if m:
+            page = m.group(1)
+            req.args['page'] = page 
+            return True
+        return False
+
+    """
+      Handles display and download requests on this plugin.
+    """
+    def process_request(self, req):
+        req.perm.assert_permission(LIST_APPEND)
+        # ------------------------------------------------
+        translate = Translate(PACKAGE, req.locale)
+        userscripts_dir = self.get_scripts_dir()
+
+        page = req.args['page']
+        message = "[%s]" % page
+
+        if page and page.startswith("install/"):
+            target = page[len("install/"):]
+            return self.do_install(req, page, target)
+        
+        if page and page.startswith("save/"):
+            target = page[len("save/"):]
+            return self.do_save(req, page, target)
+            
+        return self.do_default(req, message)
+    
+    def do_install(self, req, page, target):
+        translate = Translate(PACKAGE, req.locale)
+        userscripts_dir = self.get_scripts_dir()
+        filepath = os.path.join(userscripts_dir, target)
+        if os.path.exists(filepath):
+            try:
+                f = open(filepath)
+                text = f.read()
+                f.close()
+                text = text.replace("http://SERVER", ustr(req.base_url).encode("utf-8"))
+                req.send_header("Content-Disposition:", 'inline; filename="%s"' % target)
+                req.send(text, "application/octet-stream")
+                return
+            except Exception, e:
+                # req.send_error(sys.exc_info(), status=500, env=self.env, data = { "page" : page})
+                # return
+                message = "OOPS %s" % (e,)
+        else:
+            message = translate("userscript not found:")
+            message += " %s" % target
+        return self.do_default(req, message)
+    
+    def do_save(self, req, page, target):
+        # decode the strings from the remote page
+        programguide = target
+        programchannel = req.args["channel"]
+        date = req.args["date"]
+        time = req.args["time"]
+        title = req.args["title"]
+        on_date = re.compile(r"\w\w\s+(\d+)[.](\d+)[.]\s*")
+        on_time = re.compile(r"(\d+:\d+)\s+-\s+(\d+:\d+)")
+        newdate = None
+        newtime = None
+        endtime = None
+        m = on_date.match(date)
+        if m:
+            day = m.group(1)
+            month = m.group(2)
+            current = datetime.datetime.now()
+            year = current.year
+            if int(month) < current.month:
+                year += 1  
+            newdate = datetime.datetime(year, int(month), int(day))
+        m = on_time.match(time)
+        if m:
+            newtime = m.group(1)
+            endtime = m.group(2)
+        message = self.save(req, newdate, newtime, endtime, title, programchannel, programguide)
+        return self.do_default(req, message)
+        
+    def do_default(self, req, message):
+        translate = Translate(PACKAGE, req.locale)
+        userscripts_dir = self.get_scripts_dir()
+        userscripts = []
+        for filename in os.listdir(userscripts_dir):
+            if filename.endswith(".user.js"):
+                userscripts += [ filename ]
+        
+        programguidedata = list(self.programguidechannels_all())
+
+        add_stylesheet(req, 'common/css/wiki.css')
+        add_stylesheet(req, PACKAGE + '/css/dvbcronrecording.css')
+        add_stylesheet(req, PACKAGE + '/css/programguide.css')
+        add_script(req, 'common/js/trac.js')
+        add_script(req, 'common/js/wikitoolbar.js')
+
+        # passing variables to template
+        data = {}
+        data['message'] = message
+        data['title'] = translate('Programguide List')
+        data['scripts_url'] = self.get_scripts_url("")
+        data['install_url'] = self.get_install_url("")
+        data["_pagenum"] = req.args.get("_pagenum", "0")
+        data["_pagesize"] = req.args.get("_pagesize", "10")
+        data['datalist'] = Paginator(programguidedata, int(data["_pagenum"]), int(data["_pagesize"]))
+        data['prioritynames'] = translate("**prioritynames")
+        data['programguides'] = self.programguide_list
+        data['userscriptstitle'] = translate('Userscript List')
+        data['userscripts'] = userscripts
+        data['author'] = req.authname or ""
+        data['_'] = translate
+
+        return ('programguide_list.html', data, None)
+
+    def get_scripts_dir(self):
+        htdocsdir = self.get_htdocs_dirs()[0][1]
+        return os.path.join(str(htdocsdir), "js")
+
+    def get_install_url(self, name):
+        return "%s/%s/install/%s" % (URL, SUBURL, name)
+
+    def get_scripts_url(self, name):
+        return "/chrome/%s/js/%s" % (PACKAGE, name)
+
+    def save(self, req, newdate, newtime, endtime, title, programchannel, programguide):
+        onlydate = newdate.strftime("%d.%m.")
+        weekday = newdate.weekday()
+        session = db_cnx(self.env)
+        q = session.query(RecordingList).filter_by(title=title, onlydate=onlydate)
+        existing = q.count()
+        if existing > 0:
+            return "EXISTS"
+        item = RecordingList()
+        item.channelname = ""
+        item.newtime = newtime
+        item.endtime = endtime
+        item.extratime = 1
+        item.weekday = weekday
+        item.onlydate = onlydate
+        item.status = "ok"
+        item.priority = 3
+        item.title = title
+        for entry in self.programguidechannels_all():
+            if entry.programguide == programguide:
+                if entry.programchannel == programchannel:
+                    if entry.channelname:
+                        item.channelname = entry.channelname
+                    if entry.priority:
+                        item.priority = entry.priority
+                    extratime = 0
+                    try:
+                        if entry.extratime:
+                            extratime = int(entry.extratime)
+                        if entry.extrapercent:
+                            percent = int(entry.extrapercent)
+                            minutes = self.minutes(item.newtime, item.endtime)
+                            extratime += (minutes * percent) / 100
+                    except Exception, e:
+                        item.title += "ERROR:%s" % e
+                    except:
+                        pass
+                    item.extratime = extratime
+        if not item.channelname:
+            item.channelname = programchannel.lower()
+        session.add(item)
+        datespec = "[%s-%s%s]" % (item.newtime,
+                                item.endtime,
+                                item.onlydate 
+                               ) 
+        changed = "new recording on %s %s %s (from %s)" % (item.channelname,
+                                                 datespec,
+                                                 item.title,
+                                                 programguide)
+        session.add(RecordingChanges(tablename="RecordingList",
+                                  username=req.authname,
+                                  modified=datetime.datetime.now(),
+                                  changed=changed))
+        session.flush()
+        return u"(%s)" % item.id
+    
+    def programguidechannels_all(self):
+        """ FIXME: only hardcoded stuff here"""
+        for entry in self.hardcoded_programguidechannels():
+            yield entry
+
+    def hardcoded_programguidechannels(self):
+        import collections
+        Entry = collections.namedtuple("ProgramguideEntry",
+            ["programguide", "programchannel", "channelname",
+             "extratime", "extrapercent", "priority"])
+        yield Entry("tvspielfilm", "Das Erste", "1ard", 2, 2, 3)
+        yield Entry("tvspielfilm", "ZDF", "2zdf", 2, 2, 3)
+        yield Entry("tvspielfilm", "RTL", "rtl", 4, 4, 3)
+        yield Entry("tvspielfilm", "SAT.1", "sat1", 4, 4, 3)
+        yield Entry("tvspielfilm", "ProSieben", "pro7", 4, 5, 3)
+        yield Entry("tvspielfilm", "RTL II", "rtl2", 4, 4, 3)
+        yield Entry("tvspielfilm", "VOX", "vox", 4, 4, 3)
+        yield Entry("tvspielfilm", "ARTE", "arte", 2, 2, 4)
+        yield Entry("tvspielfilm", "TELE 5", "tele5", 3, 3, 2)
+        yield Entry("tvspielfilm", "DMAX", "dmax", 3, 3, 2)
+
+    def minutes(self, newtime, endtime):
+        make = re.compile("(\d+):(\d+)")
+        new1 = make.match(newtime)
+        end1 = make.match(endtime)
+        if new1 and end1:
+            new = int(new1.group(1)) * 60 + int(new1.group(2))
+            end = int(end1.group(1)) * 60 + int(end1.group(2))
+            if end < new:
+                end += 24 * 60
+            if end < new:
+                return 0 # unreachable?
+            assert end > new
+            return end - new
+        return 0
+
+        

src/dvbcronrecording/templates/channelsconf_nav.html

         <li><a href="${href.recording('channelsconf/list/radio')}">${_('radio channels')}</a></li>
         <li><a href="${href.recording('channelsconf/list')}">${_('channel.conf show')}</a></li>
         <li>&nbsp;<a href="${href.recording('channeltuning/list')}">${_('tuning list')}</a></li>
+        <li><a href="${href.recording('programguide')}">${_('program guide')}</a></li>
   </ul>
   

src/dvbcronrecording/templates/programguide_list.html

+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+      xmlns:xi="http://www.w3.org/2001/XInclude">
+  <xi:include href="layout.html" />
+  <xi:include href="macros.html" />
+<head><title>${title}</title></head>
+<body>
+<div id="ctxtnav" class="nav">
+</div>
+  <xi:include href="channels_nav.html" />
+  <xi:include href="channelsconf_nav.html" />
+
+<div id="content" class="programguide">
+<h2>${title}</h2>
+ 
+<py:if test="message">
+  <div class="error">
+    ${message}
+  </div>
+</py:if>
+
+<table>
+<tr>
+<th>#</th>
+<th>programguide</th>
+<th>programchannel</th>
+<th>channelname</th>
+<th>extratime</th>
+<th>extrapercent</th>
+<th>priority</th>
+</tr>
+<tr py:for="item in datalist">
+  <td>${item.id}</td>
+  <td class="programguide"><select name="programguide">
+        <option py:for="name in sorted(programguides)" 
+                value="${name}" selected="${ (name == item.programguide) or None}">
+                ${name}
+        </option>
+      </select></td>
+  <td class="programchannel"><input type="text" name="programchannel" value="${item.programchannel}"></input></td>
+  <td class="channelname"><input type="text" name="channelname" value="${item.channelname}"></input></td>
+  <td class="extratime"><input type="text" name="extratime" value="${item.extratime}"></input></td>
+  <td class="extrapercent"><input type="text" name="extrapercent" value="${item.extrapercent}"></input></td>
+  <td class="priority"><select name="priority">
+        <option py:for="name in sorted(prioritynames)" 
+                value="${name}" selected="${ (name == item.priority) or None}">
+                ${prioritynames[name]}
+        </option>
+      </select>
+   </td>
+</tr>
+</table>
+  <xi:include href="paginator_datalist.html" />
+
+<h2>${userscriptstitle}</h2>
+<table>
+<tr>
+<th>#</th>
+<th>Installation</th>
+</tr>
+<tr py:for="item in userscripts">
+  <td><a href="${href(scripts_url)+'/'+item}">*</a></td>
+  <td><a href="${href(install_url)+'/'+item}">${item}</a></td>
+</tr>
+</table>
+</div>
+
+</body>
+</html>

src/dvbcronrecording/templates/recordinglist.html

     </form>
   </div>
   </table>
+  <div>
+    <div class="deleteold">
+      <a href="${href.recording('deleteold')}">${_('delete old singular recording times')}</a>
+     </div>
+  </div>
 </div>
 <script><!--
 function parentsUntil(node, selector) {
     'DvbCronRecording.channelsconf = dvbcronrecording.channelsconf',
     'DvbCronRecording.files = dvbcronrecording.files',
     'DvbCronRecording.tuning = dvbcronrecording.tuning',
+    'DvbCronRecording.programguide = dvbcronrecording.programguide',
   ]},
   keywords = 'dvb trac recorder',
   author = 'Guido Draheim',

src/trac-dvbcronrecording-plugin.spec

 # norootforbuild
-%define _version 0.4.18
+%define _version 0.4.19
 %define _name DvbCronRecording
 %define _pkg dvbcronrecording
 %{!?revision: %define revision 0 }