Guido Draheim avatar Guido Draheim committed 406b74d

import 0.4.13

Comments (0)

Files changed (54)

+
+syntax: regexp
+^src/build$
+syntax: regexp
+^src/RPMS$
+syntax: regexp
+^src/SOURCES$
+syntax: regexp
+^src/SRPMS$
+syntax: regexp
+^src/TracSimpleRecorder\.egg-info$
+syntax: glob
+*.egg-info
+syntax: regexp
+\.mo$
+syntax: regexp
+^build$
+syntax: regexp
+^rpm$
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>DvbCronRecording</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.python.pydev.PyDevBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.python.pydev.pythonNature</nature>
+	</natures>
+</projectDescription>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?eclipse-pydev version="1.0"?>
+
+<pydev_project>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6</pydev_property>
+<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
+<path>/DvbCronRecording/src</path>
+<path>/DvbCronRecording/src/tracsimplerecorder</path>
+</pydev_pathproperty>
+<pydev_pathproperty name="org.python.pydev.PROJECT_EXTERNAL_SOURCE_PATH">
+<path>/usr/lib/python2.6/site-packages</path>
+<path>/usr/lib64/python2.6</path>
+<path>/usr/lib64/python2.6/site-packages</path>
+</pydev_pathproperty>
+</pydev_project>

.settings/org.eclipse.core.resources.prefs

+#Sun Jul 10 11:46:17 CEST 2011
+eclipse.preferences.version=1
+encoding//src/dvbcronrecording/channels.py=utf-8
+encoding//src/dvbcronrecording/channelsconf.py=utf-8
+encoding//src/dvbcronrecording/core.py=utf-8
+encoding//src/dvbcronrecording/db/tsab.py=utf-8
+encoding//src/dvbcronrecording/init.py=utf-8
+encoding//src/dvbcronrecording/tuning.py=utf-8
+encoding//src/setup.py=utf8
+http://www.opensource.org/licenses/mit-license.html
+
+Copyright (c) 2011, Guido Draheim
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+PKG = dvbcronrecording
+SPECNAME = trac-$(PKG)-plugin
+
+prefix=/usr
+run: builds install
+all: builds install restart
+upd up: rpm update restart
+
+msgfmt:
+	for po in $(PKG)/*.po; do : \
+	; name=`basename "$$po" | sed -e "s|.po\$$||"` \
+	; lang=`echo "$$name" | sed -e "s|.*[.]||"` \
+	; file=`echo "$$name" | sed -e "s|[.][^.]*$$||"` \
+	; messages=$(PKG)/messages.$$lang.po \
+	; [ -s "$$messages" ] || messages="" \
+	; [ "$$file" = "messages" ] && messages="" \
+	; dirpath=$(PKG)/locale/$$lang/LC_MESSAGES \
+	; test -d $$dirpath || mkdir -p $$dirpath \
+	; echo msgfmt $$messages $$po -o $$dirpath/$$file.mo \
+	; msgfmt $$messages $$po -o $$dirpath/$$file.mo || exit 1 \
+	; done
+	
+builds: msgfmt
+	: python setup.py --help build 
+	python setup.py build --build-base=$(BUILDDIR)
+	
+install:
+	: python setup.py --help install 
+	sudo python setup.py build --build-base=$(BUILDDIR) \
+	    install --prefix=$(prefix) --root=/
+	
+clean:
+	sudo python setup.py clean --build-base=$(BUILDDIR)
+	
+tests unittest check:
+	python setup.py build --build-base=$(BUILDDIR) \
+	   test
+
+RPMROOT=$(dir $(shell pwd))/rpm
+BUILDDIR = $(RPMROOT)/build
+RPMTEMP = '--define=_tmpath $(RPMROOT)/tmp'
+RPMBUILD =  rpmbuild '--define=_topdir $(RPMROOT)' \
+                     '--define=_builddir $(BUILDDIR)' \
+                $(RPMBUILDOPTIONS)
+
+# setuptools sdist is broken for package_data in 0.6c11 vs python 2.6.5	
+dist: msgfmt
+	: python setup.py sdist --build-base=$(BUILDDIR) \
+	   --dist-dir SOURCES -v -v $X
+	test -d $(RPMROOT)/SOURCES || mkdir -p $(RPMROOT)/SOURCES
+	tar czvf $(RPMROOT)/SOURCES/$(PKG).tgz *.py *.txt *.spec $(PKG)/
+	ls -l $(RPMROOT)/SOURCES/$(PKG).tgz
+	@ version=`cat $(SPECNAME).spec | sed -e '/define _version/!d' -e 's/.*_version //'` \
+	; echo mv $(RPMROOT)/SOURCES/$(PKG).tgz $(RPMROOT)/SOURCES/$(PKG)-$$version.tgz \
+	;      mv $(RPMROOT)/SOURCES/$(PKG).tgz $(RPMROOT)/SOURCES/$(PKG)-$$version.tgz
+
+rpm: dist
+	test -d $(RPMROOT)/RPMS || mkdir $(RPMROOT)/RPMS
+	test -d $(RPMROOT)/SRPMS || mkdir $(RPMROOT)/SRPMS
+	test -d $(RPMROOT)/BUILD || mkdir $(RPMROOT)/BUILD
+	test -d $(RPMROOT)/SPECS || mkdir $(RPMROOT)/SPECS
+	- test -d $(BUILDDIR)/$(PKG) && rm -rf $(BUILDDIR)/*
+	cp $(SPECNAME).spec $(RPMROOT)/SPECS/$(SPECNAME).spec
+	$(RPMBUILD) -ba $(SPECNAME).spec
+
+update:
+	@ version=`cat $(SPECNAME).spec | sed -e '/define _version/!d' -e 's/.*_version *//'` \
+	; package=`cat $(SPECNAME).spec | sed -e '/Name:/!d' -e 's/Name: *//'` \
+	; echo rpm -U --force $(RPMROOT)/RPMS/noarch/$$package-$$version*.noarch.rpm \
+	;      rpm -U --force $(RPMROOT)/RPMS/noarch/$$package-$$version*.noarch.rpm
+	   
+trac restart:
+	sudo /sbin/service tracd restart
+* clear bug on recordinglist (non-latin chars?)
+* fallback für recordinguser sollte "wwwrun" sein
+  oder was immer gerade als prozess läuft, damit
+  auch gelöscht werden kann.
+  

src/documentation.txt

+INTRODUCTION
+------------
+
+This TracPlugin maintains a database of recordings.
+Upon "Activate!" it creates a recording script. 
+The actual recording is performed by a tuning the
+LinuxDVB device started by a CronTab call.
+
+There are a few extensions like detection of overlap,
+the definition of secondary recording times to use,
+and a few other tricks. Having a web UI eases the
+handling considerably.
+
+
+GENESIS
+-------
+
+The system grew out of some local command-line scripts
+that would convert a list of recording times, channel
+info and program title into a matching recording script
+along with a CronTab entry. Transforming it into a
+TracPlugin allows for easier installation on a remote
+recorder box as well as sharing the recorder by 
+multiple persons with just a web access to the recorder
+box.
+
+This script / plugin is in no way comparable to the
+advanced multi-media stations like MythTV and LinVDR
+but on the other hand it is far easier to install.
+Especially the remote setup (backend setup) of the
+mentioned solutions is not quite easy - their code
+assumes that the recorder box is in the living room
+whereas I would assume the recorder box to be in some
+server room where its noise does not interfere with living.
+
+Obviously, I don't watch TV very often and sadly the
+most interesting programs run at times where I have
+better things to do. But a CronEntry can help with that.
+
+TODO
+-----
+
+* only tested with DVB-S (satellite), it is probably
+  not easy to be used for other digital broadcasting types. 
+* the code is prepared to tune multiple cards but that
+  feature is broken upon first release 
+* the code is prepared for localization but while it
+  speaks English in the code the user interface is
+  stuck on German. Sorry.
+ 
+So, I could really need some help with this - it is
+feature complete for my own usage but if you need 
+another feature done then please don't ask - just send
+patches (or "pull requests" pointing to your public code).
+I publish this code so that you have a good start for
+your own ideas.
Add a comment to this file

src/dvbcronrecording/__init__.py

Empty file added.

src/dvbcronrecording/channels.de.po

