Author: duncan
Date: Sun Mar 4 17:51:51 2007
New Revision: 9301
Added:
branches/rel-1/freevo/share/icons/status/series_keep.png (contents, props
changed)
branches/rel-1/freevo/share/icons/status/series_unwatched.png (contents,
props changed)
branches/rel-1/freevo/share/icons/status/series_watched.png (contents,
props changed)
branches/rel-1/freevo/share/icons/status/television_keep.png (contents,
props changed)
branches/rel-1/freevo/share/icons/status/television_unwatched.png
(contents, props changed)
branches/rel-1/freevo/share/icons/status/television_watched.png (contents,
props changed)
branches/rel-1/freevo/src/tv/plugins/recordings_manager.py (contents,
props changed)
branches/rel-1/freevo/src/tv/plugins/view_recordings.py (contents, props
changed)
Modified:
branches/rel-1/freevo/freevo_config.py
branches/rel-1/freevo/src/tv/tvmenu.py
Log:
[ 1672003 ] TV Recordings Manager and automatic space reclaiming
Feature by Adam Charrett added
Modified: branches/rel-1/freevo/freevo_config.py
==============================================================================
--- branches/rel-1/freevo/freevo_config.py (original)
+++ branches/rel-1/freevo/freevo_config.py Sun Mar 4 17:51:51 2007
@@ -728,6 +728,9 @@
# TV menu plugin to view scheduled recordings
plugin.activate('tv.scheduled_recordings')
+# TV menu plugin to view recordings
+plugin.activate('tv.view_recordings')
+
# TV menu plugin to view and edit favorites
plugin.activate('tv.view_favorites')
Added: branches/rel-1/freevo/share/icons/status/series_keep.png
==============================================================================
Binary file. No diff available.
Added: branches/rel-1/freevo/share/icons/status/series_unwatched.png
==============================================================================
Binary file. No diff available.
Added: branches/rel-1/freevo/share/icons/status/series_watched.png
==============================================================================
Binary file. No diff available.
Added: branches/rel-1/freevo/share/icons/status/television_keep.png
==============================================================================
Binary file. No diff available.
Added: branches/rel-1/freevo/share/icons/status/television_unwatched.png
==============================================================================
Binary file. No diff available.
Added: branches/rel-1/freevo/share/icons/status/television_watched.png
==============================================================================
Binary file. No diff available.
Added: branches/rel-1/freevo/src/tv/plugins/recordings_manager.py
==============================================================================
--- (empty file)
+++ branches/rel-1/freevo/src/tv/plugins/recordings_manager.py Sun Mar 4
17:51:51 2007
@@ -0,0 +1,632 @@
+# -*- coding: iso-8859-1 -*-
+# -----------------------------------------------------------------------
+# view_recordings.py - Directory handling
+# -----------------------------------------------------------------------
+# $Id$
+#
+# Notes:
+# Todo:
+#
+# -----------------------------------------------------------------------
+# $Log$
+#
+# -----------------------------------------------------------------------
+# Freevo - A Home Theater PC framework
+# Copyright (C) 2002 Krister Lagerstrom, et al.
+# Please see the file freevo/Docs/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
+#
+# ----------------------------------------------------------------------- */
+
+
+import os
+import datetime
+import traceback
+import re
+import stat
+import copy
+import rc
+import util.mediainfo as mediainfo
+
+import config
+import util
+
+
+import skin
+import plugin
+import osd
+import fxditem
+
+from item import Item, FileInformation
+from playlist import Playlist
+from event import *
+from gui import InputBox, AlertBox, ProgressBox
+from menu import MenuItem, Menu
+from video import VideoItem
+
+class PluginInterface(plugin.MainMenuPlugin):
+ """
+ """
+ def __init__(self):
+ """
+ normal plugin init, but sets _type to 'mainmenu_tv'
+ """
+ plugin.MainMenuPlugin.__init__(self)
+
+ self._type = 'mainmenu_tv'
+ self.parent = None
+
+ self.disk_manager = DiskManager(int(config.TVRM_MINIMUM_DISK_FREE) *
1024 * 1024)
+
+ plugin.register(self.disk_manager, "DiskManager")
+ plugin.activate(self.disk_manager)
+
+
+ def config(self):
+ return [
+ ('TVRM_MINIMUM_DISK_FREE', 2048, 'Minimum amount of disk space
that must be available at all times in MB'),
+ ('TVRM_CONSIDER_UNWATCHED_AFTER', 45, 'Number of days after which
to consider deleting unwatched shows if space is required'),
+ ('TVRM_EPISODE_FROM_PLOT', None, 'Regular expression to extract
the episode name from the plot'),
+ ]
+
+
+ def items(self, parent):
+ self.parent = parent
+ return [RecordingsDirectory(parent)]
+
+# ======================================================================
+# Recordings Directory Browsing Class
+# ======================================================================
+class RecordingsDirectory(Item):
+ """
+ class for browsing the TV Record directory
+ """
+ def __init__(self, parent):
+ Item.__init__(self, parent, skin_type='tv')
+ self.name = _('Manage Recordings')
+ self.dir = config.TV_RECORD_DIR
+
+ # ======================================================================
+ # actions
+ # ======================================================================
+
+ def actions(self):
+ """
+ return a list of actions for this item
+ """
+ items = [ ( self.browse, _('Browse directory')) ]
+
+ return items
+
+
+ def browse(self, arg=None, menuw=None):
+ """
+ build the items for the directory
+ """
+ if not os.path.exists(self.dir):
+ AlertBox(text=_('Recordings Directory does not exist')).show()
+ return
+
+ if arg == 'update':
+ 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)
+
+ 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()))
+
+ 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
+ if selected_pos != -1:
+ for i in 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(items)-1))
+ if items:
+ self.menu.selected = 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, items, reload_func=self.reload,
item_types = 'tv')
+ menuw.pushmenu(item_menu)
+
+ self.menu = item_menu
+ self.menuw = menuw
+
+ # ======================================================================
+ # Helper methods
+ # ======================================================================
+
+ def reload(self):
+ """
+ Rebuilds the menu.
+ """
+ self.browse(arg='update')
+ return None
+
+
+# ======================================================================
+# Program Class
+# ======================================================================
+
+class RecordedProgramItem(Item):
+ """
+ Class to represent a recorded TV program.
+ """
+ def __init__(self, name, video_item):
+ Item.__init__(self, None, skin_type = 'tv')
+ self.set_url(None)
+ self.type='video'
+ self.name = name
+ self.video_item = video_item
+
+ keep = self.video_item['keep']
+ if not keep:
+ keep = False
+ else:
+ keep = eval(keep)
+ self.keep = keep
+
+ watched = self.video_item['watched']
+ if not watched:
+ watched = False
+ else:
+ watched = eval(watched)
+ self.watched = watched
+
+ self.set_icon()
+
+ # ======================================================================
+ # actions
+ # ======================================================================
+
+ def actions(self):
+ """
+ return the default action
+ """
+ return [ ( self.play, _('Play') ),
+ ( self.confirm_delete, _('Delete')),
+ ( self.mark_to_keep, _('(Un)Mark to Keep')),
+ ( self.mark_as_watched, _('(Un)Mark as Watched'))]
+
+
+ def play(self, arg=None, menuw=None):
+ """
+ Play the recorded program, and then mark it as watched.
+ """
+ self.video_item.play(menuw=menuw)
+
+ # Mark this programme as watched.
+ self.update_fxd( True, self.keep)
+ self.set_icon()
+
+
+
+ def confirm_delete(self, arg=None, menuw=None):
+ """
+ Confirm whether the user really wants to delete this program.
+ """
+ self.menuw = menuw
+ ConfirmBox(text=_('Do you wish to delete\n \'%s\'?') % self.name,
+ handler=self.delete, default_choice=1,
+ handler_message=_('Deleting...')).show()
+
+
+ def delete(self):
+ """
+ Delete the recorded program.
+ """
+ self.video_item.files.delete()
+ if self.menuw:
+ self.menuw.delete_submenu(True, True)
+
+
+
+ def mark_to_keep(self, arg=None, menuw=None):
+ """
+ Toggle whether this program should be kept.
+ """
+ self.keep = not self.keep
+ self.update_fxd(self.watched, self.keep)
+ self.set_icon()
+ if menuw:
+ copy_and_replace_menu_item(menuw, self)
+
+
+ def mark_as_watched(self, arg=None, menuw=None):
+ """
+ Toggle whether this program has been watched.
+ """
+ self.watched = not self.watched
+ self.update_fxd(self.watched, self.keep)
+ self.set_icon()
+ if menuw:
+ copy_and_replace_menu_item(menuw, self)
+
+ # ======================================================================
+ # Helper methods
+ # ======================================================================
+
+ def update_fxd(self, watched=False, keep=False):
+ """
+ Update the programs fxd file.
+ """
+ from util.fxdimdb import FxdImdb, makeVideo
+ fxd = FxdImdb()
+
+ (filebase, fileext) = os.path.splitext(self.video_item.filename)
+ fxd.setFxdFile(filebase, overwrite=TRUE)
+
+ video = makeVideo('file', 'f1', self.video_item.filename)
+ fxd.setVideo(video)
+ 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['year'] = self.video_item['year']
+ fxd.info['watched'] = str(watched)
+ fxd.info['keep'] = str(keep)
+ fxd.title = self.video_item.name
+ fxd.writeFxd()
+
+
+ def set_icon(self):
+ """
+ Set the image displayed next to the menu item text, based on whether
+ the program is being kept or has been watched.
+ """
+ if self.keep:
+ self.icon = config.ICON_DIR + '/status/television_keep.png'
+ elif self.watched:
+ self.icon = config.ICON_DIR + '/status/television_watched.png'
+ else:
+ self.icon = config.ICON_DIR + '/status/television_unwatched.png'
+
+
+ def __getitem__(self, key):
+ """
+ Map through to the underlying VideoItem
+ """
+ return self.video_item[key]
+
+
+ def sort(self, mode=None):
+ """
+ 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 self.name
+
+# ======================================================================
+# Series Menu Class
+# ======================================================================
+class Series(Item):
+ """
+ Class representing a set of programs with the same name, but (probably)
+ different taglines.
+ """
+ def __init__(self, name, programs):
+ 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:
+ self.items.append(RecordedProgramItem(program['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()
+
+
+
+ # ======================================================================
+ # actions
+ # ======================================================================
+
+ def actions(self):
+ """
+ return the default action
+ """
+ return [ ( self.browse, _('Browse episodes')),
+ ( self.delete_all, _('Delete all episodes')),
+ ( self.mark_all_to_keep, _('Keep all episodes')),
+ ( self.play_all, _('Play all episodes') )]
+
+
+ def browse(self, arg=None, menuw=None):
+ """
+ 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
+
+
+ def play_all(self, arg=None, menuw=None):
+ """
+ Play all programs in a series.
+ """
+ # TODO: Implement!
+ pass
+
+
+ def confirm_delete(self, arg=None, menuw=None):
+ """
+ Confirm the user wants to delete an entire series.
+ """
+ self.menuw = menuw
+ ConfirmBox(text=_('Do you wish to delete the series\n \'%s\'?') %
self.name,
+ handler=self.delete_all, default_choice=1,
+ handler_message=_('Deleting...')).show()
+
+
+ def delete_all(self):
+ """
+ Delete all programs in a series.
+ """
+ for item in self.items:
+ item.delete()
+ if self.menuw:
+ self.menuw.delete_submenu(True, True)
+
+
+ def mark_all_to_keep(self, arg=None, menuw=None):
+ """
+ Mark all programs in a series to keep.
+ """
+ for item in self.items:
+ if not item.keep:
+ item.mark_to_keep()
+ self.set_icon()
+ if menuw:
+ copy_and_replace_menu_item(menuw, self)
+
+ # ======================================================================
+ # Helper methods
+ # ======================================================================
+
+ def set_icon(self):
+ """
+ Set the image displayed next to the menu item text, based on whether
+ the series is being kept or has been watched.
+ """
+ keep = True
+ watched = True
+
+ for item in self.items:
+ if not item.keep:
+ keep = False
+ if not item.watched:
+ watched = False
+ if keep:
+ self.icon = config.ICON_DIR + '/status/series_keep.png'
+ elif watched:
+ self.icon = config.ICON_DIR + '/status/series_watched.png'
+ else:
+ self.icon = config.ICON_DIR + '/status/series_unwatched.png'
+
+
+ def __getitem__(self, key):
+ """
+ Returns the number of episodes when
+ """
+ if key == 'tagline':
+ return unicode('%d ' % len(self.programs)) + _('episodes')
+ if key == 'content':
+ content = ''
+ for i in range(0, len(self.programs)):
+ episode_name = self.programs[i]['tagline']
+ if not episode_name:
+ episode_name = self.programs[i]['subtitle']
+ self.programs[i]['tagline'] = episode_name
+ if not episode_name:
+ try:
+ pat = re.compile(config.TVRM_EPISODE_FROM_PLOT)
+ episode_name =
pat.match(self.programs[i]['plot']).group(1)
+ self.programs[i]['tagline'] = episode_name.strip()
+ except Exception, e:
+ print self.programs[i]['name'], e
+ if not episode_name:
+ episode_name = _('(Unnamed)')
+ self.programs[i]['tagline'] = episode_name
+ content += episode_name
+ if i < (len(self.programs) - 1):
+ content += ', '
+ return content
+
+ return Item.__getitem__(self,key)
+
+
+ def sort(self, mode=None):
+ """
+ Return a string to use to sort this item.
+ """
+ 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
+ return u'%10d%s' % (int(latest_timestamp), self.name)
+
+ return self.name
+
+
+# ======================================================================
+# Disk Management Class
+# ======================================================================
+
+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_menu_only = False
+ self.required_space = required_space
+
+
+ def poll(self):
+ """
+ Check that the available disk space is greater than
TVRM_MINIMUM_DISK_FREE
+ """
+ if util.freespace(config.TV_RECORD_DIR) < self.required_space:
+ print 'Need to free up some space now!'
+ candidates = self.generate_candidates()
+
+ while (util.freespace(config.TV_RECORD_DIR) < self.required_space)
and (len(candidates) > 0):
+ # Delete a candidate
+ candidate = candidates.pop(0)
+ watched, keep = self.candidate_status(candidate)
+ print 'Deleting %s (watched %s, keep %s timestamp %s)' %
(candidate.name, watched, keep, candidate['recording_timestamp'])
+ #candidates.files.delete()
+
+
+ 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:
+ watched_candidates.append(recorded_item)
+ if not watched and not keep:
+ recorded_date =
datetime.date.fromtimestamp(float(recorded_item['recording_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'])))
+
+ 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
+# ======================================================================
+def copy_and_replace_menu_item(menuw, item):
+ cloned_item = copy.copy(item)
+ menu = menuw.menustack[-1]
+ # rebuild menu
+ menu.choices[menu.choices.index(item)] = cloned_item
+ if menu.selected is item:
+ menu.selected = cloned_item
+
+ menuw.init_page()
+ menuw.refresh()
Added: branches/rel-1/freevo/src/tv/plugins/view_recordings.py
==============================================================================
--- (empty file)
+++ branches/rel-1/freevo/src/tv/plugins/view_recordings.py Sun Mar 4
17:51:51 2007
@@ -0,0 +1,50 @@
+# -*- coding: iso-8859-1 -*-
+# -----------------------------------------------------------------------
+# view_recordings.py - View the TV recordings directory
+# -----------------------------------------------------------------------
+# $Id$
+#
+# Notes:
+# Todo:
+#
+# -----------------------------------------------------------------------
+# $Log$
+#
+# -----------------------------------------------------------------------
+# Freevo - A Home Theater PC framework
+# Copyright (C) 2002 Krister Lagerstrom, et al.
+# Please see the file freevo/Docs/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
+#
+# ----------------------------------------------------------------------- */
+
+import config
+import plugin
+from directory import DirItem
+
+class PluginInterface(plugin.Plugin):
+ """
+ """
+ def __init__(self):
+ """
+ normal plugin init, but sets _type to 'mainmenu_tv'
+ """
+ plugin.Plugin.__init__(self)
+ self._type = 'mainmenu_tv'
+
+ def items(self, parent):
+ return [DirItem(config.TV_RECORD_DIR, None, name = _('Recorded Shows'),
+ display_type='tv')]
Modified: branches/rel-1/freevo/src/tv/tvmenu.py
==============================================================================
--- branches/rel-1/freevo/src/tv/tvmenu.py (original)
+++ branches/rel-1/freevo/src/tv/tvmenu.py Sun Mar 4 17:51:51 2007
@@ -101,17 +101,11 @@
if config.TV_CHANNELS:
items.append(menu.MenuItem(_('TV Guide'),
action=self.start_tvguide))
- items.append(DirItem(config.TV_RECORD_DIR, None, name = _('Recorded
Shows'),
- display_type='tv'))
-
- # XXX: these are becomming plugins
- # items.append(menu.MenuItem(_('Search Guide'),
action=self.show_search))
-
plugins_list = plugin.get('mainmenu_tv')
for p in plugins_list:
items += p.items(self)
- menuw.pushmenu(menu.Menu(_('TV Main Menu'), items, item_types = 'tv
main menu'))
+ menuw.pushmenu(menu.Menu(_('TV Main Menu'), items, item_types='tv main
menu'))
def show_search(self, arg, menuw):
-------------------------------------------------------------------------
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