Determine episode duration (bug 811)

This patch adds two methods for detecting the
episode length (time units, not bytes):

 * iTunes-specific "duration" in the RSS feed
 * GStreamer-based length detection after download

The patch also adds duration information to the
tooltip in the episode list as a first step for
displaying this information in the UI.
This commit is contained in:
Thomas Perl 2010-06-04 20:43:38 +02:00
parent 7917c78ee8
commit 7c20ffd167
3 changed files with 85 additions and 9 deletions

View file

@ -237,25 +237,25 @@ class EpisodeListModel(gtk.ListStore):
show_missing = False show_missing = False
status_icon = None status_icon = None
status_icon_to_build_from_file = False status_icon_to_build_from_file = False
tooltip = '' tooltip = []
view_show_undeleted = True view_show_undeleted = True
view_show_downloaded = False view_show_downloaded = False
view_show_unplayed = False view_show_unplayed = False
icon_theme = gtk.icon_theme_get_default() icon_theme = gtk.icon_theme_get_default()
if downloading is not None and downloading(episode): if downloading is not None and downloading(episode):
tooltip = _('Downloading') tooltip.append(_('Downloading'))
status_icon = self.ICON_DOWNLOADING status_icon = self.ICON_DOWNLOADING
view_show_downloaded = True view_show_downloaded = True
view_show_unplayed = True view_show_unplayed = True
else: else:
if episode.state == gpodder.STATE_DELETED: if episode.state == gpodder.STATE_DELETED:
tooltip = _('Deleted') tooltip.append(_('Deleted'))
status_icon = self.ICON_DELETED status_icon = self.ICON_DELETED
view_show_undeleted = False view_show_undeleted = False
elif episode.state == gpodder.STATE_NORMAL and \ elif episode.state == gpodder.STATE_NORMAL and \
not episode.is_played: not episode.is_played:
tooltip = _('New episode') tooltip.append(_('New episode'))
status_icon = self.ICON_NEW status_icon = self.ICON_NEW
view_show_downloaded = True view_show_downloaded = True
view_show_unplayed = True view_show_unplayed = True
@ -322,9 +322,14 @@ class EpisodeListModel(gtk.ListStore):
tooltip.append(_('deletion prevented')) tooltip.append(_('deletion prevented'))
if episode.total_time > 0 and episode.current_position: if episode.total_time > 0 and episode.current_position:
tooltip.append('%d%%' % (100.*float(episode.current_position)/float(episode.total_time))) tooltip.append('%d%%' % (100.*float(episode.current_position)/float(episode.total_time),))
tooltip = ', '.join(tooltip) if episode.total_time:
total_time = util.format_time(episode.total_time)
if total_time:
tooltip.append(total_time)
tooltip = ', '.join(tooltip)
if status_icon is not None: if status_icon is not None:
status_icon = self._get_tree_icon(status_icon, show_bullet, \ status_icon = self._get_tree_icon(status_icon, show_bullet, \

View file

@ -28,6 +28,7 @@ from gpodder import util
from gpodder import feedcore from gpodder import feedcore
from gpodder import youtube from gpodder import youtube
from gpodder import corestats from gpodder import corestats
from gpodder import gstreamer
from gpodder.liblogger import log from gpodder.liblogger import log
@ -690,6 +691,13 @@ class PodcastEpisode(PodcastModelObject):
episode.link = entry.get('link', '') episode.link = entry.get('link', '')
episode.description = entry.get('summary', '') episode.description = entry.get('summary', '')
try:
# Parse iTunes-specific podcast duration metadata
total_time = util.parse_time(entry.get('itunes_duration', ''))
episode.total_time = total_time
except:
pass
# Fallback to subtitle if summary is not available0 # Fallback to subtitle if summary is not available0
if not episode.description: if not episode.description:
episode.description = entry.get('subtitle', '') episode.description = entry.get('subtitle', '')
@ -804,7 +812,7 @@ class PodcastEpisode(PodcastModelObject):
# Time attributes # Time attributes
self.total_time = 0 self.total_time = 0
self.current_position = 0 self.current_position = 0
self.current_position_updated = time.time() self.current_position_updated = 0
def get_is_locked(self): def get_is_locked(self):
return self._is_locked return self._is_locked
@ -823,6 +831,22 @@ class PodcastEpisode(PodcastModelObject):
self.state = gpodder.STATE_DOWNLOADED self.state = gpodder.STATE_DOWNLOADED
self.is_played = False self.is_played = False
self.length = os.path.getsize(filename) self.length = os.path.getsize(filename)
if not self.total_time:
try:
length = gstreamer.get_track_length(filename)
if length is not None:
length = int(length/1000)
log('Detected media length: %d seconds', length, \
sender=self)
self.total_time = length
self.db.save_episode(self)
self.db.commit()
return
except Exception, e:
log('Error while detecting media length: %s', str(e), \
sender=self)
self.db.save_downloaded_episode(self) self.db.save_downloaded_episode(self)
self.db.commit() self.db.commit()

View file

@ -1039,8 +1039,55 @@ def bluetooth_send_file(filename):
else: else:
log('Cannot send file. Please install "bluetooth-sendto" or "gnome-obex-send".') log('Cannot send file. Please install "bluetooth-sendto" or "gnome-obex-send".')
return False return False
def format_time(value):
"""Format a seconds value to a string
>>> format_time(0)
'00:00'
>>> format_time(20)
'00:20'
>>> format_time(3600)
'01:00:00'
>>> format_time(10921)
'03:02:01'
"""
dt = datetime.datetime.utcfromtimestamp(value)
if dt.hour == 0:
return dt.strftime('%M:%S')
else:
return dt.strftime('%H:%M:%S')
def parse_time(value):
"""Parse a time string into seconds
>>> parse_time('00:00')
0
>>> parse_time('00:00:00')
0
>>> parse_time('00:20')
20
>>> parse_time('00:00:20')
20
>>> parse_time('01:00:00')
3600
>>> parse_time('03:02:01')
10921
"""
if not value:
raise ValueError('Invalid value: %s' % (str(value),))
for format in ('%H:%M:%S', '%M:%S'):
try:
t = time.strptime(value, format)
return (t.tm_hour * 60 + t.tm_min) * 60 + t.tm_sec
except ValueError, ve:
continue
return int(value)
def format_seconds_to_hour_min_sec(seconds): def format_seconds_to_hour_min_sec(seconds):
""" """
Take the number of seconds and format it into a Take the number of seconds and format it into a