+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Report-Msgid-Bugs-To: guidod@gmx.de\n"
+"Plural-Forms: nplurals=3; plural=(n==0 ? 0 : (n == 1 ? 1 : 2));\n"
+
+msgid "insert"
+msgstr "einfügen"
+
+msgid "delete"
+msgstr "[loeschen]"
+
+msgid "update"
+msgstr "aktualisieren"
+
+msgid "play"
+msgstr "Abspielen"
+
+msgid "mplay"
+msgstr "mit MPlayer"
+
+
+msgid "adapter"
+msgstr "Adapter"
+
+msgid "channel.conf title"
+msgstr "Bezeichnung in der channel.conf"
+
+msgid "channel.conf show"
+msgstr "channel.conf Ansicht"
+
+msgid "action buttons"
+msgstr "Betriebsschalter"
+
+msgid "status"
+msgstr "Status"
+
+msgid "NEW"
+msgstr "NEU"
+
+msgid "SAVE"
+msgstr "SPEICHERN"
+
+msgid "newchannel"
+msgstr "neusender"
+
+msgid "existing channel.conf entry"
+msgstr "existierender channel.conf Eintrag"
+
+msgid "please do not use umlauts"
+msgstr "Bitte keine Umlaute verwenden"
+
+msgid "-default-channels"
+msgstr "3sat,arte"

src/dvbcronrecording/channels.py

+# -*- coding: utf-8 -*-
+
+import pkg #@UnresolvedImport
+# from pkg_resources import resource_filename
+import re
+import logging
+
+from trac.core import Component, implements
+from trac.web.chrome import add_stylesheet, add_script
+
+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
+from tuning import tuning_adapter_list
+
+PACKAGE = 'dvbcronrecording'
+NAV = 'recordings'
+URL = 'recording'
+SUBURL = 'channels'
+
+CHANNELS_VIEW = 'DVBREC_CHANNELS_VIEW'
+CHANNELS_EDIT = 'DVBREC_CHANNELS_EDIT'
+
+DEBUG = False
+
+logg = logging.getLogger("DvbCronRecordingChannelsPlugin")
+if DEBUG:
+    logg.addHandler(logging.FileHandler("/tmp/DvbCronRecordingChannelsPlugin.log"))
+    logg.setLevel(logging.DEBUG)
+
+from db import schema
+def affinity(table_name, column_name):
+    global tables
+    for table in schema.tables:
+        if table.name == table_name:
+            for column in table.columns:
+                if column.name == column_name:
+                    return column.type
+    return None
+def aff(value, table_name, column_name):
+    if value is None: return None
+    typed = affinity(table_name, column_name)
+    if typed in [ "int", "INT", "integer", "INTEGER"]:
+        return intnull(value)
+    return ustr(value)
+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))
+
+class MakefileEntry: pass
+class FileEntry: pass
+
+class ChainEntry:
+    def __init__(self):
+        self.entry = None
+        self.after = []
+        self.message = ""
+
+"""
+  here we are.
+"""
+class DvbCronRecordingChannelsPlugin(Component):
+
+    #
+    # Public methods
+    #
+
+    implements(IPermissionRequestor, ITemplateProvider,  IRequestHandler)
+    
+    # IPermissionRequestor methods
+
+    """
+      Returns list of permitions privided by this plugin.
+    """
+    def get_permission_actions(self):
+        return [CHANNELS_VIEW, CHANNELS_EDIT ]
+
+    # 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/%s(?:/(.*))?$" % (URL, SUBURL), req.path_info)
+        if m:
+            page = m.group(1)
+            req.args['page'] = page 
+            return True
+        return False
+
+    """
+      Handles display, append and delete requests on this plugin.
+    """
+    def process_request(self, req):
+        req.perm.assert_permission(CHANNELS_VIEW)
+        translate = Translate("channels", req.locale)
+
+        # getting cursor
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        page = req.args['page']
+        if not page: page = "list"
+        
+        # ------------------------------------------------
+        message = ""
+        
+        if page in [ None, '', 'list' ]:
+            pass
+        if page in [ 'newentry', 'append' ]:
+            message = self._channel_append(cursor, req)
+        if page in [ 'update' ]:
+            message = self._channel_update(cursor, req)
+        if page in [ 'delete' ]:
+            message = self._channel_delete(cursor, req)
+        
+        # database commit and return page content
+        db.commit()
+
+        add_stylesheet(req, 'common/css/wiki.css')
+        add_stylesheet(req, PACKAGE+'/css/recordingchannels.css')
+        add_script(req, 'common/js/trac.js')
+        add_script(req, 'common/js/wikitoolbar.js')
+        
+        if not message: message = translate("please do not use umlauts")
+        message += translate("?problem")
+        newchannel_list = self._newchannel_list(cursor, req)
+        channel_list = self._channel_list(cursor, req)
+
+        # passing variables to template
+        data = {}
+        data['message'] = message
+        data['title'] = translate('Channel List')
+        data['adapterlist'] = self._adapter_list(cursor, req)
+        data['new_datalist'] = newchannel_list
+        data["_pagenum"] = req.args.get("_pagenum", "0")
+        data["_pagesize"] = req.args.get("_pagesize", "10")
+        data['datalist'] = Paginator(channel_list, int(data["_pagenum"]), int(data["_pagesize"]))
+        data['appends'] = req.perm.has_permission(CHANNELS_VIEW);
+        data['author'] = req.authname or ""
+        data['_'] = translate
+
+        return ('channels_list.html', data, None)
+
+    # ====================================================================
+    
+    # ====================================================================
+    
+    # ====================================================================
+        
+    #
+    # Private methods
+    #
+    
+    """
+      Returns list of the adapter settings
+    """
+    def _adapter_list(self, cursor, req, defaults = [ 0 ]):
+        return tuning_adapter_list(cursor, defaults)
+    
+
+    def _newchannel_list(self, cursor, req):
+        translate = Translate("channels", req.locale)
+        _ = ['channelname', 'adapter', 'title']
+        defs = {}
+        defs["channelname"] = translate("newchannel")
+        defs["adapter"] = "0"
+        defs["title"] = translate("existing channel.conf entry")
+        return [ defs ]
+
+    """
+      Returns list of the channels
+    """
+    def _channel_list(self, cursor, req):
+        return channel_list(cursor, req.args)
+
+    """
+      Appends a new entry into channel list
+    """
+    def _channel_append(self, cursor, req):
+        cols = ['channelname', 'adapter', 'title']
+        vals = [] # { "author" : req.authname or 'anonymous' }
+        for col in cols:
+            if req.args.has_key(col):
+                vals += [ aff(req.args[col], "recording_channels", col) ]
+            else:
+                vals += [ None ]
+        sql = "INSERT INTO recording_channels (%s)" % ",".join(cols)
+        sql += " VALUES (%s)" % (",".join(["%s"] * len(cols)))
+        cursor.execute(sql, vals)
+        return "OK"
+
+    """
+      Updates an entry in the channel list
+    """
+    def _channel_update(self, cursor, req):
+        keys = ['id']
+        cols = ['channelname', 'adapter', 'title']
+        vals = [] # { "author" : req.authname or 'anonymous' }
+        for col in cols:
+            if req.args.has_key(col):
+                vals += [ aff(req.args[col], "recording_channels", col) ]
+            else:
+                vals += [ None ] 
+        where = []
+        for col in keys:
+            if req.args.has_key(col):
+                where += [ aff(req.args[col], "recording_channels", col) ]
+            else:
+                where += [ None ]
+        sqlX = ""
+        try:
+            sql = "UPDATE recording_channels " 
+            sql += " SET "+(",".join([" %s = %%s" % col for col in cols]))
+            sql += " WHERE " + (" AND ".join([" %s = %%s" % col for col in keys ]))
+            sqlX = sql + " | " + str( vals + where )
+            cursor.execute(sql, vals + where)
+        except Exception, e:
+            return "ERROR: %s\n%s" % (str(e), sqlX)
+        return "OK"
+
+    """
+      Deletes entry from recordings
+    """
+    def _channel_delete(self, cursor, req):
+        keys = ['id']
+        key_vals = []
+        for col in keys:
+            if req.args.has_key(col):
+                key_vals += [ aff(req.args[col], "recording_channels", col) ]
+            else:
+                key_vals += [ None ]
+        key_sql = " WHERE " + (" AND ".join([" %s = %%s" % col for col in keys ]))
+        cursor.execute("DELETE FROM recording_channels "+key_sql, key_vals)
+
+"""
+  Returns list of the channels
+"""
+def channel_list(cursor, args = {}):
+    view = [ "channelname", "adapter" ]
+    cols = ['id', 'channelname', 'adapter', 'title']
+    order = ["channelname", "title", "adapter"]
+    sql = "SELECT %s FROM recording_channels" % (",".join(cols))
+    order_sql = " ORDER BY %s" % (",".join(order))
+    view_vals = []
+    view_cols = []
+    for col in view:
+        if args.has_key(col):
+            view_vals += [ aff(args[col], "recording_channels", col) ]
+            view_cols += [ col ]
+    if view_cols:
+        view_sql = " WHERE " + (" AND ".join([" %s = %%s" % col for col in view_cols ]))
+        cursor.execute(sql + view_sql + order_sql, view_vals)
+    else:
+        cursor.execute(sql + order_sql)
+    entries = []
+    for entry in cursor:
+        entry = dict(zip(cols, entry))
+        entries.append(entry)
+    return entries
+
+
+"""
+  Returns list of the adapter settings
+"""
+def channelname_list(cursor, req, defaults = None):
+    if defaults is None:
+        translate = Translate("channels", req.locale)
+        _defaults = translate.get("-default-channels", "3sat")
+        defaults = [ item.strip() for item in _defaults.split(",") ]
+    cols = ['channelname']
+    sql = "SELECT DISTINCT %s FROM recording_channels" % (",".join(cols))
+    sql += " ORDER BY "+(",".join(cols))
+    cursor.execute(sql)
+    entries = []
+    for entry in cursor:
+        entries.append(entry[0])
+    for channelname in defaults:
+        if channelname not in entries:
+            entries.append(channelname)
+    return entries
+    
+class channelconfitem: pass
+def channelname_to_channelconflist(cursor, req, channelname):
+    """ input is a channelname and the result is an object list with
+        attributes adapter + channel title. A tuning operation
+        will usually specify the channel.conf for the adapter
+        along with the zap entry name to be used from that file."""
+    logg.debug("resolving %s" % channelname)
+    for entry in channel_list(cursor, { "channelname" : channelname }):
+        logg.debug("   found %s" % entry["title"])
+        logg.debug("      at %s\n" % entry["adapter"])
+        item = channelconfitem()
+        item.adapter = entry["adapter"]
+        item.title = entry["title"]
+        item.channel = entry["title"]  
+        yield item
+

