Author: dmeyer
Date: Sun Feb 5 20:35:39 2006
New Revision: 7908
Added:
trunk/tvserver/src/epg.py
trunk/tvserver/src/scheduler.py
Modified:
trunk/tvserver/src/conflict.py
trunk/tvserver/src/recorder.py
trunk/tvserver/src/recording.py
trunk/tvserver/src/server.py
Log:
Huge update in the internal structures. The code scaled much better
now for many recordings and slow machines. Scanning and resolving
of conflicts and the whole schedule part is now split into several
steps, each called from the main loop.
As a downside, adding or changing a recording or favorite won't have
an effect on the rpc return, it will take some time. This needs to be
fixed (e.g. delay the return or handle it at client side)
Modified: trunk/tvserver/src/conflict.py
==============================================================================
--- trunk/tvserver/src/conflict.py (original)
+++ trunk/tvserver/src/conflict.py Sun Feb 5 20:35:39 2006
@@ -56,14 +56,13 @@
# -----------------------------------------------------------------------------
-__all__ = [ 'resolve', 'clear_cache' ]
-
# python imports
import logging
import time
# kaa notifier for step
import kaa.notifier
+from kaa.notifier import Timer, OneShotTimer, execute_in_timer
# record imports
import recorder
@@ -72,6 +71,8 @@
# get logging object
log = logging.getLogger('conflict')
+# set some extra debug
+log.setLevel(logging.DEBUG)
class Device(object):
def __init__(self, recorder=None):
@@ -117,13 +118,13 @@
# get the bouquet where the current recordings are
bouquet = [ x for x in self.listing if recording.channel in x ][0]
-
+
# Get all recordings conflicting with the current one by time. based on
# a sort listing, the others will start before or at the same time as
# the latest. So when the stop time of an other is before the new start
# time, it is no conflict. (ignoring padding here for once)
# If the conflict is only based on the padding, add it to
conflict_padding
-
+
for r in self.rec:
if r.channel in bouquet:
# same bouquet, will always work
@@ -141,208 +142,194 @@
def remove_last(self):
self.rec = self.rec[:-1]
-
-def scan(recordings):
- """
- Scan the schedule for conflicts. A conflict is a list of recordings
- with overlapping times.
- """
- for r in recordings:
- r.start -= r.start_padding
- r.stop += r.stop_padding
- # Sort by start time
- recordings.sort(lambda l, o: cmp(l.start,o.start))
-
- # all conflicts found
- conflicts = []
-
- # recordings already scanned
- scanned = []
-
- # get current time
- ctime = time.time()
-
- # Check all recordings in the list for conflicts
- for r in recordings:
- if r in scanned:
- # Already marked as conflict
- continue
- current = []
- # Set stop time for the conflict area to current stop time. The
- # start time doesn't matter since the recordings are sorted by
- # start time and this is the first
- stop = r.stop
- while True:
- for c in recordings[recordings.index(r)+1:]:
- # Check all recordings after the current 'r' if the
- # conflict
- if c in scanned:
- # Already marked as conflict
- continue
- if c.start < stop:
- # Found a conflict here. Mark the item as conflict and
- # add to the current conflict list
- current.append(c)
- scanned.append(c)
- # Get new stop time and repeat the scanning with it
- # starting from 'r' + 1
- stop = max(stop, c.stop)
+
+class Conflict(object):
+
+ def __init__(self, callback):
+ self.callback = callback
+
+
+ @execute_in_timer(OneShotTimer, 0.01, type='override')
+ def scan(self, recordings, schedule):
+ """
+ Scan the schedule for conflicts. A conflict is a list of recordings
+ with overlapping times.
+ """
+ log.info('start conflict resolving')
+ devices = [ Device() ]
+ for p in recorder.get_recorder():
+ devices.append(Device(p))
+ devices.sort(lambda l, o: cmp(o.rating,l.rating))
+
+ # Sort by start time
+ recordings = recordings[:]
+ recordings.sort(
+ lambda l, o: cmp(l.start - l.start_padding, o.start -
o.start_padding))
+
+ # all conflicts found
+ self.conflicts = []
+
+ # recordings already scanned
+ scanned = []
+
+ # get current time
+ ctime = time.time()
+
+ # Check all recordings in the list for conflicts
+ for r in recordings:
+ if r in scanned:
+ # Already marked as conflict
+ continue
+ current = []
+ # Set stop time for the conflict area to current stop time. The
+ # start time doesn't matter since the recordings are sorted by
+ # start time and this is the first
+ stop = r.stop + r.stop_padding
+ while True:
+ for c in recordings[recordings.index(r)+1:]:
+ # Check all recordings after the current 'r' if the
+ # conflict
+ if c in scanned:
+ # Already marked as conflict
+ continue
+ if c.start - c.stop_padding < stop:
+ # Found a conflict here. Mark the item as conflict and
+ # add to the current conflict list
+ current.append(c)
+ scanned.append(c)
+ # Get new stop time and repeat the scanning with it
+ # starting from 'r' + 1
+ stop = max(stop, c.stop + c.stop_padding)
+ break
+ else:
+ # No new conflicts found, the while True is done
break
- else:
- # No new conflicts found, the while True is done
- break
- if current:
- # Conflict found. Mark the current 'r' as conflict and
- # add it to the conflict list. 'current' will be reset to
- # [] for the next scanning to get groups of conflicts
- conflicts.append([ r ] + current)
-
- for r in recordings:
- r.start += r.start_padding
- r.stop -= r.stop_padding
- return conflicts
-
-
-def rate_conflict(clist):
- """
- Rate a conflict list created by 'scan'. Result is a negative value
- about the conflict lists.
- """
- number = 0
- prio = 0
- ret = 0
- if not clist:
- return 0
- # Ideas from Sep. 02
- #
- # Rating (will be called when everything is set, for all devices except
NULL)
- # for r in recordings:
- # result += (0.1 * dev.prio) + 1 * (rec.prio + rec.length * 0.001) - cr
- # and cr is based on all recordings starting incl. padding before r,
overlap
- # with r and are not in the same bouquet as r.
- # so cr is AveragePrio * 0.01 + overlapping time in minutes
- for c in clist:
- for pos, r1 in enumerate(c[:-1]):
- # check all pairs of conflicts (stop from x with start from x + 1)
- # next recording
- r2 = c[pos+1]
- # overlapping time in seconds
- time_diff = r1.stop + r1.stop_padding - r2.start - r2.start_padding
- # min priority of the both recordings
- min_prio = min(r1.priority, r2.priority)
- # average priority of the both recordings
- average_prio = (r1.priority + r2.priority) / 2
-
- # Algorithm for the overlapping rating detection:
- # min_prio / 2 (difference between 5 card types) +
- # average_prio / 100 (low priority in algorithm) +
- # number of overlapping minutes
- ret -= min_prio / 2 + average_prio / 100 + time_diff / 60
- return ret
-
-
-def rate(devices, best_rating):
- """
- Rate device/recording settings. If the rating is better then best_rating,
- store the choosen recorder in the recording item.
- """
- rating = 0
- for d in devices[:-1]:
- for r in d.rec:
- rating += (0.1 * d.rating + 1) * r.priority
- if len(r.conflict_padding):
- rating += rate_conflict([r.conflict_padding + [ r ]])
-
- if rating > best_rating:
- # remember
- best_rating = rating
+ if current:
+ # Conflict found. Mark the current 'r' as conflict and
+ # add it to the conflict list. 'current' will be reset to
+ # [] for the next scanning to get groups of conflicts
+ self.conflicts.append([ r ] + current)
+
+ self.resolve_step(devices, schedule)
+ return False
+
+
+ @execute_in_timer(Timer, 0, type='override')
+ def resolve_step(self, devices, schedule):
+ """
+ Find and resolve conflicts in recordings.
+ """
+ if not self.conflicts:
+ # done, run callback
+ log.info('finished conflict resolving')
+ self.callback(schedule)
+ return False
+
+ conflict = self.conflicts.pop(0)
+ # some ugly debug
+ log.debug('found conflict:\n %s', '\n '.join([ str(x) for x in
conflict ] ))
+
+ # check all possible solutions for this conflict
+ self._check(devices, [], conflict, 0, schedule)
+
+ return True
+
+
+ def _check(self, devices, fixed, to_check, best_rating, schedule,
dropped=1):
+ """
+ Check all possible combinations from the recordings in to_check on all
+ devices. Call recursive again.
+ """
+ if not dropped and len(devices[-1].rec):
+ # There was a solution without any recordings dropped.
+ # It can't get better because devices[-1].rec already contains
+ # at least one recording
+ return best_rating, dropped
+
+ if not to_check:
+ return self._rate(devices, best_rating, schedule),
len(devices[-1].rec)
+
+ c = to_check[0]
+ for d in devices:
+ if d.append(c):
+ best_rating, dropped = self._check(devices, fixed + [ c ],
to_check[1:],
+ best_rating, schedule,
dropped)
+ d.remove_last()
+ return best_rating, dropped
+
+
+ def _rate(self, devices, best_rating, schedule):
+ """
+ Rate device/recording settings. If the rating is better then
best_rating,
+ store the choosen recorder in the recording item.
+ """
+ rating = 0
for d in devices[:-1]:
for r in d.rec:
- if r.status != RECORDING:
- r.status = SCHEDULED
- r.recorder = d.recorder
- r.respect_start_padding = True
- r.respect_stop_padding = True
+ rating += (0.1 * d.rating + 1) * r.priority
+ if len(r.conflict_padding):
+ rating += self._rate_conflict([r.conflict_padding + [ r ]])
+
+ if rating > best_rating:
+ # remember
+ best_rating = rating
+
+ for d in devices[:-1]:
+ for r in d.rec:
+ if r.status == RECORDING:
+ continue
+
+ schedule[r.id] = [ SCHEDULED, d.recorder, True, True ]
if r.conflict_padding:
# the start_padding conflicts with the stop paddings
# the recordings in r.conflict_padding. Fix it by
# removing the padding
# FIXME: maybe start != stop
- r.respect_start_padding = False
+ schedule[r.id][2] = False
for c in r.conflict_padding:
- c.respect_stop_padding = False
- for r in devices[-1].rec:
- r.status = CONFLICT
- r.recorder = None
- return best_rating
+ schedule[r.id][3] = False
+ for r in devices[-1].rec:
+ schedule[r.id] = [ CONFLICT, None, True, True ]
+ return best_rating
-def check(devices, fixed, to_check, best_rating, dropped=1):
- """
- Check all possible combinations from the recordings in to_check on all
- devices. Call recursive again.
- """
- if not dropped and len(devices[-1].rec):
- # There was a solution without any recordings dropped.
- # It can't get better because devices[-1].rec already contains
- # at least one recording
- return best_rating, dropped
-
- if not to_check:
- return rate(devices, best_rating), len(devices[-1].rec)
- c = to_check[0]
- for d in devices:
- if d.append(c):
- best_rating, dropped = check(devices, fixed + [ c ], to_check[1:],
- best_rating, dropped)
- d.remove_last()
- return best_rating, dropped
-
-
-# list of devices
-_devices = []
-
-def resolve(recordings, recorder):
- """
- Find and resolve conflicts in recordings.
- """
- t1 = time.time()
- if not _devices:
- # create 'devices'
- _devices.append(Device())
- for p in recorder:
- _devices.append(Device(p))
- _devices.sort(lambda l, o: cmp(o.rating,l.rating))
-
- # sort recordings
- recordings.sort(lambda l, o: cmp(l.start,o.start))
-
- # resolve recordings
- for c in scan(recordings):
- # FIXME: This keeps the main loop alive but is ugly.
- # Change it to something better when kaa.epg is thread based
- kaa.notifier.step(False)
-
- if 0:
- info = 'found conflict:\n'
- log.info(info)
- check(_devices, [], c, 0)
- if 0:
- info ='solved by setting:\n'
- for r in c:
- info += '%s\n' % str(r)
- log.debug(info)
- t2 = time.time()
- log.info('resolve conflict took %s secs' % (t2-t1))
-
-
-def clear_cache():
- """
- Clear the global conflict resolve cache
- """
- global _devices
- _devices = []
+ def _rate_conflict(self, clist):
+ """
+ Rate a conflict list created by 'scan'. Result is a negative value
+ about the conflict lists.
+ """
+ number = 0
+ prio = 0
+ ret = 0
+ if not clist:
+ return 0
+ # Ideas from Sep. 02
+ #
+ # Rating (will be called when everything is set, for all devices
except NULL)
+ # for r in recordings:
+ # result += (0.1 * dev.prio) + 1 * (rec.prio + rec.length * 0.001)
- cr
+ # and cr is based on all recordings starting incl. padding before r,
overlap
+ # with r and are not in the same bouquet as r.
+ # so cr is AveragePrio * 0.01 + overlapping time in minutes
+ for c in clist:
+ for pos, r1 in enumerate(c[:-1]):
+ # check all pairs of conflicts (stop from x with start from x
+ 1)
+ # next recording
+ r2 = c[pos+1]
+ # overlapping time in seconds
+ time_diff = r1.stop + r1.stop_padding - r2.start -
r2.start_padding
+ # min priority of the both recordings
+ min_prio = min(r1.priority, r2.priority)
+ # average priority of the both recordings
+ average_prio = (r1.priority + r2.priority) / 2
+
+ # Algorithm for the overlapping rating detection:
+ # min_prio / 2 (difference between 5 card types) +
+ # average_prio / 100 (low priority in algorithm) +
+ # number of overlapping minutes
+ ret -= min_prio / 2 + average_prio / 100 + time_diff / 60
+ return ret
Added: trunk/tvserver/src/epg.py
==============================================================================
--- (empty file)
+++ trunk/tvserver/src/epg.py Sun Feb 5 20:35:39 2006
@@ -0,0 +1,189 @@
+# -*- coding: iso-8859-1 -*-
+# -----------------------------------------------------------------------------
+# epg.py - EPG handling for the recordserver
+# -----------------------------------------------------------------------------
+# $Id: server.py 7893 2006-01-29 17:53:54Z dmeyer $
+#
+#
+# -----------------------------------------------------------------------------
+# Freevo - A Home Theater PC framework
+# Copyright (C) 2002-2005 Krister Lagerstrom, Dirk Meyer, et al.
+#
+# First Edition: Dirk Meyer <[EMAIL PROTECTED]>
+# Maintainer: Dirk Meyer <[EMAIL PROTECTED]>
+#
+# Please see the file doc/CREDITS for a complete list of authors.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MER-
+# CHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# -----------------------------------------------------------------------------
+
+# python imports
+import time
+import logging
+
+# kaa imports
+import kaa.epg
+from kaa.notifier import OneShotTimer, Timer, Signal, execute_in_timer
+
+# record imports
+from record_types import *
+from recording import Recording
+
+# get logging object
+log = logging.getLogger('record')
+
+
+class EPG(object):
+
+ def __init__(self):
+ self.signals = { 'changed': Signal() }
+
+
+ def channels(self):
+ """
+ Return list of channels.
+ """
+ return kaa.epg.channels
+
+
+ def check_all(self, favorites, recordings, callback, *args, **kwargs):
+ """
+ Check recordings and favorites against epg.
+ """
+ self.check_recordings(recordings, self.check_favorities, favorites,
+ recordings, callback, *args, **kwargs)
+
+
+ def check_recordings(self, recordings, callback, *args, **kwargs):
+ """
+ Check current recordings against the database
+ """
+ cb = None
+ if callback:
+ cb = OneShotTimer(callback, *args, **kwargs)
+
+ # get list of recordings to check
+ ctime = time.time() + 60 * 15
+ recordings = [ r for r in recordings if r.start - r.start_padding >
ctime \
+ and r.status in (CONFLICT, SCHEDULED) ]
+ # start check_recordings_step
+ self.check_recordings_step(recordings, cb)
+
+
+ @execute_in_timer(Timer, 0)
+ def check_recordings_step(self, recordings, callback):
+ """
+ Check one recording
+ """
+ if not recordings:
+ # start callback
+ if callback:
+ callback.start(0)
+ return False
+
+ # get one recording to check
+ rec = recordings.pop(0)
+
+ # Search epg for that recording. The recording should be at the
+ # same time, maybe it has moved +- 20 minutes. If the program
+ # moved a larger time interval, it won't be found again.
+ interval = (rec.start - 20 * 60, rec.start + 20 * 60)
+ results = kaa.epg.search(rec.name, rec.channel, exact_match=True,
+ interval = interval)
+
+ for epginfo in results:
+ # check all results
+ if epginfo.start == rec.start and epginfo.stop == rec.stop:
+ # found the recording
+ break
+ else:
+ # try to find it
+ for epginfo in results:
+ if rec.start - 20 * 60 < epginfo.start < rec.start + 20 * 60:
+ # found it again, set new start and stop time
+ old_info = str(rec)
+ rec.start = epginfo.start
+ rec.stop = epginfo.stop
+ log.info('changed schedule\n%s\n%s' % (old_info, rec))
+ self.signals['changed'].emit(rec)
+ break
+ else:
+ log.info('unable to find recording in epg:\n%s' % rec)
+ return True
+
+ # check if attributes changed (Note: String() should not be
+ # needed here, everything has to be unicode, at least when
+ # kaa.epg2 is done)
+ for attr in ('description', 'episode', 'subtitle'):
+ if String(getattr(rec, attr)) != String(getattr(epginfo, attr)):
+ log.info('%s changed for %s', attr, rec.name)
+ setattr(rec, attr, getattr(epginfo, attr))
+ return True
+
+
+ def check_favorities(self, favorites, recordings, callback, *args,
**kwargs):
+ """
+ Check favorites against the database and add them to the list of
+ recordings. If callback is given, the callback will be called
+ when checking is done.
+ """
+ cb = None
+ if callback:
+ cb = OneShotTimer(callback, *args, **kwargs)
+ self.check_favorites_step(favorites, favorites[:], recordings, cb)
+
+
+ @execute_in_timer(Timer, 0)
+ def check_favorites_step(self, all_favorites, favorites, recordings,
callback):
+ """
+ Check one favorite or run the callback when finished
+ """
+ if not favorites:
+ # start callback
+ if callback:
+ callback.start(0)
+ return False
+
+ # get favorite to check
+ fav = favorites.pop(0)
+
+ # Now search the db
+ for p in kaa.epg.search(fav.name, exact_match=not fav.substring):
+ if not fav.match(p.title, p.channel.id, p.start):
+ continue
+
+ rec = Recording(p.title, p.channel.id, fav.priority,
+ p.start, p.stop, episode=p.episode,
+ subtitle=p.subtitle, description=p.description)
+
+ if rec in recordings:
+ # This does not only avoid adding recordings twice, it
+ # also prevents from added a deleted favorite as active
+ # again.
+ continue
+
+ fav.add_data(rec)
+ recordings.append(rec)
+ log.info('added\n%s', rec)
+
+ self.signals['changed'].emit(rec)
+
+ if fav.once:
+ all_favorites.remove(fav)
+ break
+
+ # done with this one favorite
+ return True
Modified: trunk/tvserver/src/recorder.py
==============================================================================
--- trunk/tvserver/src/recorder.py (original)
+++ trunk/tvserver/src/recorder.py Sun Feb 5 20:35:39 2006
@@ -29,6 +29,8 @@
#
# -----------------------------------------------------------------------------
+__all__ = [ 'signals', 'connect', 'get_recorder' ]
+
# python imports
import os
import sys
@@ -38,7 +40,7 @@
import logging
# kaa imports
-from kaa.notifier import OneShotTimer, Callback
+from kaa.notifier import OneShotTimer, Signal
import kaa.epg
# freevo core imports
@@ -51,18 +53,49 @@
# get logging object
log = logging.getLogger('record')
+# global RecorderList object
+_recorder = None
+
+# signals for this module
+signals = { 'changed': Signal(),
+ 'start-recording': Signal(),
+ 'stop-recording': Signal()
+ }
+
+def connect():
+ """
+ Connect to mbus. This will create the global RecorderList object
+ """
+ global _recorder
+ if _recorder:
+ return False
+ _recorder = RecorderList()
+
+
+def get_recorder(channel=None):
+ """
+ Get recorder. If channel is given, return the best recorder for this
+ channel, if not, return all recorder objects.
+ """
+ if not _recorder:
+ raise RuntimeError('recorder not connected')
+ if channel:
+ return _recorder.best_recorder.get(channel)
+ return _recorder.recorder
+
+
+# ****************************************************************************
+# Internal stuff
+# ****************************************************************************
+
# internal 'unique' ids
UNKNOWN_ID = -1
IN_PROGRESS = -2
-# recording daemon
-DAEMON = {'type': 'home-theatre', 'module': 'tvdev'}
-
class RecorderList(object):
- def __init__(self, server):
+ def __init__(self):
self.recorder = []
self.best_recorder = {}
- self.server = server
mbus = freevo.ipc.Instance('tvserver')
mbus.signals['new-entity'].connect(self.new_entity)
@@ -101,7 +134,8 @@
self.best_recorder[c] = p.rating, p, p.device
for c in self.best_recorder:
self.best_recorder[c] = self.best_recorder[c][1]
- self.server.check_recordings(True)
+
+ signals['changed'].emit()
def __iter__(self):
@@ -112,7 +146,7 @@
"""
Update recorders on entity changes.
"""
- if not entity.matches(DAEMON):
+ if not entity.matches({'type': 'home-theatre', 'module': 'tvdev'}):
# no recorder
return True
@@ -148,9 +182,9 @@
return True
if event.name.endswith('started'):
- self.server.start_recording(rec.recording)
+ signals['start-recording'].emit(rec.recording)
else:
- self.server.stop_recording(rec.recording)
+ signals['stop-recording'].emit(rec.recording)
return True
@@ -198,7 +232,7 @@
log.info('%s lost entity' % self)
self.handler.remove(self)
-
+
def sys_exit(self):
config.save()
log.error('Unknown channels detected on device %s.' % self.device)
@@ -208,7 +242,7 @@
def normalize_name(self, name):
return String(name.replace('.', '').replace(' ', '')).upper().strip()
-
+
def describe_cb(self, result):
"""
RPC return for device.describe()
@@ -355,7 +389,7 @@
break
if not remote.valid:
# remove the recording
- log.info('%s: remove %s' % (String(self.name),
String(remote.recording.name)))
+ log.info('%s: remove %s', self.name, remote.recording.name)
try:
rpc = self.rpc('home-theatre.vdr.remove',
self.start_recording_cb)
rpc.call(remote_id)
Modified: trunk/tvserver/src/recording.py
==============================================================================
--- trunk/tvserver/src/recording.py (original)
+++ trunk/tvserver/src/recording.py Sun Feb 5 20:35:39 2006
@@ -71,8 +71,7 @@
NEXT_ID = 0
def __init__(self, name = 'unknown', channel = 'unknown',
- priority = 0, start = 0, stop = 0, info = {},
- status = SCHEDULED ):
+ priority = 0, start = 0, stop = 0, **info ):
self.id = Recording.NEXT_ID
Recording.NEXT_ID += 1
@@ -82,7 +81,7 @@
self.priority = priority
self.start = start
self.stop = stop
- self.status = status
+ self.status = CONFLICT
self.info = {}
self.subtitle = ''
@@ -98,19 +97,16 @@
self.start_padding = config.record.start_padding
self.stop_padding = config.record.stop_padding
- for i in info:
- if i == 'subtitle':
- self.subtitle = Unicode(info[i])
- elif i == 'description':
- self.description = Unicode(info[i])
- elif i == 'url':
- self.url = String(info[i])
- elif i == 'start-padding':
- self.start_padding = int(info[i])
- elif i == 'stop-padding':
- self.stop_padding = int(info[i])
+ for key, value in info.items():
+ if key in ('subtitle', 'description'):
+ setattr(self, key, Unicode(value))
+ elif key == 'url':
+ self.url = String(value)
+ elif key in ('start-padding', 'stop_padding'):
+ setattr(self, key, int(value))
else:
- self.info[i] = Unicode(info[i])
+ self.info[key] = Unicode(value)
+
self.recorder = None
self.respect_start_padding = True
self.respect_stop_padding = True
Added: trunk/tvserver/src/scheduler.py
==============================================================================
--- (empty file)
+++ trunk/tvserver/src/scheduler.py Sun Feb 5 20:35:39 2006
@@ -0,0 +1,111 @@
+# -*- coding: iso-8859-1 -*-
+# -----------------------------------------------------------------------------
+# scheduler.py - Schedule future recordings to devices
+# -----------------------------------------------------------------------------
+# $Id: server.py 7893 2006-01-29 17:53:54Z dmeyer $
+#
+#
+# -----------------------------------------------------------------------------
+# Freevo - A Home Theater PC framework
+# Copyright (C) 2002-2005 Krister Lagerstrom, Dirk Meyer, et al.
+#
+# First Edition: Dirk Meyer <[EMAIL PROTECTED]>
+# Maintainer: Dirk Meyer <[EMAIL PROTECTED]>
+#
+# Please see the file doc/CREDITS for a complete list of authors.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MER-
+# CHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# -----------------------------------------------------------------------------
+
+# python imports
+import time
+import logging
+
+# record imports
+import recorder
+from record_types import *
+from conflict import Conflict
+
+# get logging object
+log = logging.getLogger('record')
+
+# set some extra debug
+log.setLevel(logging.DEBUG)
+
+
+class Scheduler(object):
+
+ def __init__(self, callback):
+ self.callback = callback
+ self.conflict = Conflict(self.conflict_callback)
+
+
+ def schedule(self, recordings):
+
+ # get current time
+ ctime = time.time()
+
+ # remeber data (before copy and deleting any)
+ self.recordings = recordings
+
+ # create a new list of recordings based on the status
+ recordings = [ r for r in recordings if r.status in \
+ (CONFLICT, SCHEDULED, RECORDING) ]
+
+ # new dict for schedule information. Each entry is r.status,
+ # r.recorder, r.respect_start_padding, r.respect_stop_padding
+ schedule = {}
+
+ # sort by start time
+ recordings.sort(lambda l, o: cmp(l.start,o.start))
+
+ for r in recordings[:]:
+ # check recordings we missed (stop passed or start over 10
+ # minutes ago), remember that in status and remove this
+ # recording from the list.
+ if r.stop < ctime or (r.start + 600 < ctime and r.status !=
RECORDING):
+ schedule[r.id] = [ MISSED, None, True, True ]
+ recordings.remove(r)
+
+ elif r.status == RECORDING:
+ # mark current running recordings
+ schedule[r.id] = [ r.status, r.recorder,
r.respect_start_padding, \
+ r.respect_stop_padding ]
+
+ else:
+ device = recorder.get_recorder(r.channel)
+ if device:
+ # set to the best recorder for each recording
+ schedule[r.id] = [ SCHEDULED, device, True, True ]
+ else:
+ # no recorder found, remove from the list
+ schedule[r.id] = [ CONFLICT, None, True, True ]
+ recordings.remove(r)
+
+ # recordings is a list fo current running or future recordings
+ # detect possible conflicts (delayed to avoid blocking the main loop)
+ self.conflict.scan(recordings, schedule)
+
+
+ def conflict_callback(self, schedule):
+ for r in self.recordings:
+ if r.id in schedule:
+ r.status, r.recorder, r.respect_start_padding, \
+ r.respect_stop_padding = schedule[r.id]
+ elif r.status in (CONFLICT, RECORDING, SCHEDULED):
+ log.error('missing info for %s', r)
+
+ self.callback()
Modified: trunk/tvserver/src/server.py
==============================================================================
--- trunk/tvserver/src/server.py (original)
+++ trunk/tvserver/src/server.py Sun Feb 5 20:35:39 2006
@@ -38,10 +38,8 @@
import time
import logging
-# kaa.epg
-import kaa.epg
import kaa.thumb
-from kaa.notifier import Timer, OneShotTimer
+from kaa.notifier import Timer, OneShotTimer, Callback, execute_in_timer
# freevo imports
import freevo.fxdparser
@@ -54,11 +52,15 @@
from record_types import *
from recording import Recording
from favorite import Favorite
-import conflict
+from scheduler import Scheduler
+from epg import EPG
# get logging object
log = logging.getLogger('record')
+# set some extra debug
+log.setLevel(logging.DEBUG)
+
class RecordServer(object):
"""
Class for the recordserver. It handles the rpc calls and checks the
@@ -72,19 +74,19 @@
# connect exposed functions
mbus.connect(self)
- # add notify callback
- mbus.signals['lost-entity'].connect(self.lost_entity)
-
# set status information
mbus.connect('freevo.ipc.status')
self.status = mbus.status
+ self.send_event = mbus.send_event
+
+ self.scheduler = Scheduler(self.scheduler_callback)
+ self.epg = EPG()
- self.clients = []
self.last_listing = []
self.live_tv_map = {}
# add port for channels and check if they are in live-tv mode
port = 6000
- for index, channel in enumerate(kaa.epg.channels):
+ for index, channel in enumerate(self.epg.channels()):
channel.port = port + index
channel.registered = []
@@ -93,16 +95,14 @@
# load the recordings file
self.load()
- # timer to handle save and print debug in background
- self.save_timer = OneShotTimer(self.save, False)
- self.ps_timer = OneShotTimer(self.print_schedule, False)
-
- # create recorder
- self.recorder = recorder.RecorderList(self)
-
- # start by checking the favorites
- self.check_current_recordings()
- self.check_favorites()
+ # connect to recorder signals and create recorder
+ recorder.signals['start-recording'].connect(self.start_recording)
+ recorder.signals['stop-recording'].connect(self.stop_recording)
+ recorder.signals['changed'].connect(self.reschedule)
+ recorder.connect()
+
+ # start by checking the recordings/favorites
+ self.epg_update()
# add schedule timer for SCHEDULE_TIMER / 3 seconds
Timer(self.schedule).start(SCHEDULE_TIMER / 3)
@@ -111,27 +111,12 @@
self.update_status()
Timer(self.update_status).start(60)
-
- def send_update(self, update):
- """
- Send and updated list to the clients
- """
- for c in self.clients:
- log.info('send update to %s' % c)
- c.send('home-theatre.record.list.update', *update)
- # save fxd file
- self.save()
-
- def print_schedule(self, schedule=True):
+ @execute_in_timer(OneShotTimer, 0.1, type='once')
+ def print_schedule(self):
"""
Print current schedule (for debug only)
"""
- if schedule:
- if not self.ps_timer.active():
- self.ps_timer.start(0.01)
- return
-
if hasattr(self, 'only_print_current'):
# print only latest recordings
all = False
@@ -154,67 +139,37 @@
info += '%s\n' % f
log.info(info)
-
- def check_recordings(self, force=False):
+
+ def reschedule(self):
"""
- Check the current recordings. This includes checking conflicts,
- removing old entries. At the end, the timer is set for the next
- recording.
+ Reschedule all recordings.
"""
- ctime = time.time()
- # remove informations older than one week
- self.recordings = filter(lambda r: r.start > ctime - 60*60*24*7,
- self.recordings)
- # sort by start time
- self.recordings.sort(lambda l, o: cmp(l.start,o.start))
-
- to_check = (CONFLICT, SCHEDULED, RECORDING)
-
- # check recordings we missed
- for r in self.recordings:
- if r.stop < ctime and r.status in to_check:
- r.status = MISSED
-
- # scan for conflicts
- next_recordings = filter(lambda r: r.stop + r.stop_padding > ctime \
- and r.status in to_check, self.recordings)
-
- for r in next_recordings:
- try:
- r.recorder = self.recorder.best_recorder[r.channel]
- if r.status != RECORDING:
- r.status = SCHEDULED
- r.respect_start_padding = True
- r.respect_stop_padding = True
- except KeyError:
- r.recorder = None
- r.status = CONFLICT
+ self.scheduler.schedule(self.recordings)
- if force:
- # clear conflict resolve cache
- conflict.clear_cache()
+
+ def scheduler_callback(self):
+ log.info('answer from scheduler')
- # Resolve conflicts. This will resolve the conflicts for the
- # next recordings now, the others will be resolved with a timer
- conflict.resolve(next_recordings, self.recorder)
-
# send update
sending = []
listing = []
+
for r in self.recordings:
short_list = r.short_list()
listing.append(short_list)
if not short_list in self.last_listing:
sending.append(short_list)
self.last_listing = listing
- self.send_update(sending)
+
+ # send update to all clients
+ self.send_event('home-theatre.record.list.update', *sending)
+
+ # save fxd file
+ self.save()
# print some debug
self.print_schedule()
- # sort by start time
- self.recordings.sort(lambda l, o: cmp(l.start,o.start))
-
# schedule recordings in recorder
self.schedule()
@@ -223,8 +178,17 @@
"""
Schedule recordings on recorder for the next SCHEDULE_TIMER seconds.
"""
- ctime = time.time()
log.info('calling self.schedule')
+ # sort by start time
+ self.recordings.sort(lambda l, o: cmp(l.start,o.start))
+
+ # get current time
+ ctime = time.time()
+
+ # remove old recorderings
+ self.recordings = filter(lambda r: r.start > ctime - 60*60*24*7,
+ self.recordings)
+ # schedule current (== now + SCHEDULE_TIMER) recordings
for r in self.recordings:
if r.start > ctime + SCHEDULE_TIMER:
# do not schedule to much in the future
@@ -236,149 +200,32 @@
return True
- def check_current_recordings(self):
+ def epg_update(self):
"""
- Check current recordings against the database/
+ Update recordings based on favorites and epg.
"""
- ctime = time.time() + 60 * 15
- recordings = filter(lambda r: r.start - r.start_padding > ctime \
- and r.status in (CONFLICT, SCHEDULED),
- self.recordings)
-
- # list of changes
- update = []
- for rec in recordings:
- # This could block the main loop. But we guess that there is
- # a reasonable number of future recordings, not 1000 recordings
- # that would block us here. Still, we need to find out if a very
- # huge database with over 100 channels will slow the database
- # down.
-
- # FIXME: This keeps the main loop alive but is ugly.
- # Change it to something better when kaa.epg is thread based
- kaa.notifier.step(False)
-
- # Search epg for that recording. The recording should be at the
- # same time, maybe it has moved +- 20 minutes. If the program
- # moved a larger time interval, it won't be found again.
- interval = (rec.start - 20 * 60, rec.start + 20 * 60)
- results = kaa.epg.search(rec.name, rec.channel, exact_match=True,
- interval = interval)
- epginfo = None
- changed = False
- for p in results:
- # check all results
- if p.start == rec.start and p.stop == rec.stop:
- # found the recording
- epginfo = p
- break
- else:
- # try to find it
- for p in results:
- if rec.start - 20 * 60 < p.start < rec.start + 20 * 60:
- # found it again, set new start and stop time
- old_info = str(rec)
- rec.start = p.start
- rec.stop = p.stop
- log.info('changed schedule\n%s\n%s' % (old_info, rec))
- changed = True
- epginfo = p
- break
- else:
- log.info('unable to find recording in epg:\n%s' % rec)
-
- if epginfo:
- # check if attributes changed
- if String(rec.description) != String(epginfo.description):
- log.info('description changed for %s' % String(rec.name))
- rec.description = epginfo.description
- if String(rec.episode) != String(epginfo.episode):
- log.info('episode changed for %s' % String(rec.name))
- rec.episode = epginfo.episode
- if String(rec.subtitle) != String(epginfo.subtitle):
- log.info('subtitle changed for %s' % String(rec.name))
- rec.subtitle = epginfo.subtitle
-
- if changed:
- update.append(rec.short_list())
-
- # send update about the recordings
- self.send_update(update)
-
-
- def check_favorites(self):
- """
- Check favorites against the database and add them to the list of
- recordings
- """
- t1 = time.time()
-
- update = []
-
- # Check current scheduled recordings if the start time has changed.
- # Only check recordings with start time greater 15 minutes from now
- # to avoid changing running recordings
- for f in copy.copy(self.favorites):
- # Now check all the favorites. Again, this could block but we
- # assume a reasonable number of favorites.
- for p in kaa.epg.search(f.name, exact_match=not f.substring):
-
- # FIXME: This keeps the main loop alive but is ugly.
- # Change it to something better when kaa.epg is thread based
- kaa.notifier.step(False)
-
- if not f.match(p.title, p.channel.id, p.start):
- continue
-
- r = Recording(p.title, p.channel.id, f.priority,
- p.start, p.stop)
- if r in self.recordings:
- # This does not only avoid adding recordings twice, it
- # also prevents from added a deleted favorite as active
- # again.
- continue
- r.episode = p.episode
- r.subtitle = p.subtitle
- r.description = p.description
- log.info('added %s: %s (%s)' % (String(p.channel.id),
- String(p.title), p.start))
- f.add_data(r)
- self.recordings.append(r)
- update.append(r.short_list())
- if f.once:
- self.favorites.remove(f)
- break
-
- t2 = time.time()
- log.info('check favorites took %s secs' % (t2-t1))
+ self.epg.check_all(self.favorites, self.recordings, self.reschedule)
+
- # send update about the new recordings
- self.send_update(update)
-
- # now check the schedule again
- self.check_recordings()
-
- t2 = time.time()
- log.info('everything scheduled after %s secs' % (t2-t1))
-
-
#
# load / save fxd file with recordings and favorites
#
- def __load_recording(self, parser, node):
+ def fxd_load_recording(self, parser, node):
"""
callback for <recording> in the fxd file
"""
try:
r = Recording()
r.parse_fxd(parser, node)
+ if r.status == SCHEDULED:
+ r.status = CONFLICT
self.recordings.append(r)
except Exception, e:
log.exception('recordserver.load_recording')
- def __load_favorite(self, parser, node):
+ def fxd_load_favorite(self, parser, node):
"""
callback for <favorite> in the fxd file
"""
@@ -398,22 +245,18 @@
self.favorites = []
try:
fxd = freevo.fxdparser.FXD(self.fxdfile)
- fxd.set_handler('recording', self.__load_recording)
- fxd.set_handler('favorite', self.__load_favorite)
+ fxd.set_handler('recording', self.fxd_load_recording)
+ fxd.set_handler('favorite', self.fxd_load_favorite)
fxd.parse()
except Exception, e:
log.exception('recordserver.load: %s corrupt:' % self.fxdfile)
- def save(self, schedule=True):
+ @execute_in_timer(OneShotTimer, 1, type='override')
+ def save(self):
"""
save the fxd file
"""
- if schedule:
- if not self.save_timer.active():
- self.save_timer.start(0.01)
- return
-
if not len(self.recordings) and not len(self.favorites):
# do not save here, it is a bug I havn't found yet
log.info('do not save fxd file')
@@ -433,7 +276,7 @@
#
- # function to change a status
+ # callbacks from the recorder
#
def start_recording(self, recording):
@@ -442,8 +285,10 @@
return
log.info('recording started')
recording.status = RECORDING
- # send update to mbus entities
- self.send_update([recording.short_list()])
+ # send update to all clients
+ self.send_event('home-theatre.record.list.update',
recording.short_list())
+ # save fxd file
+ self.save()
# create fxd file
recording.create_fxd()
# print some debug
@@ -478,25 +323,15 @@
log.info('failed: stopped %s secs to early' % \
(recording.stop - time.time()))
recording.status = FAILED
- # send update to mbus entities
- self.send_update([recording.short_list()])
+ # send update to all clients
+ self.send_event('home-theatre.record.list.update',
recording.short_list())
+ # save fxd file
+ self.save()
# print some debug
self.print_schedule()
#
- # global mbus stuff
- #
-
- def lost_entity(self, entity):
- if entity in self.clients:
- log.info('lost client %s' % entity)
- self.clients.remove(entity)
- return
- return
-
-
- #
# home.theatre.recording rpc commands
#
@@ -506,9 +341,6 @@
list the current recordins in a short form.
result: [ ( id channel priority start stop status ) (...) ]
"""
- if not source in self.clients:
- log.info('add client %s' % source)
- self.clients.append(source)
ret = []
for r in self.recordings:
ret.append(r.short_list())
@@ -538,20 +370,20 @@
info = dict(info)
log.info('recording.add: %s' % String(name))
- r = Recording(name, channel, priority, start, stop, info = info)
+ r = Recording(name, channel, priority, start, stop, **info)
if r in self.recordings:
r = self.recordings[self.recordings.index(r)]
if r.status == DELETED:
- r.status = SCHEDULED
+ r.status = CONFLICT
r.favorite = False
- # send update about the new recording
- self.send_update([r.short_list()])
- self.check_recordings()
+ # update schedule, this will also send an update to all
+ # clients registered.
+ self.reschedule()
return [ r.id ]
raise AttributeError('Already scheduled')
self.recordings.append(r)
- self.check_recordings()
+ self.reschedule()
return [ r.id - 1 ]
@@ -568,10 +400,9 @@
r.status = SAVED
else:
r.status = DELETED
- # send update about the new recording
- self.send_update([r.short_list()])
- # update listing
- self.check_recordings()
+ # update schedule, this will also send an update to all
+ # clients registered.
+ self.reschedule()
return []
raise IndexError('Recording not found')
@@ -592,10 +423,9 @@
for key in key_val:
setattr(cp, key, key_val[key])
self.recordings[self.recordings.index(r)] = cp
- # send update about the new recording
- self.send_update([r.short_list()])
- # update listing
- self.check_recordings()
+ # update schedule, this will also send an update to all
+ # clients registered.
+ self.reschedule()
return []
return IndexError('Recording not found')
@@ -610,7 +440,7 @@
live recording
parameter: channel
"""
- for c in kaa.epg.channels:
+ for c in self.epg.channels():
if c.id == channel:
channel = c
break
@@ -635,7 +465,7 @@
# FIXME: right now we take one recorder no matter if it is
# recording right now.
- rec = self.recorder.best_recorder[channel.id]
+ rec = recorder.get_recorder(channel.id)
if not rec:
return RuntimeError('no recorder for %s found' % channel.id)
@@ -690,8 +520,7 @@
"""
updates favorites with data from the database
"""
- self.check_current_recordings()
- self.check_favorites()
+ self.epg_update()
return []
@@ -717,9 +546,6 @@
def rpc_favorite_list(self, source):
"""
"""
- if not source in self.clients:
- log.info('add client %s' % source)
- self.clients.append(source)
ret = []
for f in self.favorites:
ret.append(f.long_list())
-------------------------------------------------------
This SF.net email is sponsored by: Splunk Inc. Do you grep through log files
for problems? Stop! Download the new AJAX search engine that makes
searching your log files as easy as surfing the web. DOWNLOAD SPLUNK!
http://sel.as-us.falkag.net/sel?cmd=lnk&kid=103432&bid=230486&dat=121642
_______________________________________________
Freevo-cvslog mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/freevo-cvslog