Determine episode duration (bug 811)
[gpodder.git] / src / gpodder / gtkui / model.py
blob81d92095c5ec800124c08f7010ea81373408411e
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # gpodder.gtkui.model - GUI model classes for gPodder (2009-08-13)
23 # Based on code from libpodcasts.py (thp, 2005-10-29)
26 import gpodder
28 _ = gpodder.gettext
30 from gpodder import util
31 from gpodder.liblogger import log
33 from gpodder.gtkui import draw
35 import os
36 import gtk
37 import xml.sax.saxutils
39 try:
40 import gio
41 have_gio = True
42 except ImportError:
43 have_gio = False
45 class EpisodeListModel(gtk.ListStore):
46 C_URL, C_TITLE, C_FILESIZE_TEXT, C_EPISODE, C_STATUS_ICON, \
47 C_PUBLISHED_TEXT, C_DESCRIPTION, C_TOOLTIP, \
48 C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
49 C_VIEW_SHOW_UNPLAYED = range(11)
51 SEARCH_COLUMNS = (C_TITLE, C_DESCRIPTION)
53 VIEW_ALL, VIEW_UNDELETED, VIEW_DOWNLOADED, VIEW_UNPLAYED = range(4)
55 # In which steps the UI is updated for "loading" animations
56 _UI_UPDATE_STEP = .03
58 def __init__(self):
59 gtk.ListStore.__init__(self, str, str, str, object, \
60 gtk.gdk.Pixbuf, str, str, str, bool, bool, bool)
62 # Update progress (if we're currently being updated)
63 self._update_progress = 0.
64 self._last_redraw_progress = 0.
66 # Filter to allow hiding some episodes
67 self._filter = self.filter_new()
68 self._view_mode = self.VIEW_ALL
69 self._search_term = None
70 self._filter.set_visible_func(self._filter_visible_func)
72 # Are we currently showing the "all episodes" view?
73 self._all_episodes_view = False
75 # "ICON" is used to mark icon names in source files
76 ICON = lambda x: x
78 self._icon_cache = {}
79 self.ICON_AUDIO_FILE = ICON('audio-x-generic')
80 self.ICON_VIDEO_FILE = ICON('video-x-generic')
81 self.ICON_IMAGE_FILE = ICON('image-x-generic')
82 self.ICON_GENERIC_FILE = ICON('text-x-generic')
83 self.ICON_DOWNLOADING = gtk.STOCK_GO_DOWN
84 self.ICON_DELETED = gtk.STOCK_DELETE
85 self.ICON_NEW = gtk.STOCK_ABOUT
86 self.ICON_UNPLAYED = ICON('emblem-new')
87 self.ICON_LOCKED = ICON('emblem-readonly')
88 self.ICON_MISSING = ICON('emblem-unreadable')
91 def _format_filesize(self, episode):
92 if episode.length > 0:
93 return util.format_filesize(episode.length, 1)
94 else:
95 return None
98 def _filter_visible_func(self, model, iter):
99 # If searching is active, set visibility based on search text
100 if self._search_term is not None:
101 key = self._search_term.lower()
102 return any((key in (model.get_value(iter, column) or '').lower()) for column in self.SEARCH_COLUMNS)
104 if self._view_mode == self.VIEW_ALL:
105 return True
106 elif self._view_mode == self.VIEW_UNDELETED:
107 return model.get_value(iter, self.C_VIEW_SHOW_UNDELETED)
108 elif self._view_mode == self.VIEW_DOWNLOADED:
109 return model.get_value(iter, self.C_VIEW_SHOW_DOWNLOADED)
110 elif self._view_mode == self.VIEW_UNPLAYED:
111 return model.get_value(iter, self.C_VIEW_SHOW_UNPLAYED)
113 return True
115 def get_update_progress(self):
116 return self._update_progress
118 def reset_update_progress(self):
119 self._update_progress = 0.
121 def get_filtered_model(self):
122 """Returns a filtered version of this episode model
124 The filtered version should be displayed in the UI,
125 as this model can have some filters set that should
126 be reflected in the UI.
128 return self._filter
130 def set_view_mode(self, new_mode):
131 """Sets a new view mode for this model
133 After setting the view mode, the filtered model
134 might be updated to reflect the new mode."""
135 if self._view_mode != new_mode:
136 self._view_mode = new_mode
137 self._filter.refilter()
139 def get_view_mode(self):
140 """Returns the currently-set view mode"""
141 return self._view_mode
143 def set_search_term(self, new_term):
144 if self._search_term != new_term:
145 self._search_term = new_term
146 self._filter.refilter()
148 def get_search_term(self):
149 return self._search_term
151 def _format_description(self, episode, include_description=False, is_downloading=None):
152 if include_description and self._all_episodes_view:
153 return '%s\n<small>%s</small>' % (xml.sax.saxutils.escape(episode.title),
154 _('from %s') % xml.sax.saxutils.escape(episode.channel.title))
155 elif include_description:
156 return '%s\n<small>%s</small>' % (xml.sax.saxutils.escape(episode.title),
157 xml.sax.saxutils.escape(episode.one_line_description()))
158 else:
159 return xml.sax.saxutils.escape(episode.title)
161 def add_from_channel(self, channel, downloading=None, \
162 include_description=False, generate_thumbnails=False, \
163 treeview=None):
165 Add episode from the given channel to this model.
166 Downloading should be a callback.
167 include_description should be a boolean value (True if description
168 is to be added to the episode row, or False if not)
171 self._update_progress = 0.
172 self._last_redraw_progress = 0.
173 if treeview is not None:
174 util.idle_add(treeview.queue_draw)
176 self._all_episodes_view = getattr(channel, 'ALL_EPISODES_PROXY', False)
178 episodes = channel.get_all_episodes()
179 if not isinstance(episodes, list):
180 episodes = list(episodes)
181 count = len(episodes)
183 for position, episode in enumerate(episodes):
184 iter = self.append()
185 self.set(iter, \
186 self.C_URL, episode.url, \
187 self.C_TITLE, episode.title, \
188 self.C_FILESIZE_TEXT, self._format_filesize(episode), \
189 self.C_EPISODE, episode, \
190 self.C_PUBLISHED_TEXT, episode.cute_pubdate())
191 self.update_by_iter(iter, downloading, include_description, \
192 generate_thumbnails, reload_from_db=False)
194 self._update_progress = float(position+1)/count
195 if treeview is not None and \
196 (self._update_progress > self._last_redraw_progress + self._UI_UPDATE_STEP or position+1 == count):
197 def in_gtk_main_thread():
198 treeview.queue_draw()
199 while gtk.events_pending():
200 gtk.main_iteration(False)
201 util.idle_add(in_gtk_main_thread)
202 self._last_redraw_progress = self._update_progress
204 def update_all(self, downloading=None, include_description=False, \
205 generate_thumbnails=False):
206 for row in self:
207 self.update_by_iter(row.iter, downloading, include_description, \
208 generate_thumbnails)
210 def update_by_urls(self, urls, downloading=None, include_description=False, \
211 generate_thumbnails=False):
212 for row in self:
213 if row[self.C_URL] in urls:
214 self.update_by_iter(row.iter, downloading, include_description, \
215 generate_thumbnails)
217 def update_by_filter_iter(self, iter, downloading=None, \
218 include_description=False, generate_thumbnails=False):
219 # Convenience function for use by "outside" methods that use iters
220 # from the filtered episode list model (i.e. all UI things normally)
221 self.update_by_iter(self._filter.convert_iter_to_child_iter(iter), \
222 downloading, include_description, generate_thumbnails)
224 def update_by_iter(self, iter, downloading=None, include_description=False, \
225 generate_thumbnails=False, reload_from_db=True):
226 episode = self.get_value(iter, self.C_EPISODE)
227 if reload_from_db:
228 episode.reload_from_db()
230 if include_description or gpodder.ui.maemo:
231 icon_size = 32
232 else:
233 icon_size = 16
235 show_bullet = False
236 show_padlock = False
237 show_missing = False
238 status_icon = None
239 status_icon_to_build_from_file = False
240 tooltip = []
241 view_show_undeleted = True
242 view_show_downloaded = False
243 view_show_unplayed = False
244 icon_theme = gtk.icon_theme_get_default()
246 if downloading is not None and downloading(episode):
247 tooltip.append(_('Downloading'))
248 status_icon = self.ICON_DOWNLOADING
249 view_show_downloaded = True
250 view_show_unplayed = True
251 else:
252 if episode.state == gpodder.STATE_DELETED:
253 tooltip.append(_('Deleted'))
254 status_icon = self.ICON_DELETED
255 view_show_undeleted = False
256 elif episode.state == gpodder.STATE_NORMAL and \
257 not episode.is_played:
258 tooltip.append(_('New episode'))
259 status_icon = self.ICON_NEW
260 view_show_downloaded = True
261 view_show_unplayed = True
262 elif episode.state == gpodder.STATE_DOWNLOADED:
263 tooltip = []
264 view_show_downloaded = True
265 view_show_unplayed = not episode.is_played
266 show_bullet = not episode.is_played
267 show_padlock = episode.is_locked
268 show_missing = not episode.file_exists()
269 filename = episode.local_filename(create=False, check_only=True)
271 file_type = episode.file_type()
272 if file_type == 'audio':
273 tooltip.append(_('Downloaded episode'))
274 status_icon = self.ICON_AUDIO_FILE
275 elif file_type == 'video':
276 tooltip.append(_('Downloaded video episode'))
277 status_icon = self.ICON_VIDEO_FILE
278 elif file_type == 'image':
279 tooltip.append(_('Downloaded image'))
280 status_icon = self.ICON_IMAGE_FILE
282 # Optional thumbnailing for image downloads
283 if generate_thumbnails:
284 if filename is not None:
285 # set the status icon to the path itself (that
286 # should be a good identifier anyway)
287 status_icon = filename
288 status_icon_to_build_from_file = True
289 else:
290 tooltip.append(_('Downloaded file'))
291 status_icon = self.ICON_GENERIC_FILE
293 # Try to find a themed icon for this file
294 if filename is not None and have_gio:
295 file = gio.File(filename)
296 if file.query_exists():
297 file_info = file.query_info('*')
298 icon = file_info.get_icon()
299 for icon_name in icon.get_names():
300 if icon_theme.has_icon(icon_name):
301 status_icon = icon_name
302 break
304 if show_missing:
305 tooltip.append(_('missing file'))
306 else:
307 if show_bullet:
308 if file_type == 'image':
309 tooltip.append(_('never displayed'))
310 elif file_type in ('audio', 'video'):
311 tooltip.append(_('never played'))
312 else:
313 tooltip.append(_('never opened'))
314 else:
315 if file_type == 'image':
316 tooltip.append(_('displayed'))
317 elif file_type in ('audio', 'video'):
318 tooltip.append(_('played'))
319 else:
320 tooltip.append(_('opened'))
321 if show_padlock:
322 tooltip.append(_('deletion prevented'))
324 if episode.total_time > 0 and episode.current_position:
325 tooltip.append('%d%%' % (100.*float(episode.current_position)/float(episode.total_time),))
327 if episode.total_time:
328 total_time = util.format_time(episode.total_time)
329 if total_time:
330 tooltip.append(total_time)
332 tooltip = ', '.join(tooltip)
334 if status_icon is not None:
335 status_icon = self._get_tree_icon(status_icon, show_bullet, \
336 show_padlock, show_missing, icon_size, status_icon_to_build_from_file)
338 description = self._format_description(episode, include_description, downloading)
339 self.set(iter, \
340 self.C_STATUS_ICON, status_icon, \
341 self.C_VIEW_SHOW_UNDELETED, view_show_undeleted, \
342 self.C_VIEW_SHOW_DOWNLOADED, view_show_downloaded, \
343 self.C_VIEW_SHOW_UNPLAYED, view_show_unplayed, \
344 self.C_DESCRIPTION, description, \
345 self.C_TOOLTIP, tooltip)
347 def _get_icon_from_image(self,image_path, icon_size):
349 Load an local image file and transform it into an icon.
351 Return a pixbuf scaled to the desired size and may return None
352 if the icon creation is impossible (file not found etc).
354 if not os.path.exists(image_path):
355 return None
356 # load image from disc (code adapted from CoverDownloader
357 # except that no download is needed here)
358 loader = gtk.gdk.PixbufLoader()
359 pixbuf = None
360 try:
361 loader.write(open(image_path, 'rb').read())
362 loader.close()
363 pixbuf = loader.get_pixbuf()
364 except:
365 log('Data error while loading image %s', image_path, sender=self)
366 return None
367 # Now scale the image with ratio (copied from _resize_pixbuf_keep_ratio)
368 # Resize if too wide
369 if pixbuf.get_width() > icon_size:
370 f = float(icon_size)/pixbuf.get_width()
371 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
372 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
373 # Resize if too high
374 if pixbuf.get_height() > icon_size:
375 f = float(icon_size)/pixbuf.get_height()
376 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
377 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
378 return pixbuf
381 def _get_tree_icon(self, icon_name, add_bullet=False, \
382 add_padlock=False, add_missing=False, icon_size=32, \
383 build_icon_from_file = False):
385 Loads an icon from the current icon theme at the specified
386 size, suitable for display in a gtk.TreeView. Additional
387 emblems can be added on top of the icon.
389 Caching is used to speed up the icon lookup.
391 The `build_icon_from_file` argument indicates (when True) that
392 the icon has to be created on the fly from a given image
393 file. The `icon_name` argument is then interpreted as the path
394 to this file. Those specific icons will *not be cached*.
397 # Add all variables that modify the appearance of the icon, so
398 # our cache does not return the same icons for different requests
399 cache_id = (icon_name, add_bullet, add_padlock, add_missing, icon_size)
401 if cache_id in self._icon_cache:
402 return self._icon_cache[cache_id]
404 icon_theme = gtk.icon_theme_get_default()
406 try:
407 if build_icon_from_file:
408 icon = self._get_icon_from_image(icon_name,icon_size)
409 else:
410 icon = icon_theme.load_icon(icon_name, icon_size, 0)
411 except:
412 icon = icon_theme.load_icon(gtk.STOCK_DIALOG_QUESTION, icon_size, 0)
414 if icon and (add_bullet or add_padlock or add_missing):
415 # We'll modify the icon, so use .copy()
416 if add_missing:
417 try:
418 icon = icon.copy()
419 # Desaturate the icon so it looks even more "missing"
420 icon.saturate_and_pixelate(icon, 0.0, False)
421 emblem = icon_theme.load_icon(self.ICON_MISSING, icon_size/2, 0)
422 (width, height) = (emblem.get_width(), emblem.get_height())
423 xpos = icon.get_width() - width
424 ypos = icon.get_height() - height
425 emblem.composite(icon, xpos, ypos, width, height, xpos, ypos, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
426 except:
427 pass
428 elif add_bullet:
429 try:
430 icon = icon.copy()
431 emblem = icon_theme.load_icon(self.ICON_UNPLAYED, icon_size/2, 0)
432 (width, height) = (emblem.get_width(), emblem.get_height())
433 xpos = icon.get_width() - width
434 ypos = icon.get_height() - height
435 emblem.composite(icon, xpos, ypos, width, height, xpos, ypos, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
436 except:
437 pass
438 if add_padlock:
439 try:
440 icon = icon.copy()
441 emblem = icon_theme.load_icon(self.ICON_LOCKED, icon_size/2, 0)
442 (width, height) = (emblem.get_width(), emblem.get_height())
443 emblem.composite(icon, 0, 0, width, height, 0, 0, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
444 except:
445 pass
447 self._icon_cache[cache_id] = icon
448 return icon
451 class PodcastChannelProxy(object):
452 ALL_EPISODES_PROXY = True
454 def __init__(self, db, config, channels):
455 self._db = db
456 self._config = config
457 self.channels = channels
458 self.title = _('All episodes')
459 self.description = _('from all podcasts')
460 self.parse_error = ''
461 self.url = ''
462 self.id = None
463 self._save_dir_size_set = False
464 self.save_dir_size = 0L
465 self.cover_file = os.path.join(gpodder.images_folder, 'podcast-all.png')
467 def __getattribute__(self, name):
468 try:
469 return object.__getattribute__(self, name)
470 except AttributeError:
471 log('Unsupported method call (%s)', name, sender=self)
473 def get_statistics(self):
474 # Get the total statistics for all channels from the database
475 return self._db.get_total_count()
477 def get_all_episodes(self):
478 """Returns a generator that yields every episode"""
479 channel_lookup_map = dict((c.id, c) for c in self.channels)
480 return self._db.load_all_episodes(channel_lookup_map)
482 def request_save_dir_size(self):
483 if not self._save_dir_size_set:
484 self.update_save_dir_size()
485 self._save_dir_size_set = True
487 def update_save_dir_size(self):
488 self.save_dir_size = util.calculate_size(self._config.download_dir)
491 class PodcastListModel(gtk.ListStore):
492 C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
493 C_COVER, C_ERROR, C_PILL_VISIBLE, \
494 C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
495 C_VIEW_SHOW_UNPLAYED, C_HAS_EPISODES, C_SEPARATOR = range(13)
497 SEARCH_COLUMNS = (C_TITLE, C_DESCRIPTION)
499 @classmethod
500 def row_separator_func(cls, model, iter):
501 return model.get_value(iter, cls.C_SEPARATOR)
503 def __init__(self, cover_downloader):
504 gtk.ListStore.__init__(self, str, str, str, gtk.gdk.Pixbuf, \
505 object, gtk.gdk.Pixbuf, str, bool, bool, bool, bool, bool, bool)
507 # Filter to allow hiding some episodes
508 self._filter = self.filter_new()
509 self._view_mode = -1
510 self._search_term = None
511 self._filter.set_visible_func(self._filter_visible_func)
513 self._cover_cache = {}
514 if gpodder.ui.fremantle:
515 self._max_image_side = 64
516 else:
517 self._max_image_side = 40
518 self._cover_downloader = cover_downloader
520 def _filter_visible_func(self, model, iter):
521 # If searching is active, set visibility based on search text
522 if self._search_term is not None:
523 key = self._search_term.lower()
524 columns = (model.get_value(iter, c) for c in self.SEARCH_COLUMNS)
525 return any((key in c.lower() for c in columns if c is not None))
527 if model.get_value(iter, self.C_SEPARATOR):
528 return True
529 if self._view_mode == EpisodeListModel.VIEW_ALL:
530 return model.get_value(iter, self.C_HAS_EPISODES)
531 elif self._view_mode == EpisodeListModel.VIEW_UNDELETED:
532 return model.get_value(iter, self.C_VIEW_SHOW_UNDELETED)
533 elif self._view_mode == EpisodeListModel.VIEW_DOWNLOADED:
534 return model.get_value(iter, self.C_VIEW_SHOW_DOWNLOADED)
535 elif self._view_mode == EpisodeListModel.VIEW_UNPLAYED:
536 return model.get_value(iter, self.C_VIEW_SHOW_UNPLAYED)
538 return True
540 def get_filtered_model(self):
541 """Returns a filtered version of this episode model
543 The filtered version should be displayed in the UI,
544 as this model can have some filters set that should
545 be reflected in the UI.
547 return self._filter
549 def set_view_mode(self, new_mode):
550 """Sets a new view mode for this model
552 After setting the view mode, the filtered model
553 might be updated to reflect the new mode."""
554 if self._view_mode != new_mode:
555 self._view_mode = new_mode
556 self._filter.refilter()
558 def get_view_mode(self):
559 """Returns the currently-set view mode"""
560 return self._view_mode
562 def set_search_term(self, new_term):
563 if self._search_term != new_term:
564 self._search_term = new_term
565 self._filter.refilter()
567 def get_search_term(self):
568 return self._search_term
570 def enable_separators(self, channeltree):
571 channeltree.set_row_separator_func(self._show_row_separator)
573 def _show_row_separator(self, model, iter):
574 return model.get_value(iter, self.C_SEPARATOR)
576 def _resize_pixbuf_keep_ratio(self, url, pixbuf):
578 Resizes a GTK Pixbuf but keeps its aspect ratio.
579 Returns None if the pixbuf does not need to be
580 resized or the newly resized pixbuf if it does.
582 changed = False
583 result = None
585 if url in self._cover_cache:
586 return self._cover_cache[url]
588 # Resize if too wide
589 if pixbuf.get_width() > self._max_image_side:
590 f = float(self._max_image_side)/pixbuf.get_width()
591 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
592 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
593 changed = True
595 # Resize if too high
596 if pixbuf.get_height() > self._max_image_side:
597 f = float(self._max_image_side)/pixbuf.get_height()
598 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
599 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
600 changed = True
602 if changed:
603 self._cover_cache[url] = pixbuf
604 result = pixbuf
606 return result
608 def _resize_pixbuf(self, url, pixbuf):
609 if pixbuf is None:
610 return None
612 return self._resize_pixbuf_keep_ratio(url, pixbuf) or pixbuf
614 def _get_cover_image(self, channel):
615 if self._cover_downloader is None:
616 return None
618 pixbuf = self._cover_downloader.get_cover(channel, avoid_downloading=True)
619 return self._resize_pixbuf(channel.url, pixbuf)
621 def _get_pill_image(self, channel, count_downloaded, count_unplayed):
622 if count_unplayed > 0 or count_downloaded > 0:
623 return draw.draw_pill_pixbuf(str(count_unplayed), str(count_downloaded))
624 else:
625 return None
627 def _format_description(self, channel, total, deleted, \
628 new, downloaded, unplayed):
629 title_markup = xml.sax.saxutils.escape(channel.title)
630 description_markup = xml.sax.saxutils.escape(util.get_first_line(channel.description) or ' ')
631 d = []
632 if new:
633 d.append('<span weight="bold">')
634 d.append(title_markup)
635 if new:
636 d.append('</span>')
637 return ''.join(d+['\n', '<small>', description_markup, '</small>'])
639 def _format_error(self, channel):
640 if channel.parse_error:
641 return str(channel.parse_error)
642 else:
643 return None
645 def set_channels(self, db, config, channels):
646 # Clear the model and update the list of podcasts
647 self.clear()
649 if config.podcast_list_view_all and channels:
650 all_episodes = PodcastChannelProxy(db, config, channels)
651 iter = self.append()
652 self.set(iter, \
653 self.C_URL, all_episodes.url, \
654 self.C_CHANNEL, all_episodes, \
655 self.C_COVER, self._get_cover_image(all_episodes), \
656 self.C_SEPARATOR, False)
657 self.update_by_iter(iter)
659 iter = self.append()
660 self.set(iter, self.C_SEPARATOR, True)
662 for channel in channels:
663 iter = self.append()
664 self.set(iter, \
665 self.C_URL, channel.url, \
666 self.C_CHANNEL, channel, \
667 self.C_COVER, self._get_cover_image(channel), \
668 self.C_SEPARATOR, False)
669 self.update_by_iter(iter)
671 def get_filter_path_from_url(self, url):
672 # Return the path of the filtered model for a given URL
673 child_path = self.get_path_from_url(url)
674 if child_path is None:
675 return None
676 else:
677 return self._filter.convert_child_path_to_path(child_path)
679 def get_path_from_url(self, url):
680 # Return the tree model path for a given URL
681 if url is None:
682 return None
684 for row in self:
685 if row[self.C_URL] == url:
686 return row.path
687 return None
689 def update_first_row(self):
690 # Update the first row in the model (for "all episodes" updates)
691 self.update_by_iter(self.get_iter_first())
693 def update_by_urls(self, urls):
694 # Given a list of URLs, update each matching row
695 for row in self:
696 if row[self.C_URL] in urls:
697 self.update_by_iter(row.iter)
699 def iter_is_first_row(self, iter):
700 iter = self._filter.convert_iter_to_child_iter(iter)
701 path = self.get_path(iter)
702 return (path == (0,))
704 def update_by_filter_iter(self, iter):
705 self.update_by_iter(self._filter.convert_iter_to_child_iter(iter))
707 def update_all(self):
708 for row in self:
709 self.update_by_iter(row.iter)
711 def update_by_iter(self, iter):
712 # Given a GtkTreeIter, update volatile information
713 try:
714 channel = self.get_value(iter, self.C_CHANNEL)
715 except TypeError, te:
716 return
717 if channel is None:
718 return
719 total, deleted, new, downloaded, unplayed = channel.get_statistics()
720 description = self._format_description(channel, total, deleted, new, \
721 downloaded, unplayed)
723 pill_image = self._get_pill_image(channel, downloaded, unplayed)
724 self.set(iter, \
725 self.C_TITLE, channel.title, \
726 self.C_DESCRIPTION, description, \
727 self.C_ERROR, self._format_error(channel), \
728 self.C_PILL, pill_image, \
729 self.C_PILL_VISIBLE, pill_image != None, \
730 self.C_VIEW_SHOW_UNDELETED, total - deleted > 0, \
731 self.C_VIEW_SHOW_DOWNLOADED, downloaded + new > 0, \
732 self.C_VIEW_SHOW_UNPLAYED, unplayed + new > 0, \
733 self.C_HAS_EPISODES, total > 0)
735 def add_cover_by_url(self, url, pixbuf):
736 # Resize and add the new cover image
737 pixbuf = self._resize_pixbuf(url, pixbuf)
738 for row in self:
739 if row[self.C_URL] == url:
740 row[self.C_COVER] = pixbuf
741 break
743 def delete_cover_by_url(self, url):
744 # Remove the cover from the model
745 for row in self:
746 if row[self.C_URL] == url:
747 row[self.C_COVER] = None
748 break
750 # Remove the cover from the cache
751 if url in self._cover_cache:
752 del self._cover_cache[url]