src/dvbcronrecording/channelsconf.de.po

+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Report-Msgid-Bugs-To: guidod@gmx.de\n"
+"Plural-Forms: nplurals=3; plural=(n==0 ? 0 : (n == 1 ? 1 : 2));\n"
+
+msgid "insert"
+msgstr "einfuegen"
+
+msgid "delete"
+msgstr "loeschen"
+
+msgid "update"
+msgstr "aktualisieren"
+
+msgid "takeover"
+msgstr "uebernehmen"
+
+msgid "play"
+msgstr "Abspielen"
+
+msgid "mplay"
+msgstr "mit MPlayer"
+
+msgid "channel.conf title"
+msgstr "Bezeichnung in der channel.conf"
+
+msgid "channel.conf show"
+msgstr "channel.conf Ansicht"
+
+msgid "action"
+msgstr "vorgehen"
+
+msgid "adapter"
+msgstr "Adapter"
+
+msgid "frequency"
+msgstr "Frequenz"
+
+msgid "polarity"
+msgstr "Polarität"
+
+msgid "source"
+msgstr "Quelle"
+
+msgid "symbolrate"
+msgstr "SymbolRate"
+
+msgid "vpid"
+msgstr "V-pid"
+
+msgid "apid"
+msgstr "A-pid"
+
+msgid "tpid"
+msgstr "T-pid"
+
+msgid "action buttons"
+msgstr "Betriebsschalter"
+
+msgid "status"
+msgstr "Status"
+
+msgid "NEW"
+msgstr "NEU"
+
+msgid "SAVE"
+msgstr "SPEICHERN"
+
+msgid "please do not use umlauts"
+msgstr "Bitte keine Umlaute verwenden"
+
+msgid "use the traditional dvbscan/zap syntax"
+msgstr "Nutze die uebliche dvbscan/zap Syntax"
+
+msgid "have NOT checked it!"
+msgstr "wurde NICHT überprüft!"
+
+msgid "have checked it!"
+msgstr "wurde überprüft!"
+
+msgid "-new-channelsconf"
+msgstr "NEW CHANNEL"
+
+msgid "-new-frequency"
+msgstr "12000"
+
+msgid "-new-polarity"
+msgstr "H"
+
+msgid "-new-symbolrate"
+msgstr "27500"
+
+msgid "Saved"
+msgstr "Abgespeichert"
+
+msgid "Edit"
+msgstr "Editor"
+
+msgid "Takeover"
+msgstr "Übernommen"
+

src/dvbcronrecording/channelsconf.py

