Author: duncan
Date: Fri Mar 23 18:35:37 2007
New Revision: 9377
Modified:
branches/rel-1/freevo/src/tv/plugins/recordings_manager.py
Log:
[ 1672003 ] TV Recordings Manager and automatic space reclaiming
Patch from Adam Charrett applied
Modified: branches/rel-1/freevo/src/tv/plugins/recordings_manager.py
==============================================================================
--- branches/rel-1/freevo/src/tv/plugins/recordings_manager.py (original)
+++ branches/rel-1/freevo/src/tv/plugins/recordings_manager.py Fri Mar 23
18:35:37 2007
@@ -27,7 +27,13 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# -----------------------------------------------------------------------
-
+"""
+Plugin to browse the TV recordings directory based on series rather than
+just a flat view of the recordings.
+Programs not in a series are placed at the top level, while programs that
+are a member of a series are placed in a series menu and the menu placed
+at the top level.
+"""
import os
import datetime
@@ -54,22 +60,27 @@
from menu import MenuItem, Menu
from video import VideoItem
+disk_manager = None
+
class PluginInterface(plugin.MainMenuPlugin):
"""
+ Plugin interface for the Manage Recordings TV menu and
+ the disk space reclaiming feature.
"""
def __init__(self):
"""
normal plugin init, but sets _type to 'mainmenu_tv'
"""
+ global disk_manager
plugin.MainMenuPlugin.__init__(self)
self._type = 'mainmenu_tv'
self.parent = None
- self.disk_manager = DiskManager(int(config.TVRM_MINIMUM_DISK_FREE) *
1024 * 1024)
+ disk_manager = DiskManager(int(config.TVRM_MINIMUM_DISK_FREE) * 1024 *
1024)
- plugin.register(self.disk_manager, "DiskManager")
- plugin.activate(self.disk_manager)
+ plugin.register(disk_manager, "DiskManager")
+ plugin.activate(disk_manager)
def config(self):
@@ -126,92 +137,41 @@
selected_id = self.menu.selected.id()
selected_pos = self.menu.choices.index(self.menu.selected)
- if config.OSD_BUSYICON_TIMER:
- osd.get_singleton().busyicon.wait(config.OSD_BUSYICON_TIMER[0])
-
- files = vfs.listdir(self.dir, include_overlay=True)
- num_changes = mediainfo.check_cache(self.dir)
-
- pop = None
- callback=None
- if num_changes > 10:
- pop = ProgressBox(text=_('Scanning recordings, be patient...'),
- full=num_changes)
- pop.show()
- callback=pop.tick
-
-
- elif config.OSD_BUSYICON_TIMER and len(files) >
config.OSD_BUSYICON_TIMER[1]:
- # many files, just show the busy icon now
- osd.get_singleton().busyicon.wait(0)
-
-
- if num_changes > 0:
- mediainfo.cache_dir(self.dir, callback=callback)
-
- series = {}
-
- recordings = fxditem.mimetype.get(self, files)
- for recorded_item in recordings:
- if series.has_key(recorded_item.name):
- series_items = series[recorded_item.name]
- else:
- series_items = []
- series[recorded_item.name] = series_items
- series_items.append(recorded_item)
-
- items = []
-
- for name,programs in series.iteritems():
- if len(programs) == 1:
- # Just one program in so don't bother to add it to a series
menu.
- items.append(RecordedProgramItem(programs[0].name,
programs[0]))
- else:
- # Create a series menu and add all the programs in order.
- items.append(Series(name, programs))
- items.sort(lambda l, o: cmp(o.sort('date+name').upper(),
l.sort('date+name').upper()))
+ segregated_recordings.sort(lambda l, o:
cmp(o.sort('date+name').upper(), l.sort('date+name').upper()))
- if pop:
- pop.destroy()
- # closing the poup will rebuild the menu which may umount
- # the drive
-
- if config.OSD_BUSYICON_TIMER:
- # stop the timer. If the icons is drawn, it will stay there
- # until the osd is redrawn, if not, we don't need it to pop
- # up the next milliseconds
- osd.get_singleton().busyicon.stop()
if arg == 'update':
- # update because of dirwatcher changes
- self.menu.choices = items
+ # update because of DiskManager
+ self.menu.choices = segregated_recordings
if selected_pos != -1:
- for i in items:
+ for i in segregated_recordings:
if Unicode(i.id()) == Unicode(selected_id):
self.menu.selected = i
break
else:
# item is gone now, try to the selection close
# to the old item
- pos = max(0, min(selected_pos-1, len(items)-1))
- if items:
- self.menu.selected = items[pos]
+ pos = max(0, min(selected_pos-1,
len(segregated_recordings)-1))
+ if segregated_recordings:
+ self.menu.selected = segregated_recordings[pos]
else:
self.menu.selected = None
if self.menu.selected and selected_pos != -1:
self.menuw.rebuild_page()
else:
self.menuw.init_page()
- self.menuw.refresh()
+ self.menuw.refresh()
else:
# normal menu build
- item_menu = Menu(self.name, items, reload_func=self.reload,
item_types = 'tv')
+ item_menu = Menu(self.name, segregated_recordings,
reload_func=self.reload, item_types = 'tv')
menuw.pushmenu(item_menu)
self.menu = item_menu
self.menuw = menuw
-
+
+ disk_manager.set_update_menu(self, menuw, None)
+
# ======================================================================
# Helper methods
# ======================================================================
@@ -253,6 +213,11 @@
watched = eval(watched)
self.watched = watched
+ try:
+ self.timestamp = float(self.video_item['recording_timestamp'])
+ except ValueError:
+ self.timestamp = 0.0
+
self.set_icon()
# ======================================================================
@@ -299,23 +264,8 @@
self.video_item.files.delete()
if self.menuw:
self.menuw.delete_submenu(True, True)
- try:
- # we also need to delete the item from the menu
- menu = self.menuw.menustack[-1]
- idx = menu.choices.index(self)
- # delete it from menu
- menu.choices.pop(idx)
- if len(menu.choices)>0:
- # if there are more items
- self.menuw.init_page()
- # refresh the menu
- self.menuw.refresh()
- else:
- # if menu is empty, remove it
- self.menuw.delete_menu()
- except ValueError, e:
- pass
-
+
+
def mark_to_keep(self, arg=None, menuw=None):
"""
Toggle whether this program should be kept.
@@ -337,6 +287,39 @@
if menuw:
copy_and_replace_menu_item(menuw, self)
+
+ def set_name_to_episode(self):
+ """
+ Set the name of this recorded program item to the name of
+ the episode of this program.
+ """
+ episode_name = self.video_item['tagline']
+ if not episode_name:
+ episode_name = self.video_item['subtitle']
+ if not episode_name:
+ try:
+ pat = re.compile(config.TVRM_EPISODE_FROM_PLOT)
+ episode = pat.match(self.video_item['plot']).group(1)
+ episode_name = episode.strip()
+ except Exception, e:
+ episode_name = None
+ if not episode_name and (self.timestamp > 0.0):
+ try:
+ episode = datetime.datetime.fromtimestamp(self.timestamp)
+ episode_name =
episode.strftime(config.TVRM_EPISODE_TIME_FORMAT)
+ except Exception, e:
+ episode_name = None
+ if not episode_name:
+ try:
+ episode =
datetime.datetime.fromtimestamp(os.path.getctime(self.video_item['filename']))
+ episode_name =
episode.strftime(config.TVRM_EPISODE_TIME_FORMAT)
+ except Exception, e:
+ episode_name = None
+ if not episode_name:
+ episode_name = _('(Unnamed)')
+ self.video_item['tagline'] = episode_name
+ self.name = episode_name
+
# ======================================================================
# Helper methods
# ======================================================================
@@ -356,7 +339,7 @@
fxd.info['tagline'] = fxd.str2XML(self.video_item['tagline'])
fxd.info['plot'] = fxd.str2XML(self.video_item['plot'])
fxd.info['runtime'] = self.video_item['length']
- fxd.info['recording_timestamp'] =
self.video_item['recording_timestamp']
+ fxd.info['recording_timestamp'] = self.timestamp
fxd.info['year'] = self.video_item['year']
fxd.info['watched'] = str(watched)
fxd.info['keep'] = str(keep)
@@ -389,10 +372,7 @@
Return a string to use to sort this item.
"""
if mode == 'date+name':
- try:
- return u'%010.0f%s' %
(float(self.video_item['recording_timestamp']), self.name)
- except ValueError:
- return u'%010.0f%s' % (0.0, self.name)
+ return u'%010.0f%s' % (self.timestamp, self.name)
return self.name
@@ -408,21 +388,13 @@
Class representing a set of programs with the same name, but (probably)
different taglines.
"""
- def __init__(self, name, programs):
+ def __init__(self, name, items):
Item.__init__(self, None, skin_type = 'tv')
self.set_url(None)
self.type = 'dir'
self.name = name
- self.programs = programs
- self.items = []
- for program in self.programs:
- tagline = self._get_episode_name(program)
- self.items.append(RecordedProgramItem(tagline, program))
- # TODO: Replace with smart sort that knows about 'n/m <subtitle>'
style names
- self.items.sort(lambda l, o: cmp(l.sort().upper(), o.sort().upper()))
- self.set_icon()
-
+ self.update(items)
# ======================================================================
@@ -443,12 +415,48 @@
"""
Browse the recorded programs in a series.
"""
- # normal menu build
- item_menu = Menu(self.name, self.items, item_types = 'tv')
- menuw.pushmenu(item_menu)
-
- self.menu = item_menu
- self.menuw = menuw
+ if arg == 'update':
+ # series has been deleted!
+ if not self.items:
+ rc.post_event(MENU_BACK_ONE_MENU)
+ return
+
+ if not self.menu.choices:
+ selected_pos = -1
+ else:
+ # store the current selected item
+ selected_id = self.menu.selected.id()
+ selected_pos = self.menu.choices.index(self.menu.selected)
+
+ # update because of DiskManager
+ self.menu.choices = self.items
+ if selected_pos != -1:
+ for i in self.items:
+ if Unicode(i.id()) == Unicode(selected_id):
+ self.menu.selected = i
+ break
+ else:
+ # item is gone now, try to the selection close
+ # to the old item
+ pos = max(0, min(selected_pos-1, len(self.items)-1))
+ if self.items:
+ self.menu.selected = self.items[pos]
+ else:
+ self.menu.selected = None
+ if self.menu.selected and selected_pos != -1:
+ self.menuw.rebuild_page()
+ else:
+ self.menuw.init_page()
+ self.menuw.refresh()
+ else:
+ # normal menu build
+ item_menu = Menu(self.name, self.items, item_types = 'tv')
+ menuw.pushmenu(item_menu)
+
+ self.menu = item_menu
+ self.menuw = menuw
+
+ disk_manager.set_update_menu(self, menuw, None)
def play_all(self, arg=None, menuw=None):
@@ -490,6 +498,20 @@
if menuw:
copy_and_replace_menu_item(menuw, self)
+
+ def update(self, items):
+ """
+ Update the list of programs that make up this series.
+ """
+ self.items = items
+ for item in self.items:
+ item.set_name_to_episode()
+
+ # TODO: Replace with smart sort that knows about 'n/m <subtitle>'
style names
+ self.items.sort(lambda l, o: cmp(l.sort().upper(), o.sort().upper()))
+ self.set_icon()
+
+
# ======================================================================
# Helper methods
# ======================================================================
@@ -515,48 +537,17 @@
self.icon = config.ICON_DIR + '/status/series_unwatched.png'
- def _get_episode_name(self, program):
- episode_name = program['tagline']
- if not episode_name:
- episode_name = program['subtitle']
- if not episode_name:
- try:
- pat = re.compile(config.TVRM_EPISODE_FROM_PLOT)
- episode = pat.match(program['plot']).group(1)
- episode_name = episode.strip()
- except Exception, e:
- episode_name = None
- if not episode_name:
- try:
- episode = datetime.datetime.fromtimestamp(
- float(program['recording_timestamp']))
- episode_name =
episode.strftime(config.TVRM_EPISODE_TIME_FORMAT)
- except Exception, e:
- episode_name = None
- if not episode_name:
- try:
- episode = datetime.datetime.fromtimestamp(
- os.path.getctime(program['filename']))
- episode_name =
episode.strftime(config.TVRM_EPISODE_TIME_FORMAT)
- except Exception, e:
- episode_name = None
- if not episode_name:
- episode_name = _('(Unnamed)')
- program['tagline'] = episode_name
- return Unicode(program['tagline'])
-
-
def __getitem__(self, key):
"""
Returns the number of episodes when
"""
if key == 'tagline':
- return unicode('%d ' % len(self.programs)) + _('episodes')
+ return unicode('%d ' % len(self.items)) + _('episodes')
if key == 'content':
content = ''
- for i in range(0, len(self.programs)):
- content += self._get_episode_name(self.programs[i])
- if i < (len(self.programs) - 1):
+ for i in range(0, len(self.items)):
+ content += self.items[i].name
+ if i < (len(self.items) - 1):
content += ', '
return content
@@ -569,10 +560,9 @@
"""
if mode == 'date+name':
latest_timestamp = 0.0
- for program in self.programs:
- timestamp = float(program['recording_timestamp'])
- if timestamp > latest_timestamp:
- latest_timestamp = timestamp
+ for item in self.items:
+ if item.timestamp > latest_timestamp:
+ latest_timestamp = item.timestamp
return u'%10d%s' % (int(latest_timestamp), self.name)
return self.name
@@ -585,21 +575,129 @@
# Disk Management Class
# ======================================================================
+series_table = {}
+segregated_recordings = []
+all_recordings = []
+
class DiskManager(plugin.DaemonPlugin):
"""
Class to ensure a minimum amount of disk space is always available.
"""
def __init__(self, required_space):
plugin.DaemonPlugin.__init__(self)
- self.poll_interval = 10 # Once a second
+ self.poll_interval = 5 # half a second
self.poll_menu_only = False
self.required_space = required_space
+ self.last_time = 0;
+ self.menu = None
+ self.menuw = None
+ self.interested_series = None
+
+ self.update_recordings()
def poll(self):
"""
Check that the available disk space is greater than
TVRM_MINIMUM_DISK_FREE
"""
+ # Check space first so that any removals are picked up straight away.
+ self.check_space()
+
+ self.check_recordings()
+
+ def set_update_menu(self, menu, menuw, interested_series):
+ """
+ Set the menu to update when the recordings directory changes.
+ """
+ self.menuw = menuw
+ self.interested_series = interested_series
+ self.menu = menu
+
+
+ def check_recordings(self):
+ """
+ Check the TV recordings directory to determine if the contents have
changed,
+ and if they have update the list of recordings and the currently
registered
+ menu.
+ """
+ changed = False
+ try:
+ if vfs.mtime(config.TV_RECORD_DIR) > self.last_time:
+ changed = True
+ except (OSError, IOError):
+ # the directory is gone
+ _debug_('DiskManager: unable to read recordings directory')
+ return
+
+ if changed:
+ self.last_time = vfs.mtime(config.TV_RECORD_DIR)
+ self.update_recordings()
+ if self.menu:
+ self.menu.browse(menuw=self.menuw, arg='update')
+
+ def update_recordings(self):
+ """
+ Update the list of recordings.
+ """
+ global all_recordings, segregated_recordings, series_table
+ files = vfs.listdir(config.TV_RECORD_DIR, include_overlay=True)
+ num_changes = mediainfo.check_cache(config.TV_RECORD_DIR)
+
+ if num_changes > 0:
+ mediainfo.cache_dir(config.TV_RECORD_DIR, callback=None)
+
+ series = {}
+
+ recordings = fxditem.mimetype.get(None, files)
+ for recorded_item in recordings:
+ rpitem = RecordedProgramItem(recorded_item.name, recorded_item)
+
+ if series.has_key(recorded_item.name):
+ series_items = series[recorded_item.name]
+ else:
+ series_items = []
+ series[recorded_item.name] = series_items
+ series_items.append(rpitem)
+
+ items = []
+ all_items = []
+ for name,programs in series.iteritems():
+ if len(programs) == 1:
+ # Just one program in so don't bother to add it to a series
menu.
+ item = programs[0]
+ if name in series_table:
+ series_item = series_table[name]
+ series_item.items = None
+ del series_table[name]
+ else:
+ if name in series_table:
+ item = series_table[name]
+ item.update(programs)
+ else:
+ # Create a series menu and add all the programs in order.
+ item = Series(name, programs)
+ series_table[name] = item
+
+ items.append(item)
+ all_items += programs
+
+ # Clean the series table of series that no longer exist
+ for name,series_item in series_table.items():
+ if not series_item in items:
+ series_item.items = None
+ del series_table[name]
+
+ segregated_recordings = items
+ all_recordings = all_items
+
+ self.last_time = vfs.mtime(config.TV_RECORD_DIR)
+
+
+ def check_space(self):
+ """
+ Check the amount of disk space has not dropped below the minimum
required.
+ If it has attempt to remove some recordings.
+ """
if util.freespace(config.TV_RECORD_DIR) < self.required_space:
print 'Need to free up some space now!'
candidates = self.generate_candidates()
@@ -612,48 +710,29 @@
def generate_candidates(self):
- files = vfs.listdir(config.TV_RECORD_DIR, include_overlay=True)
- num_changes = mediainfo.check_cache(config.TV_RECORD_DIR)
-
watched_candidates = []
unwatched_candidates = []
today = datetime.date.today()
- recordings = fxditem.mimetype.get(None, files)
- for recorded_item in recordings:
- watched, keep = self.candidate_status(recorded_item)
- if watched and not keep:
+ for recorded_item in all_recordings:
+ if recorded_item.watched and not recorded_item.keep:
watched_candidates.append(recorded_item)
- if not watched and not keep:
- recorded_date =
datetime.date.fromtimestamp(float(recorded_item['recording_timestamp']))
+
+ elif not recorded_item.watched and not recorded_item.keep:
+ recorded_date =
datetime.date.fromtimestamp(recorded_item.timestamp)
timediff = today - recorded_date
+
if (timediff.days > config.TVRM_CONSIDER_UNWATCHED_AFTER):
unwatched_candidates.append(recorded_item)
# Now sort the recordings so the oldest one is first.
- watched_candidates.sort(lambda l, o:
cmp(float(l['recording_timestamp']), float(o['recording_timestamp'])))
- unwatched_candidates.sort(lambda l, o:
cmp(float(l['recording_timestamp']), float(o['recording_timestamp'])))
+ watched_candidates.sort(lambda l, o: cmp(l.timestamp, o.timestamp))
+ unwatched_candidates.sort(lambda l, o: cmp(l.timestamp, o.timestamp))
return watched_candidates + unwatched_candidates
-
- def candidate_status(self, candidate):
- keep = candidate['keep']
- if not keep:
- keep = False
- else:
- keep = eval(keep)
-
- watched = candidate['watched']
- if not watched:
- watched = False
- else:
- watched = eval(watched)
-
- return (watched, keep)
-
-
-
+
+
# ======================================================================
# Helper functions
# ======================================================================
-------------------------------------------------------------------------
Take Surveys. Earn Cash. Influence the Future of IT
Join SourceForge.net's Techsay panel and you'll get the chance to share your
opinions on IT & business topics through brief surveys-and earn cash
http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV
_______________________________________________
Freevo-cvslog mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/freevo-cvslog