+# -*- coding: utf-8 -*-
+
+import pkg #@UnresolvedImport
+import re
+from urllib import urlencode
+from urllib2 import quote as quote2
+
+import logging
+
+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
+from tuning import tuning_adapter_list
+
+PACKAGE = u'dvbcronrecording'
+NAV = u'recordings'
+URL = u'recording'
+SUBURL = u'channelsconf'
+
+CHANNELSCONF_VIEW = 'DVBREC_CHANNELSCONF_VIEW'
+CHANNELSCONF_EDIT = 'DVBREC_CHANNELSCONF_EDIT'
+
+DEBUG = False
+
+from db import schema
+def affinity(table_name, column_name):
+    global tables
+    for table in schema.tables:
+        if table.name == table_name:
+            for column in table.columns:
+                if column.name == column_name:
+                    return column.type
+    return None
+def aff(value, table_name, column_name):
+    if value is None: return None
+    typed = affinity(table_name, column_name)
+    if typed in [ "int", "INT", "integer", "INTEGER"]:
+        return intnull(value)
+    return ustr(value)
+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))
+def q(text):
+    return unicode(quote2(unicode(str(text)).encode("utf-8")))
+    # return text
+    # return unicode(quote_plus(unicode(str(text))))
+
+logg = logging.getLogger("DvbCronRecordingChannelsConfPlugin")
+if DEBUG:
+    logg.addHandler(logging.FileHandler("/tmp/DvbCronRecordingChannelsConfPlugin.log"))
+    logg.setLevel(logging.DEBUG)
+
+polaritylist = { "H" : "H", "V" : "V", "lr" : "LR", "hr" : "HR" }
+
+"""
+  here we are.
+"""
+class DvbCronRecordingChannelsConfPlugin(Component):
+
+    #
+    # Public methods
+    #
+
+    implements(IPermissionRequestor, ITemplateProvider,  IRequestHandler)
+
+    # IPermissionRequestor methods
+
+    """
+      Returns list of permitions privided by this plugin.
+    """
+    def get_permission_actions(self):
+        return [CHANNELSCONF_VIEW, CHANNELSCONF_EDIT ]
+
+    # 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/%s(?:/(.*))?$" % (URL, SUBURL), req.path_info)
+        if m:
+            page = m.group(1)
+            req.args['page'] = page 
+            return True
+        return False
+
+    """
+      Handles display, append and delete requests on this plugin.
+    """
+    def process_request(self, req):
+        req.perm.assert_permission(CHANNELSCONF_VIEW)
+        translate = Translate("channelsconf", req.locale)
+
+        # getting cursor
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        page = req.args['page']
+        if not page: page = "show"
+        if page in [ "save" ]:
+            text = req.args["text"]
+            result = self.savetext(cursor, req, text)
+            db.commit()
+            data = {}
+            data['title'] = translate("Saved")
+            data['messages'] = ['(%s)' % text, result]
+            data['adapterlist'] = self._adapter_list(cursor, req)
+            data['text'] = self.loadtext(cursor, req)
+            data['_'] = translate
+            add_stylesheet(req, PACKAGE+'/css/recordingchannelsconf.css')
+            add_script(req, 'common/js/trac.js')
+            return ('channelsconf_editor.html', data, None)
+        if page in [ "edit" ]:
+            data = {}
+            data['title'] = translate("Edit")
+            data['messages'] = [ translate('use the traditional dvbscan/zap syntax') ]
+            data['adapterlist'] = self._adapter_list(cursor, req)
+            data['text'] = self.loadtext(cursor, req)
+            data['_'] = translate
+            add_stylesheet(req, PACKAGE+'/css/recordingchannelsconf.css')
+            add_script(req, 'common/js/trac.js')
+            return ('channelsconf_editor.html', data, None)
+        if page in [ "take", "takeover" ]:
+            channels = self._channelsconf_selectby(cursor, req, ['adapter', 'title'])
+            data = {}
+            data['title'] = translate("Takeover")
+            data['messages'] = [ "" ]
+            data['adapterlist'] = self._adapter_list(cursor, req)
+            data["_pagenum"] = req.args.get("_pagenum", "0")
+            data["_pagesize"] = req.args.get("_pagesize", "10")
+            data['datalist'] = Paginator(channels, int(data["_pagenum"]), int(data["_pagesize"]))
+            data['_'] = translate
+            data['urlencode'] = urlencode
+            data['q'] = q
+            add_stylesheet(req, PACKAGE+'/css/recordingchannelsconf.css')
+            add_script(req, 'common/js/trac.js')
+            return ('channelsconf_take.html', data, None)
+            
+        
+        # ------------------------------------------------
+        message = ""
+        
+        if page in [ 'show']:
+            for col in ['apid', 'vpid']:
+                req.args[col+"notnull"] = u"checked"
+            message = translate("have checked it!") + req.args["vpidnotnull"] + " " + str(req.args.has_key("vpidnotnull"))
+        else:
+            message = translate("have NOT checked it!")
+        if page in [ None, '', 'list' ]:
+            pass
+        if page in [ 'newentry', 'append' ]:
+            message = self._channelsconf_append(cursor, req)
+        if page in [ 'update' ]:
+            message = self._channelsconf_update(cursor, req)
+        if page in [ 'delete' ]:
+            message = self._channelsconf_delete(cursor, req)
+        
+        # database commit and return page content
+        db.commit()
+
+        add_stylesheet(req, 'common/css/wiki.css')
+        add_stylesheet(req, PACKAGE+'/css/recordingchannelsconf.css')
+        add_script(req, 'common/js/trac.js')
+        add_script(req, 'common/js/wikitoolbar.js')
+        
+        if not message: message = translate("please do not use umlauts")
+        message += translate("?problem")
+        
+        newchannels_list = self._newchannels_list(cursor, req)
+        channels_list = self._channelsconf_list(cursor, req)
+
+        # passing variables to template
+        data = {}
+        data['message'] = message
+        data['title'] = translate('Channel List')
+        data['polaritylist'] = polaritylist
+        data['adapterlist'] = self._adapter_list(cursor, req)
+        data['new_datalist'] = newchannels_list
+        data["_pagenum"] = req.args.get("_pagenum", "0")
+        data["_pagesize"] = req.args.get("_pagesize", "10")
+        data['datalist'] = Paginator(channels_list, int(data["_pagenum"]), int(data["_pagesize"]))
+        data['appends'] = req.perm.has_permission(CHANNELSCONF_VIEW);
+        data['author'] = req.authname or ""
+        data['_'] = translate
+        data['urlencode'] = urlencode
+        data['q'] = q
+
+        return ('channelsconf_list.html', data, None)
+
+    # ====================================================================
+    
+    # ====================================================================
+    
+    # ====================================================================
+        
+    #
+    # Private methods
+    #
+
+    """
+      Returns list of the adapter settings
+    """
+    def _adapter_list(self, cursor, req, defaults = [ 0 ]):
+        return tuning_adapter_list(cursor, defaults)
+    
+    """
+      Returns list of the channels.conf
+    """
+    def _channelsconf_selectby(self, cursor, req, selectby = ['adapter', 'title']):
+        view = selectby
+        cols = ['adapter', 'title','frequency','polarity','source','symbolrate','vpid', 'apid', 'tpid']
+        sql = "SELECT %s FROM recording_channelsconf" % (",".join(cols))
+        order_sql = " ORDER BY title, adapter, source LIMIT 11"
+        view_vals = []
+        view_cols = []
+        for col in view:
+            if req.args.has_key(col):
+                view_vals += [ aff(req.args[col], "recording_channelsconf", col) ]
+                view_cols += [ col ]
+        if view_cols:
+            view_sql = " WHERE " + (" AND ".join([" %s = %%s" % col for col in view_cols ]))
+            logg.info("sql = %s", sql + view_sql)
+            cursor.execute(sql + view_sql + order_sql, view_vals)
+        else:
+            logg.info("sql = %s;", sql + order_sql)
+            cursor.execute(sql)
+        entries = []
+        for entry in cursor:
+            entry = dict(zip(cols, entry))
+            entries.append(entry)
+        return entries
+    
+    def _newchannels_list(self, cursor, req):
+        _ = ['adapter', 'title','frequency','polarity','source','symbolrate','vpid', 'apid', 'tpid']
+        translate = Translate("channelsconf", req.locale)
+        defs = {}
+        defs["adapter"] = "0"
+        defs["title"] = translate.get("-new-channel", "NEW CHANNEL")
+        defs["frequency"] = translate.get("-new-frequency", "12000")
+        defs["polarity"] = translate.get("-new-polarity", "H")
+        defs["source"] = translate.get("-new-source", "0")
+        defs["symbolrate"] = translate.get("-new-symbolrate", "27500")
+        defs["vpid"] = translate.get("-new-vpid", "100")
+        defs["apid"] = translate.get("-new-apid", "101")
+        defs["tpid"] = translate.get("-new-tpid", "100")
+        return [ defs ]
+
+    """
+      Returns list of the channels.conf
+    """
+    def _channelsconf_list(self, cursor, req):
+        notnull = ['apid', 'vpid']
+        view = ['adapter']
+        cols = ['adapter', 'title','frequency','polarity','source','symbolrate','vpid', 'apid', 'tpid']
+        sql = "SELECT %s FROM recording_channelsconf" % (",".join(cols))
+        order_sql = " ORDER BY title, adapter, source"
+        notnull_cols = []
+        for col in notnull:
+            if req.args.has_key(col+"notnull") and req.args[col+"notnull"]:
+                notnull_cols += [ col ]
+        view_vals = []
+        view_cols = []
+        for col in view:
+            if req.args.has_key(col):
+                view_vals += [ aff(req.args[col], "recording_channelsconf", col) ]
+                view_cols += [ col ]
+        if view_cols:
+            view_sql = " WHERE " + (" AND ".join([" %s = %%s" % col for col in view_cols ]))
+            if notnull_cols:
+                view_sql += " AND "+ (" AND ".join([" %s > 0" % col for col in notnull_cols ])) 
+            logg.info("sql = %s", sql + view_sql)
+            cursor.execute(sql + view_sql + order_sql, view_vals)
+        else:
+            view_sql = ""
+            if notnull_cols:
+                view_sql += " WHERE "+ (" AND ".join([" %s > 0" % col for col in notnull_cols ])) 
+            logg.info("sql = %s;", sql + order_sql)
+            cursor.execute(sql + view_sql + order_sql)
+        entries = []
+        for entry in cursor:
+            entry = dict(zip(cols, entry))
+            entries.append(entry)
+        return entries
+
+    """
+      Appends a new entry into channels.conf list
+    """
+    def _channelsconf_append(self, cursor, req):
+        cols = ['adapter', 'title','frequency','polarity','source','symbolrate','vpid', 'apid', 'tpid']
+        vals = [] # { "author" : req.authname or 'anonymous' }
+        for col in cols:
+            if req.args.has_key(col):
+                vals += [ aff(req.args[col], "recording_channel", col) ]
+            else:
+                vals += [ None ]
+        sql = "INSERT INTO recording_channelsconf (%s)" % ",".join(cols)
+        sql += " VALUES (%s)" % (",".join(["%s"] * len(cols)))
+        cursor.execute(sql, vals)
+        return "OK"
+
+    """
+      Updates an entry in the channels.conf list
+    """
+    def _channelsconf_update(self, cursor, req):
+        keys = ['adapter', 'title']
+        cols = ['frequency','polarity','source','symbolrate','vpid', 'apid', 'tpid']
+        rename = { "newtitle" : "title" }
+        vals = [] # { "author" : req.authname or 'anonymous' }
+        for col in cols:
+            if req.args.has_key(col):
+                vals += [ aff(req.args[col], "recording_channelsconf", col) ]
+            else:
+                vals += [ None ] 
+        where = []
+        for col in keys:
+            if req.args.has_key(col):
+                where += [ aff(req.args[col], "recording_channelsconf", col) ]
+            else:
+                where += [ None ]
+        for col in rename:
+            if req.args.has_key(col):
+                col2 = rename[col]
+                vals += [ aff(req.args[col2], "recording_channelsconf", col) ]
+                cols += col2
+        sqlX = ""
+        try:
+            sql = "UPDATE recording_channelsconf " 
+            sql += " SET "+(",".join([" %s = %%s" % col for col in cols]))
+            sql += " WHERE " + (" AND ".join([" %s = %%s" % col for col in keys ]))
+            sqlX = sql + " | " + str( vals + where )
+            cursor.execute(sql, vals + where)
+        except Exception, e:
+            return "ERROR: %s\n%s" % (str(e), sqlX)
+        return "OK"
+
+    """
+      Deletes entry from recordings
+    """
+    def _channelsconf_delete(self, cursor, req):
+        keys = ['adapter', 'title']
+        key_vals = []
+        for col in keys:
+            if req.args.has_key(col):
+                key_vals += [ aff(req.args[col], "recording_channelsconf", col) ]
+            else:
+                key_vals += [ None ]
+        key_sql = " WHERE " + (" AND ".join([" %s = %%s" % col for col in keys ]))
+        cursor.execute("DELETE FROM recording_channelsconf "+key_sql, key_vals)
+
+
+    # ---------------------------------------------------------------
+    def savetext(self, cursor, req, text):
+        if not req.args.has_key("adapter"):
+            return "no adapter given in update"
+        adapter = req.args["adapter"]
+        sql = "DELETE FROM recording_channelsconf WHERE adapter = %s"
+        cursor.execute(sql, adapter)
+        msg = []
+        for line in text.split("\n"):
+            msg += [ self.savetextline(cursor, req, unicode(line)) ]
+        return "(%s lines = %s)" % (len(msg), str(msg))
+    def savetextline(self, cursor, req, line):
+        cols = ["title", "frequency", "polarity", "source", "symbolrate", "vpid","apid","tpid"]
+        # VIVA PLUS:12551:v:0:22000:171:172:12120
+        m = re.match(r"([^:]+):([^:]+):([^:]+):([^:]+):([^:]+):([^:]+):([^:]+):([^:]+)", line)
+        if m:
+            vals = m.groups()
+            for n in xrange(len(cols)):
+                if n < len(vals):
+                    val = vals[n]
+                    if val: val = val.strip()
+                    req.args[ cols[n] ] = val
+            return self._channelsconf_append(cursor, req)
+        else:
+            return "NO."
+    def loadtext(self, cursor, req):
+        return "\n".join(list(self.loadtextlines(cursor, req)))
+    def loadtextlines(self, cursor, req):
+        cols = ["title", "frequency", "polarity", "source", "symbolrate", "vpid","apid","tpid"]
+        entries = self._channelsconf_list(cursor, req)
+        for entry in entries:
+            vals = []
+            for col in cols:
+                if col in entry:
+                    vals += [ ustr(entry[col]) ]
+                else:
+                    vals += ""
+            yield (":".join(vals))

src/dvbcronrecording/computer.py

+import time
+import re
+import datetime
+
+import logging
+
+DEBUG = True
+
+logg = logging.getLogger("DvbCronRecordingComputer")
+if DEBUG:
+    logg.addHandler(logging.FileHandler("/tmp/DvbCronRecordingComputer.log"))
+    logg.setLevel(logging.DEBUG)
+
+onlydate_rank = 0.5 # +1
+rankup_channels = [ "3sat", "arte" ]
+rankup_plus = 0.3
+
+def intnull(value, default = None):
+    if value is None: return default
+    try: return int(value)
+    except Exception: return default
+
+class ChainEntry:
+    def __init__(self):
+        self.entry = None
+        self.after = []
+        self.message = ""
+
+class RecorderItem:
+    """ converting the database entry - which is similar to what
+        the user entered in the input form fields - to an object
+        with computable data attributes. Note that newtime/endtime
+        are given as minutes-since-midnight. """
+    def __init__(self):
+        self.entry = None # the original entry
+        self.weekday = None
+        self.onlydate = None
+        self.onlyday = None
+        self.months = []
+        self.newtimeMMM = 0
+        self.endtimeMMM = 0
+        self.extratimeM = 0
+        self.channelname = None
+        self.title = ""
+        self.message = ""
+        self.rank = 0.0
+        self.cloned = None
+        self.tuning_channel = None
+        self.tuning_adapter = None
+    def clone(self):
+        item = RecorderItem()
+        item.entry = self.entry
+        item.weekday = self.weekday
+        item.onlydate = self.onlydate
+        item.onlyday = self.onlyday
+        item.months = self.months
+        item.newtimeMMM = self.newtimeMMM
+        item.endtimeMMM = self.endtimeMMM
+        item.extratimeM = self.extratimeM
+        item.channelname = self.channelname
+        item.title = self.title
+        item.message = self.message
+        item.rank = self.rank 
+        item.cloned = self
+        item.tuning_channel = self.tuning_channel
+        item.tuning_adapter = self.tuning_adapter
+        return item
+    def __unicode__(self):
+        return u"(%s) '%s'" % (unicode(self.months), unicode(self.title))
+    
+class RecorderGroup:
+    def __init__(self):
+        self.items = [] # RecordingItem
+        self.newtimeMMM = None
+        self.endtimeMMM = None
+        self.extratimeM = None
+    def __unicode__(self):
+        return u"["+unicode(",".join([unicode(item) for item in self.items]))+"]"
+
+class RecorderGroupPlan:
+    def __init__(self):
+        self._groups = []
+    def items(self):
+        groupcount = 0
+        for group in self._groups:
+            if not group.items: continue
+            for item in group.items:
+                item.groupcount = groupcount
+                yield item
+            groupcount += 1
+            yield None # group seperator
+    def groups(self):
+        for group in self._groups:
+            yield group
+    def addgroup(self, group):
+        self._groups += [ group ]
+
+class RecorderFlatPlan:
+    def __init__(self):
+        self._items = []
+    def items(self):
+        return self._items
+    def additem(self, item):
+        self._items += [ item ]
+    def deleted_items(self):
+        for item in self._items:
+            if item.newtimeMMM == item.endtimeMMM:
+                yield item
+
+class RecorderTime:
+    def __init__(self, orig = None):
+        self.item = orig
+        self.message = ""
+        self.datetimeX = None
+        self.datetimeY = None
+        self.deleted = False
+        self.newtimeMMM = 0
+        self.endtimeMMM = 0
+        self.extratimeM = 0
+        self.set(orig)
+    def set(self, orig):
+        if orig is not None:
+            self.newtimeMMM = orig.newtimeMMM
+            self.endtimeMMM = orig.endtimeMMM
+            self.extratimeM = orig.extratimeM
+        return self
+
+months_ahead = 3
+    
+class TracSimpleRecorderComputer:
+    def zero(self, spec):
+        if not spec: return 0
+        return int(spec)
+    
+    """ 
+        This is doing the actual computations
+    """
+    def _entries_to_startchain(self, entries):
+        weekday1 = None
+        endtime1 = None
+        extratime1 = None
+        start = []
+        chain = None
+        for entry in entries:
+            if entry["status"] == "no": continue
+            newtime2 = self.zero(self.minutes(entry["newtime"]))
+            endtime2 = self.zero(self.minutes(entry["endtime"]))
+            extratime2 = self.zero(self.minutes(entry["extratime"]))
+            weekday2 = intnull(entry["weekday"])
+            if endtime2 < newtime2: endtime2 += 24*60
+            if endtime2 == endtime1: continue
+            isafter = False
+            if endtime1 is not None:
+                if not weekday1 : weekday1 = 0
+                if not weekday2 : weekday2 = 0
+                daytime1 = weekday1 * 24 * 60
+                daytime2 = weekday2 * 24 * 60
+                message = " [%s < %s]" % (daytime1+endtime1+extratime1, daytime2+newtime2)
+                if daytime1+endtime1+extratime1 >= daytime2+newtime2:
+                    isafter = True
+            else:
+                message = " [FIRST] "
+            if chain: chain.message += message
+            if isafter:
+                chain.after += [ entry ]
+            else:
+                if chain is not None:
+                    start += [ chain ] 
+                chain = ChainEntry()
+                chain.entry = entry
+                chain.after = []
+            weekday1 = weekday2
+            endtime1 = endtime2
+            extratime1 = extratime2
+        return start
+
+
+    def minutes(self, spec):
+        """ convert HH:MM spec into minutes (since midnight if not an interval) """
+        m = re.match("(\d+):(\d+)", spec)
+        if m: return int(m.group(1))*60 + int(m.group(2))
+        m = re.match("(\d+)", spec)
+        if m: return int(m.group(1))
+        return None
+    
+    def get_allowed_months(self, ahead = months_ahead):
+        this_month = time.localtime().tm_mon
+        allowed = []
+        for add_months in xrange(months_ahead):
+            month = this_month + add_months
+            if month > 12: month -= 12
+            if month > 12: continue
+            if month not in allowed:
+                allowed += [ month ]
+        return allowed
+    
+    def get_past_days_this_month(self):
+        today = time.localtime()
+        this_month = today.tm_mon
+        this_day = today.tm_mday
+        allowed = []
+        for day in xrange(1, this_day):
+            allowed += [ (day, this_month) ]
+        return allowed
+    
+    """
+       check this month and the next 3 months whether there are
+       entries with 'onlydate' that would only run once.
+    """
+    def recordinglist_to_recorderitems(self, entries, ahead = months_ahead):
+        allowed_months = self.get_allowed_months(ahead)
+        past_days = self.get_past_days_this_month()
+        logg.info("allowed_months %s" % (str(allowed_months)))    
+        for entry in entries:
+            if entry["status"] == "no": 
+                logg.debug("status disabled for %s", entry["title"]) 
+                continue
+            item = RecorderItem()
+            item.channelname = entry["channelname"]
+            item.title = entry["title"]
+            if not item.title:
+                logg.debug("item had not title") 
+                continue            
+            if not item.channelname: 
+                logg.debug("no channel name for '%s", item.title) 
+                continue            
+            m = re.match("(\d+)[.](\d+)[.]", entry["onlydate"])
+            if m:
+                logg.debug("onlydate %s for %s", entry["onlydate"], item.title)
+                try:
+                    onlyday = intnull(m.group(1))
+                    onlymonth = intnull(m.group(2))
+                    if not onlyday: continue
+                    if not onlymonth: continue
+                    if onlymonth not in allowed_months:
+                        logg.debug("onlymonth not in allowed_months '%s'", item.title) 
+                        continue
+                    if (onlyday, onlymonth) in past_days: 
+                        logg.debug("(onlyday, onlymonth) in past_days '%s'", item.title) 
+                        continue
+                    item.onlyday = onlyday
+                    item.months = [ onlymonth ]
+                    logg.info("onlyday %s onlymonths %s for %s", item.onlyday, item.months, item.title)
+                except Exception:
+                    logg.debug("conversion error %s '%s'", entry["onlydate"], item.title)
+                item.weekday = intnull(entry["weekday"])
+            else: 
+                item.weekday = intnull(entry["weekday"])
+                if item.weekday is None:
+                    logg.debug("item.weekday is None '%s'", item.title) 
+                    continue
+                if item.weekday >= 7: item.weekday = item.weekday % 7
+            item.newtimeMMM = self.zero(self.minutes(entry["newtime"]))
+            item.endtimeMMM = self.zero(self.minutes(entry["endtime"]))
+            item.extratimeM = self.zero(self.minutes(entry["extratime"]))
+            try: item.rank = float(int(entry["priority"]))
+            except: pass
+            if item.channelname in rankup_channels:
+                item.rank += rankup_plus
+            logg.debug("originally %s-%s '%s'" % (entry["newtime"], entry["endtime"], item.title))
+            yield item
+    def recordinglist_to_recordergroups(self, entries, ahead = months_ahead):
+        recorderitems = list(self.recordinglist_to_recorderitems(entries, ahead))
+        onlymonths = []
+        for item in recorderitems:
+            for month in item.months: 
+                if month not in onlymonths:
+                    onlymonths.append(month)
+        logg.info("onlymonths %s", onlymonths)
+        regularmonths = [ month for month in xrange(1,13) if month not in onlymonths ]
+        logg.info("regularmonths %s", regularmonths)
+        localtime = time.localtime()
+        this_month = localtime.tm_mon
+        this_year = localtime.tm_year
+        past_days = self.get_past_days_this_month()
+        for month in sorted(onlymonths):
+            year = this_year
+            if month < this_month: year += 1
+            for day in xrange(1,32):
+                if (day, month) in past_days: continue
+                try:
+                    # ValueError if invalid day-of-month
+                    date = datetime.date(year, month, day)
+                    # no generate the recordings for that day
+                    items = self.clone_filtered_for_date(recorderitems, date)
+                    for group in self.recordergroups_from_filtered(items):
+                        logg.info("onlydate group %s", unicode(group))
+                        yield group
+                except ValueError:
+                    pass # expected
+        items = self.clone_filtered_for_regular(recorderitems, regularmonths)
+        logg.info("=======================================================")
+        items = list(items)
+        for item in items:
+            logg.info("regular item %s", unicode(item))
+        for group in self.recordergroups_from_filtered(items):
+            logg.info("regular group %s", unicode(group))
+            yield group
+    def clone_filtered_for_date(self, recorderitems, date):
+        for item in recorderitems:
+            if item.onlyday: 
+                if item.onlyday != date.day: continue
+            elif item.months:
+                if date.month not in item.months: continue
+            else:
+                if item.weekday != date.weekday(): continue
+            newitem = item.clone()
+            newitem.onlydate = date
+            newitem.onlyday = date.day
+            newitem.months = [ date.month ]
+            newitem.weekday = date.weekday()
+            if item.onlyday: 
+                newitem.rank += onlydate_rank
+            yield newitem
+    def clone_filtered_for_regular(self, recorderitems, regularmonths):
+        for item in recorderitems:
+            if item.onlyday: continue
+            if item.months: continue
+            newitem = item.clone()
+            newitem.months = regularmonths
+            yield newitem
+    def recordergroups_from_filtered(self, recorderitems):
+        """ group items into recordings with overlapping time intervals """    
+        group = None
+        previous = None
+        nextday = 0
+        for item in recorderitems:
+            newtimeMMM = item.newtimeMMM
+            endtimeMMM = item.endtimeMMM
+            extratimeM = item.extratimeM 
+            if group is None: # first run
+                group = RecorderGroup()
+                group.newtimeMMM = newtimeMMM
+            elif group.endtimeMMM + group.extratimeM < newtimeMMM + nextday:
+                yield group # non-recording interval
+                group = RecorderGroup()
+                group.newtimeMMM = newtimeMMM
+            else: # overlapping?
+                assert previous is not None # because of first run
+                previous_ended_weekday = previous.weekday
+                if previous.endtimeMMM < previous.newtimeMMM:
+                    previous_ended_weekday += 1
+                if item.weekday != previous_ended_weekday:
+                    yield group # non-recording interval
+                    group = RecorderGroup()
+                    group.newtimeMMM = newtimeMMM
+                pass 
+            if endtimeMMM < newtimeMMM:
+                nextday = 24*60
+            group.endtimeMMM = endtimeMMM + nextday 
+            group.extratimeM = extratimeM
+            group.items += [ item ]
+            previous = item
+        if group is not None and group.endtimeMMM is not None:
+            yield group
+    def tuning_split_recordergroup(self, group, channels):
+        """ this function is supposed to get a list of adapters and
+            the channels that can be tuned on them. The recordings
+            of the recordergroup can then be scheduled to different
+            transponders and returned. Additionally, each item in the
+            returned groups has its endtimeMMM set so that the difference
+            of newtime-endtime is the recording time interval. """
+        yield self.adjust_recordergroup(group) 
+    def adjust_recordergroup(self, group):
+        logg.info("adjust group %s", unicode(group))
+        nextday = 0
+        previous = None
+        for item in group.items:
+            if previous is None:
+                previous = item
+                continue
+            if item.endtimeMMM < item.newtimeMMM:
+                nextday = 24 * 60
+            if previous.rank > item.rank:
+                item_newtime = item.newtimeMMM
+                item.newtimeMMM = previous.endtimeMMM + previous.extratimeM
+                if item_newtime != item.newtimeMMM:
+                    item.message += " %s >>" % MMMtoHHMM(item_newtime) 
+                    item.message += " %s;" % MMMtoHHMM(item.newtimeMMM)
+            elif previous.rank < item.rank:
+                previous_endtime = previous.endtimeMMM
+                previous.endtimeMMM = item.newtimeMMM
+                previous.extratimeM = 0
+                if previous_endtime != previous.endtimeMMM:
+                    previous.message += " %s << " % MMMtoHHMM(previous.endtimeMMM)
+                    previous.message += " %s;" % MMMtoHHMM(previous_endtime)
+            elif item.newtimeMMM < previous.endtimeMMM + previous.extratimeM:
+                item_newtime = item.newtimeMMM
+                item.newtimeMMM = previous.endtimeMMM + previous.extratimeM
+                if item_newtime != item.newtimeMMM:
+                    item.message += " %s ->" % MMMtoHHMM(item_newtime)
+                    item.message += " %s;;" % MMMtoHHMM(item.newtimeMMM)
+            if item.endtimeMMM + nextday <= item.newtimeMMM:
+                item.endtimeMMM = item.newtimeMMM
+                item.extratimeM = 0
+                item.message += " [DEL]"
+            elif previous.endtimeMMM == previous.newtimeMMM:
+                previous.message += " [DEL]"
+            previous = item
+            nextday = 0
+        return group
+    def make_cronmonths(self, group):
+        logg.info("adjust group %s", unicode(group))
+        for item in group.items:
+            ranges = []
+            months = item.months
+            if len(months) > 1:
+                start = None
+                previous = None
+                for month in months:
+                    if start is None:
+                        start = month
+                        previous = month
+                    elif month == previous + 1:
+                        previous = month
+                    else:
+                        ranges += [ (start,previous) ]
+                        start = month
+                        previous = month
+                ranges += [ (start,previous) ]
+            else:
+                ranges += [ (months[0],months[0])]
+            item.cronmonths = ranges
+        return group
+    def plan1(self, entries):
+        """ main entry point """
+        plan = RecorderGroupPlan()
+        for group in self.recordinglist_to_recordergroups(entries):
+            group = self.adjust_recordergroup(group)
+            group = self.make_cronmonths(group)
+            plan.addgroup(group)
+        return plan
+    def recordinglist_to_flat_recorderitems(self, entries, ahead = months_ahead):
+        recorderitems = list(self.recordinglist_to_recorderitems(entries, ahead))
+        localtime = time.localtime()
+        this_month = localtime.tm_mon
+        this_year = localtime.tm_year
+        past_days = self.get_past_days_this_month()
+        for month in list(xrange(this_month,13)) + list(xrange(1, this_month)):
+            year = this_year
+            if month < this_month: year += 1
+            for day in xrange(1,32):
+                if (day, month) in past_days: continue
+                try:
+                    # ValueError if invalid day-of-month
+                    date = datetime.date(year, month, day)
+                    for item in self.clone_filtered_for_date(recorderitems, date):
+                        yield item
+                except:
+                    pass
+    def adjust_flat_recorderitems(self, itemlist):
+        previous = None
+        for item in itemlist:
+            if item is None:
+                continue # old group separator
+            if item.endtimeMMM == item.newtimeMMM:
+                continue # deleted element
+            if previous is None:
+                previous = item
+                continue
+            item_newtimeMMM = item.newtimeMMM
+            if previous.onlydate == item.onlydate:
+                pass
+            elif previous.onlydate + datetime.timedelta(1) == item.onlydate:
+                item_newtimeMMM += 24 * 60
+            else:
+                previous = None
+                continue # items are separate by more than a day
+            if previous.endtimeMMM + previous.extratimeM < item_newtimeMMM:
+                previous = None
+                continue # separate by at least a few minutes
+            # -------------------------------------------------
+            nextday = 0
+            if item.endtimeMMM < item.newtimeMMM:
+                nextday = 24 * 60
+            if previous.rank > item.rank:
+                item_newtime = item.newtimeMMM
+                item.newtimeMMM = previous.endtimeMMM + previous.extratimeM
+                if item_newtime != item.newtimeMMM:
+                    item.message += " %s >>" % MMMtoHHMM(item_newtime) 
+                    item.message += " %s;" % MMMtoHHMM(item.newtimeMMM)
+            elif previous.rank < item.rank:
+                previous_endtime = previous.endtimeMMM
+                previous.endtimeMMM = item.newtimeMMM
+                previous.extratimeM = 0
+                if previous_endtime != previous.endtimeMMM:
+                    previous.message += " %s << " % MMMtoHHMM(previous.endtimeMMM)
+                    previous.message += " %s;" % MMMtoHHMM(previous_endtime)
+            elif item.newtimeMMM < previous.endtimeMMM + previous.extratimeM:
+                item_newtime = item.newtimeMMM
+                item.newtimeMMM = previous.endtimeMMM + previous.extratimeM
+                if item_newtime != item.newtimeMMM:
+                    item.message += " %s ->" % MMMtoHHMM(item_newtime)
+                    item.message += " %s;;" % MMMtoHHMM(item.newtimeMMM)
+            if item.endtimeMMM + nextday <= item.newtimeMMM:
+                item.endtimeMMM = item.newtimeMMM
+                item.extratimeM = 0
+                item.message += " [DEL]"
+            elif previous.endtimeMMM == previous.newtimeMMM:
+                previous.message += " [DEL]"
+            previous = item
+    def recorderitems_groups(self, itemlist):
+        group = None
+        for item in itemlist: 
+            if not group:
+                group = RecorderGroup()
+                group.firstdate = item.onlydate
+                group.newtimeMMM = item.newtimeMMM
+                group.endtimeMMM = item.endtimeMMM
+                group.extratimeM = item.extratimeM
+                group.items += [ item ]
+                continue
+            delta = item.onlydate - group.firstdate
+            deltaMMM = delta.days * 24 * 60
+            if group.endtimeMMM < item.newtimeMMM + deltaMMM:
+                yield group
+                group = RecorderGroup()
+                group.firstdate = item.onlydate
+                group.newtimeMMM = item.newtimeMMM
+                group.endtimeMMM = item.endtimeMMM
+                group.extratimeM = item.extratimeM
+                group.items += [ item ]
+            else:
+                group.endtimeMMM = item.endtimeMMM + deltaMMM
+                group.extratimeM = item.extratimeM
+                group.items += [ item ]
+        if group is not None:
+            yield group
+    def deleted_items(self, itemlist):
+        for item in itemlist:
+            if item.newtimeMMM == item.endtimeMMM:
+                yield item
+    def plan2(self, entries):
+        """ main entry point """
+        itemlist = list(self.recordinglist_to_flat_recorderitems(entries))
+        deleted = 0
+        for round in xrange(10):
+            self.adjust_flat_recorderitems(itemlist)
+            newdeleted = len(list(self.deleted_items(itemlist)))
+            if newdeleted > deleted:
+                continue
+        plan = RecorderGroupPlan()
+        for group in self.recorderitems_groups(itemlist):
+            plan.addgroup(group)
+        group = RecorderGroup()
+        item = RecorderItem()
+        item.message = "itemlist length %s" % len(itemlist)
+        group.items += [ item ]
+        plan.addgroup(group)        
+        return plan
+    def recordinglist_to_recordertimelist(self, entries, ahead = months_ahead):
+        recorderitems = list(self.recordinglist_to_recorderitems(entries, ahead))
+        localtime = time.localtime()
+        this_month = localtime.tm_mon
+        this_year = localtime.tm_year
+        past_days = self.get_past_days_this_month()
+        for month in list(xrange(this_month,13)) + list(xrange(1, this_month)):
+            year = this_year
+            if month < this_month: year += 1
+            for day in xrange(1,32):
+                if (day, month) in past_days: continue
+                try:
+                    # ValueError if invalid day-of-month
+                    date = datetime.datetime(year, month, day)
+                    for item in recorderitems:
+                        if item.onlyday: 
+                            if item.onlyday != date.day: continue
+                            if date.month not in item.months: continue
+                        elif item.months:
+                            if date.month not in item.months: continue
+                        else:
+                            if item.weekday != date.weekday(): continue
+                        logg.info("* %s => [%s.%s.] w%s '%s'", date, item.onlyday, item.months, item.weekday, item.title)
+                        newtimeMMM = item.newtimeMMM
+                        endtimeMMM = item.endtimeMMM
+                        if endtimeMMM < newtimeMMM: endtimeMMM += 24 * 60
+                        endtimeMMM += item.extratimeM
+                        elem = RecorderTime(item)
+                        elem.datetimeX = date + datetime.timedelta(minutes = newtimeMMM)
+                        elem.datetimeY = date + datetime.timedelta(minutes = endtimeMMM)
+                        # elem.message += " (([%s .. %s] from %s..%s))" % (elem.datetimeX, elem.datetimeY, newtimeMMM, endtimeMMM)
+                        yield elem
+                except:
+                    pass
+    def adjust_recordertimelist(self, recordertimelist):
+        """ recordertimelist contains absolute X Y dates """
+        minimum = datetime.timedelta(minutes = 2)
+        previous = None
+        for current in recordertimelist:
+            if current.deleted:
+                continue
+            if previous is None:
+                previous = current
+                continue
+            if previous.datetimeY <= current.datetimeX:
+                previous = current
+                continue # nothing to do - there is a gap
+            if previous.datetimeX > current.datetimeX:
+                raise Exception("previous starts after current")
+            overlapY = previous.datetimeY - current.datetimeX
+            overlapM = deltaM(overlapY)
+            assert overlapM > 0
+            if previous.item.rank < current.item.rank:
+                # move previous endtime down
+                if overlapM <= previous.extratimeM:
+                    previous.extratimeM -= overlapM
+                    previous.datetimeY -= overlapY
+                    previous.message += " cut extratimeM %03d," % overlapM
+                    previous = current
+                else: 
+                    extraY = datetime.timedelta(minutes = previous.extratimeM)
+                    previous.datetimeY -= extraY
+                    previous.extratimeM = 0 
+                    if current.datetimeX <= previous.datetimeX:
+                        previous.deleted = True
+                        previous.datetimeY = previous.datetimeX
+                        previous.endtimeMMM = previous.newtimeMMM
+                        previous.extratimeM = 0
+                        previous.message += " deleted as next one starts %s," % MMMtoHHMM(current.newtimeMMM)
+                    else:
+                        if previous.datetimeY > current.datetimeX:
+                            previous.datetimeY = current.datetimeX
+                            previous.endtimeMMM = current.newtimeMMM
+                            previous.message += " end time set to next start %s," % MMMtoHHMM(current.newtimeMMM)
+                        if previous.datetimeY - previous.datetimeX < minimum:
+                            previous.deleted = True
+                            previous.datetimeY = previous.datetimeX
+                            previous.endtimeMMM = previous.newtimeMMM
+                            previous.extratimeM = 0
+                            previous.message += " and deleted as it is too short now"
+                        else:
+                            previous = current
+            else:
+                # move current starttime up
+                if current.datetimeY <= previous.datetimeY:
+                    if previous.datetimeX > current.datetimeX:
+                        current.message+= "previous starts after current"
+                    current.deleted = True
+                    current.datetimeX = current.datetimeY
+                    current.newtimeMMM = current.endtimeMMM
+                    current.extratimeM = 0
+                    current.message += " deleted as previous one ends %s," % MMMtoHHMM(previous.endtimeMMM)
+                else:
+                    if current.datetimeX < previous.datetimeY:
+                        current.datetimeX = previous.datetimeY
+                        # current.item.weekday = current.datetimeX.weekday
+                        # if current.item.onlyday: # might have crossed midnight 
+                        #    current.item.onlyday = current.datetimeX.day
+                        #    current.item.months = [ current.datetimeX.month ] 
+                        current.newtimeMMM = previous.endtimeMMM + previous.extratimeM
+                        current.message += " set start time to previous end %s," % MMMtoHHMM(current.newtimeMMM)
+                    if current.datetimeY - current.datetimeX < minimum:
+                        current.deleted = True
+                        current.datetimeX = current.datetimeY
+                        current.newtimeMMM = current.endtimeMMM
+                        current.extratimeM = 0
+                        current.message += " and deleted as it is too short now"
+                    else:
+                        previous = current
+    def timelist_groups(self, timelist):
+        group = None
+        previous = None
+        previousgroup = None
+        predeletes = []
+        seen = ""
+        for elem in timelist:
+            item = elem.item.clone()
+            item.newtimeMMM = elem.newtimeMMM
+            item.endtimeMMM = elem.endtimeMMM
+            item.extratimeM = elem.extratimeM
+            item.weekday = elem.datetimeX.weekday()
+            item.onlyday = elem.datetimeX.day
+            item.months = [ elem.datetimeX.month ]
+            if item.newtimeMMM != elem.item.newtimeMMM:
+                item.message += " [%s]>>[%s]" % (MMMtoHHMM(elem.item.newtimeMMM), MMMtoHHMM(item.newtimeMMM))
+            if item.endtimeMMM != elem.item.endtimeMMM:
+                item.message += " [%s]<<[%s]" % (MMMtoHHMM(item.endtimeMMM), MMMtoHHMM(elem.item.endtimeMMM))
+            # ==>
+            if not group or elem.datetimeX > previous.datetimeY:
+                # START or TIME GAP
+                if group:
+                    yield group ; previousgroup = group; group = None
+                item.message = " ."+item.message
+                if elem.deleted: 
+                    item.message += " [DEL]"
+                    predeletes += [ item ]
+                else:
+                    previous = elem
+                    group = RecorderGroup()
+                    group.newtimeMMM = item.newtimeMMM
+                    group.endtimeMMM = item.endtimeMMM
+                    group.extratimeM = item.extratimeM
+                    group.items = predeletes + [ item ]
+                    predeletes = []
+            else:
+                # HANDOVER
+                item.message = " :"+item.message
+                if elem.deleted: 
+                    item.message += " [DEL]"
+                    group.items += [ item ]
+                else:
+                    previous = elem
+                    group.endtimeMMM = item.endtimeMMM
+                    group.extratimeM = item.extratimeM
+                    group.items += [ item ]
+            item.message += elem.message + seen
+        if predeletes:
+            if group:
+                group.items += predeletes
+            elif previousgroup:
+                previousgroup.items += predeletes
+        if group:
+            # END
+            yield group ; group = None
+    def reduce_regular_groups(self, plan):
+        class BlockGroup:
+            def __init__(self):
+                self.groups = []
+        blocks = {}
+        for group in plan.groups():
+            group.deleted = False
+            group.weekday = group.items[0].weekday
+            group.hasonlydays = 0
+            for item in group.items:
+                if item.cloned and item.cloned.onlyday:
+                    group.hasonlydays += 1
+            key = (group.weekday, group.newtimeMMM, group.endtimeMMM, group.extratimeM)
+            if key not in blocks:
+                blocks[key] = BlockGroup()
+            blocks[key].groups += [ group ]
+        for key, block in blocks.items():
+            block.master = None
+            months_with_onlydays = []
+            for group in block.groups:
+                month = group.items[0].months[0]
+                if group.hasonlydays:
+                    if month not in months_with_onlydays:
+                        months_with_onlydays += [ month ]
+            # now we have a candidate of groups that we want to collapse
+            block.months = [ month for month in xrange(1,13) 
+                            if month not in months_with_onlydays ]
+            for group in block.groups:
+                month = group.items[0].months[0]
+                if month not in block.months: # month in months_with_onlydays
+                    continue
+                if block.master is None:
+                    block.master = group
+                    for item in group.items:
+                        item.onlyday = None
+                        item.months = block.months
+                    self.make_cronmonths(group)
+                else:
+                    group.deleted = True
+                    for item in group.items:
+                        item.onlyday = None
+                        item.months = []
+        newplan = RecorderGroupPlan()
+        for group in plan.groups():
+            if group.deleted: continue
+            newplan.addgroup(group)
+        return newplan           
+    def plan3(self, entries):
+        """ main entry point """
+        timelist = list(self.recordinglist_to_recordertimelist(entries))
+        old_deleted = 0
+        for round in xrange(2):
+            timelist = sorted(timelist, key = lambda x: x.datetimeX )
+            self.adjust_recordertimelist(timelist)
+            deleted = [ elem for elem in timelist if elem.deleted ]
+            new_deleted = len(deleted)
+            if old_deleted < new_deleted:
+                old_deleted = new_deleted
+                continue
+        timelist = sorted(timelist, key = lambda x: x.datetimeX )
+        plan = RecorderGroupPlan()
+        for group in self.timelist_groups(timelist):
+            plan.addgroup(group)
+        if True:
+            return self.reduce_regular_groups(plan)
+        return plan
+            
+    def plan(self, entries):
+        return self.plan3(entries)
+
+def deltaM(td):
+    return td.seconds / 60 + (td.days * 24 * 60)
+
+def MMMtoHHMM(mmm):
+    if mmm is None: return ""
+    return "%02i:%02i" % (mmm / 60, mmm % 60)
+        

src/dvbcronrecording/core.de.po

+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Report-Msgid-Bugs-To: guidod@gmx.de\n"
+"Plural-Forms: nplurals=3; plural=(n==0 ? 0 : (n == 1 ? 1 : 2));\n"
+
+msgid ".recorder."
+msgstr "Recorder"
+
+msgid ".recording."
+msgstr "Recorder"
+
+msgid "insert" 
+msgstr "einfügen"
+
+msgid "delete" 
+msgstr "[löschen]"
+
+msgid "update"
+msgstr "aktualisieren"
+
+
+msgid "play"
+msgstr "Abspielen"
+
+msgid "mplay"
+msgstr "mit MPlayer"
+
+msgid "starts"
+msgstr "Startzeit"
+
+msgid "ends"
+msgstr "Endzeit"
+
+msgid "extra"
+msgstr "Extra"
+
+msgid "weekday"
+msgstr "Wo-Tag"
+
+msgid "onlydate"
+msgstr " +Datum"
+
+msgid "status"
+msgstr "Status"
+
+msgid "priority"
+msgstr "Priorität"
+
+msgid "prio"
+msgstr "Prio"
+
+msgid "adapter"
+msgstr "Adapter"
+
+msgid "channel.conf title"
+msgstr "Bezeichnung in der channel.conf"
+
+msgid "channel.conf show"
+msgstr "channel.conf Ansicht"
+
+msgid "scansettings"
+msgstr "Scan-Einstellungen"
+
+msgid "satellite"
+msgstr "Satellit"
+
+msgid "transponder"
+msgstr "Transponder"
+
+msgid "action buttons"
+msgstr "Betriebsschalter"
+
+msgid "newtime"
+msgstr "Startzeit"
+
+msgid "endtime"
+msgstr "Endzeit"
+
+msgid "hint"
+msgstr "Hinweis"
+
+msgid "NEW"
+msgstr "NEU"
+
+msgid "SAVE"
+msgstr "SPEICHERN"
+
+msgid "Show Video"
+msgstr "Video Anzeigen"
+
+msgid "Recorded Files Overview"
+msgstr "Überblick aufgezeichneter Dateien"
+
+msgid "Makefile Overview"
+msgstr "Make-Datei Überblick"
+
+msgid "Cronfile Overview"
+msgstr "Cron-Datei Überblick"
+
+msgid "Recording Plan"
+msgstr "Der Aufnahmeplan"
+
+msgid "Cron Activate"
+msgstr "Cron Aktivierung"
+
+msgid "Recordings List"
+msgstr "Die Recorder-Liste"
+
+
+# ---------------------------------------------------------
+
+msgid "fully internationalized"
+msgstr "Umlaute sind jetzt möglich."
+
+msgid "-.prio"
+msgstr "--"
+
+msgid "0.prio"
+msgstr "hmm"
+
+msgid "1.prio" 
+msgstr "egal"
+
+msgid "2.prio"
+msgstr "kann"
+
+msgid "3.prio"
+msgstr "soll"
+
+msgid "4.prio"
+msgstr "muss"
+
+msgid "5.prio"
+msgstr "muss!"
+
+msgid "6.prio"
+msgstr "top!"
+
+msgid "7.prio"
+msgstr "über!"
+
+msgid "-.status"
+msgstr "--"
+
+msgid "no.status"
+msgstr "no"
+
+msgid "ok.status"
+msgstr "ok"
+
+msgid "prime-time-channel"
+msgstr "pro7"
+
+msgid "prime-time-starts"
+msgstr "20:15"
+