All episodes: Sort by date, show podcast (bug 921)
[gpodder.git] / src / gpodder / gtkui / model.py
blob71ac9d1caa7658b1336f1c1bd1dd06ac8b7e5917
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 import model
32 from gpodder.liblogger import log
34 from gpodder.gtkui import draw
36 import os
37 import gtk
38 import xml.sax.saxutils
40 try:
41 import gio
42 have_gio = True
43 except ImportError:
44 have_gio = False
46 class EpisodeListModel(gtk.ListStore):
47 C_URL, C_TITLE, C_FILESIZE_TEXT, C_EPISODE, C_STATUS_ICON, \
48 C_PUBLISHED_TEXT, C_DESCRIPTION, C_TOOLTIP, \
49 C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
50 C_VIEW_SHOW_UNPLAYED = range(11)
52 SEARCH_COLUMNS = (C_TITLE, C_DESCRIPTION)
54 VIEW_ALL, VIEW_UNDELETED, VIEW_DOWNLOADED, VIEW_UNPLAYED = range(4)
56 # In which steps the UI is updated for "loading" animations
57 _UI_UPDATE_STEP = .03
59 def __init__(self):
60 gtk.ListStore.__init__(self, str, str, str, object, \
61 gtk.gdk.Pixbuf, str, str, str, bool, bool, bool)
63 # Update progress (if we're currently being updated)
64 self._update_progress = 0.
65 self._last_redraw_progress = 0.
67 # Filter to allow hiding some episodes
68 self._filter = self.filter_new()
69 self._view_mode = self.VIEW_ALL
70 self._search_term = None
71 self._filter.set_visible_func(self._filter_visible_func)
73 # Are we currently showing the "all episodes" view?
74 self._all_episodes_view = False
76 # "ICON" is used to mark icon names in source files
77 ICON = lambda x: x
79 self._icon_cache = {}
80 self.ICON_AUDIO_FILE = ICON('audio-x-generic')
81 self.ICON_VIDEO_FILE = ICON('video-x-generic')
82 self.ICON_IMAGE_FILE = ICON('image-x-generic')
83 self.ICON_GENERIC_FILE = ICON('text-x-generic')
84 self.ICON_DOWNLOADING = gtk.STOCK_GO_DOWN
85 self.ICON_DELETED = gtk.STOCK_DELETE
86 self.ICON_NEW = gtk.STOCK_ABOUT
87 self.ICON_UNPLAYED = ICON('emblem-new')
88 self.ICON_LOCKED = ICON('emblem-readonly')
89 self.ICON_MISSING = ICON('emblem-unreadable')
92 def _format_filesize(self, episode):
93 if episode.length > 0:
94 return util.format_filesize(episode.length, 1)
95 else:
96 return None
99 def _filter_visible_func(self, model, iter):
100 # If searching is active, set visibility based on search text
101 if self._search_term is not None:
102 key = self._search_term.lower()
103 return any((key in (model.get_value(iter, column) or '').lower()) for column in self.SEARCH_COLUMNS)
105 if self._view_mode == self.VIEW_ALL:
106 return True
107 elif self._view_mode == self.VIEW_UNDELETED:
108 return model.get_value(iter, self.C_VIEW_SHOW_UNDELETED)
109 elif self._view_mode == self.VIEW_DOWNLOADED:
110 return model.get_value(iter, self.C_VIEW_SHOW_DOWNLOADED)
111 elif self._view_mode == self.VIEW_UNPLAYED:
112 return model.get_value(iter, self.C_VIEW_SHOW_UNPLAYED)
114 return True
116 def get_update_progress(self):
117 return self._update_progress
119 def reset_update_progress(self):
120 self._update_progress = 0.
122 def get_filtered_model(self):
123 """Returns a filtered version of this episode model
125 The filtered version should be displayed in the UI,
126 as this model can have some filters set that should
127 be reflected in the UI.
129 return self._filter
131 def set_view_mode(self, new_mode):
132 """Sets a new view mode for this model
134 After setting the view mode, the filtered model
135 might be updated to reflect the new mode."""
136 if self._view_mode != new_mode:
137 self._view_mode = new_mode
138 self._filter.refilter()
140 def get_view_mode(self):
141 """Returns the currently-set view mode"""
142 return self._view_mode
144 def set_search_term(self, new_term):
145 if self._search_term != new_term:
146 self._search_term = new_term
147 self._filter.refilter()
149 def get_search_term(self):
150 return self._search_term
152 def _format_description(self, episode, include_description=False, is_downloading=None):
153 if include_description and self._all_episodes_view:
154 return '%s\n<small>%s</small>' % (xml.sax.saxutils.escape(episode.title),
155 _('from %s') % xml.sax.saxutils.escape(episode.channel.title))
156 elif include_description:
157 return '%s\n<small>%s</small>' % (xml.sax.saxutils.escape(episode.title),
158 xml.sax.saxutils.escape(episode.one_line_description()))
159 else:
160 return xml.sax.saxutils.escape(episode.title)
162 def add_from_channel(self, channel, downloading=None, \
163 include_description=False, generate_thumbnails=False, \
164 treeview=None):
166 Add episode from the given channel to this model.
167 Downloading should be a callback.
168 include_description should be a boolean value (True if description
169 is to be added to the episode row, or False if not)
172 self._update_progress = 0.
173 self._last_redraw_progress = 0.
174 if treeview is not None:
175 util.idle_add(treeview.queue_draw)
177 self._all_episodes_view = getattr(channel, 'ALL_EPISODES_PROXY', False)
179 episodes = list(channel.get_all_episodes())
180 count = len(episodes)
182 for position, episode in enumerate(episodes):
183 iter = self.append()
184 self.set(iter, \
185 self.C_URL, episode.url, \
186 self.C_TITLE, episode.title, \
187 self.C_FILESIZE_TEXT, self._format_filesize(episode), \
188 self.C_EPISODE, episode, \
189 self.C_PUBLISHED_TEXT, episode.cute_pubdate())
190 self.update_by_iter(iter, downloading, include_description, \
191 generate_thumbnails, reload_from_db=False)
193 self._update_progress = float(position+1)/count
194 if treeview is not None and \
195 (self._update_progress > self._last_redraw_progress + self._UI_UPDATE_STEP or position+1 == count):
196 def in_gtk_main_thread():
197 treeview.queue_draw()
198 while gtk.events_pending():
199 gtk.main_iteration(False)
200 util.idle_add(in_gtk_main_thread)
201 self._last_redraw_progress = self._update_progress
203 def update_all(self, downloading=None, include_description=False, \
204 generate_thumbnails=False):
205 for row in self:
206 self.update_by_iter(row.iter, downloading, include_description, \
207 generate_thumbnails)
209 def update_by_urls(self, urls, downloading=None, include_description=False, \
210 generate_thumbnails=False):
211 for row in self:
212 if row[self.C_URL] in urls:
213 self.update_by_iter(row.iter, downloading, include_description, \
214 generate_thumbnails)
216 def update_by_filter_iter(self, iter, downloading=None, \
217 include_description=False, generate_thumbnails=False):
218 # Convenience function for use by "outside" methods that use iters
219 # from the filtered episode list model (i.e. all UI things normally)
220 self.update_by_iter(self._filter.convert_iter_to_child_iter(iter), \
221 downloading, include_description, generate_thumbnails)
223 def update_by_iter(self, iter, downloading=None, include_description=False, \
224 generate_thumbnails=False, reload_from_db=True):
225 episode = self.get_value(iter, self.C_EPISODE)
226 if reload_from_db:
227 episode.reload_from_db()
229 if include_description or gpodder.ui.maemo:
230 icon_size = 32
231 else:
232 icon_size = 16
234 show_bullet = False
235 show_padlock = False
236 show_missing = False
237 status_icon = None
238 status_icon_to_build_from_file = False
239 tooltip = ''
240 view_show_undeleted = True
241 view_show_downloaded = False
242 view_show_unplayed = False
243 icon_theme = gtk.icon_theme_get_default()
245 if downloading is not None and downloading(episode):
246 tooltip = _('Downloading')
247 status_icon = self.ICON_DOWNLOADING
248 view_show_downloaded = True
249 view_show_unplayed = True
250 else:
251 if episode.state == gpodder.STATE_DELETED:
252 tooltip = _('Deleted')
253 status_icon = self.ICON_DELETED
254 view_show_undeleted = False
255 elif episode.state == gpodder.STATE_NORMAL and \
256 not episode.is_played:
257 tooltip = _('New episode')
258 status_icon = self.ICON_NEW
259 view_show_downloaded = True
260 view_show_unplayed = True
261 elif episode.state == gpodder.STATE_DOWNLOADED:
262 tooltip = []
263 view_show_downloaded = True
264 view_show_unplayed = not episode.is_played
265 show_bullet = not episode.is_played
266 show_padlock = episode.is_locked
267 show_missing = not episode.file_exists()
268 filename = episode.local_filename(create=False, check_only=True)
270 file_type = episode.file_type()
271 if file_type == 'audio':
272 tooltip.append(_('Downloaded episode'))
273 status_icon = self.ICON_AUDIO_FILE
274 elif file_type == 'video':
275 tooltip.append(_('Downloaded video episode'))
276 status_icon = self.ICON_VIDEO_FILE
277 elif file_type == 'image':
278 tooltip.append(_('Downloaded image'))
279 status_icon = self.ICON_IMAGE_FILE
281 # Optional thumbnailing for image downloads
282 if generate_thumbnails:
283 if filename is not None:
284 # set the status icon to the path itself (that
285 # should be a good identifier anyway)
286 status_icon = filename
287 status_icon_to_build_from_file = True
288 else:
289 tooltip.append(_('Downloaded file'))
290 status_icon = self.ICON_GENERIC_FILE
292 # Try to find a themed icon for this file
293 if filename is not None and have_gio:
294 file = gio.File(filename)
295 if file.query_exists():
296 file_info = file.query_info('*')
297 icon = file_info.get_icon()
298 for icon_name in icon.get_names():
299 if icon_theme.has_icon(icon_name):
300 status_icon = icon_name
301 break
303 if show_missing:
304 tooltip.append(_('missing file'))
305 else:
306 if show_bullet:
307 if file_type == 'image':
308 tooltip.append(_('never displayed'))
309 elif file_type in ('audio', 'video'):
310 tooltip.append(_('never played'))
311 else:
312 tooltip.append(_('never opened'))
313 else:
314 if file_type == 'image':
315 tooltip.append(_('displayed'))
316 elif file_type in ('audio', 'video'):
317 tooltip.append(_('played'))
318 else:
319 tooltip.append(_('opened'))
320 if show_padlock:
321 tooltip.append(_('deletion prevented'))
323 tooltip = ', '.join(tooltip)
325 if status_icon is not None:
326 status_icon = self._get_tree_icon(status_icon, show_bullet, \
327 show_padlock, show_missing, icon_size, status_icon_to_build_from_file)
329 description = self._format_description(episode, include_description, downloading)
330 self.set(iter, \
331 self.C_STATUS_ICON, status_icon, \
332 self.C_VIEW_SHOW_UNDELETED, view_show_undeleted, \
333 self.C_VIEW_SHOW_DOWNLOADED, view_show_downloaded, \
334 self.C_VIEW_SHOW_UNPLAYED, view_show_unplayed, \
335 self.C_DESCRIPTION, description, \
336 self.C_TOOLTIP, tooltip)
338 def _get_icon_from_image(self,image_path, icon_size):
340 Load an local image file and transform it into an icon.
342 Return a pixbuf scaled to the desired size and may return None
343 if the icon creation is impossible (file not found etc).
345 if not os.path.exists(image_path):
346 return None
347 # load image from disc (code adapted from CoverDownloader
348 # except that no download is needed here)
349 loader = gtk.gdk.PixbufLoader()
350 pixbuf = None
351 try:
352 loader.write(open(image_path, 'rb').read())
353 loader.close()
354 pixbuf = loader.get_pixbuf()
355 except:
356 log('Data error while loading image %s', image_path, sender=self)
357 return None
358 # Now scale the image with ratio (copied from _resize_pixbuf_keep_ratio)
359 # Resize if too wide
360 if pixbuf.get_width() > icon_size:
361 f = float(icon_size)/pixbuf.get_width()
362 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
363 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
364 # Resize if too high
365 if pixbuf.get_height() > icon_size:
366 f = float(icon_size)/pixbuf.get_height()
367 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
368 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
369 return pixbuf
372 def _get_tree_icon(self, icon_name, add_bullet=False, \
373 add_padlock=False, add_missing=False, icon_size=32, \
374 build_icon_from_file = False):
376 Loads an icon from the current icon theme at the specified
377 size, suitable for display in a gtk.TreeView. Additional
378 emblems can be added on top of the icon.
380 Caching is used to speed up the icon lookup.
382 The `build_icon_from_file` argument indicates (when True) that
383 the icon has to be created on the fly from a given image
384 file. The `icon_name` argument is then interpreted as the path
385 to this file. Those specific icons will *not be cached*.
388 # Add all variables that modify the appearance of the icon, so
389 # our cache does not return the same icons for different requests
390 cache_id = (icon_name, add_bullet, add_padlock, add_missing, icon_size)
392 if cache_id in self._icon_cache:
393 return self._icon_cache[cache_id]
395 icon_theme = gtk.icon_theme_get_default()
397 try:
398 if build_icon_from_file:
399 icon = self._get_icon_from_image(icon_name,icon_size)
400 else:
401 icon = icon_theme.load_icon(icon_name, icon_size, 0)
402 except:
403 icon = icon_theme.load_icon(gtk.STOCK_DIALOG_QUESTION, icon_size, 0)
405 if icon and (add_bullet or add_padlock or add_missing):
406 # We'll modify the icon, so use .copy()
407 if add_missing:
408 try:
409 icon = icon.copy()
410 # Desaturate the icon so it looks even more "missing"
411 icon.saturate_and_pixelate(icon, 0.0, False)
412 emblem = icon_theme.load_icon(self.ICON_MISSING, icon_size/2, 0)
413 (width, height) = (emblem.get_width(), emblem.get_height())
414 xpos = icon.get_width() - width
415 ypos = icon.get_height() - height
416 emblem.composite(icon, xpos, ypos, width, height, xpos, ypos, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
417 except:
418 pass
419 elif add_bullet:
420 try:
421 icon = icon.copy()
422 emblem = icon_theme.load_icon(self.ICON_UNPLAYED, icon_size/2, 0)
423 (width, height) = (emblem.get_width(), emblem.get_height())
424 xpos = icon.get_width() - width
425 ypos = icon.get_height() - height
426 emblem.composite(icon, xpos, ypos, width, height, xpos, ypos, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
427 except:
428 pass
429 if add_padlock:
430 try:
431 icon = icon.copy()
432 emblem = icon_theme.load_icon(self.ICON_LOCKED, icon_size/2, 0)
433 (width, height) = (emblem.get_width(), emblem.get_height())
434 emblem.composite(icon, 0, 0, width, height, 0, 0, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
435 except:
436 pass
438 self._icon_cache[cache_id] = icon
439 return icon
442 class PodcastChannelProxy(object):
443 ALL_EPISODES_PROXY = True
445 def __init__(self, db, config, channels):
446 self._db = db
447 self._config = config
448 self.channels = channels
449 self.title = _('All episodes')
450 self.description = _('from all podcasts')
451 self.parse_error = ''
452 self.url = ''
453 self.id = None
454 self._save_dir_size_set = False
455 self.save_dir_size = 0L
456 self.cover_file = os.path.join(gpodder.images_folder, 'podcast-all.png')
458 def __getattribute__(self, name):
459 try:
460 return object.__getattribute__(self, name)
461 except AttributeError:
462 log('Unsupported method call (%s)', name, sender=self)
464 def get_statistics(self):
465 # Get the total statistics for all channels from the database
466 return self._db.get_total_count()
468 def get_all_episodes(self):
469 """Returns a generator that yields every episode"""
470 def all_episodes():
471 for channel in self.channels:
472 for episode in channel.get_all_episodes():
473 episode._all_episodes_view = True
474 yield episode
475 return model.PodcastEpisode.sort_by_pubdate(all_episodes(), reverse=True)
477 def request_save_dir_size(self):
478 if not self._save_dir_size_set:
479 self.update_save_dir_size()
480 self._save_dir_size_set = True
482 def update_save_dir_size(self):
483 self.save_dir_size = util.calculate_size(self._config.download_dir)
486 class PodcastListModel(gtk.ListStore):
487 C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
488 C_COVER, C_ERROR, C_PILL_VISIBLE, \
489 C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
490 C_VIEW_SHOW_UNPLAYED, C_HAS_EPISODES, C_SEPARATOR = range(13)
492 SEARCH_COLUMNS = (C_TITLE, C_DESCRIPTION)
494 @classmethod
495 def row_separator_func(cls, model, iter):
496 return model.get_value(iter, cls.C_SEPARATOR)
498 def __init__(self, cover_downloader):
499 gtk.ListStore.__init__(self, str, str, str, gtk.gdk.Pixbuf, \
500 object, gtk.gdk.Pixbuf, str, bool, bool, bool, bool, bool, bool)
502 # Filter to allow hiding some episodes
503 self._filter = self.filter_new()
504 self._view_mode = -1
505 self._search_term = None
506 self._filter.set_visible_func(self._filter_visible_func)
508 self._cover_cache = {}
509 if gpodder.ui.fremantle:
510 self._max_image_side = 64
511 else:
512 self._max_image_side = 40
513 self._cover_downloader = cover_downloader
515 def _filter_visible_func(self, model, iter):
516 # If searching is active, set visibility based on search text
517 if self._search_term is not None:
518 key = self._search_term.lower()
519 return any((key in model.get_value(iter, column).lower()) for column in self.SEARCH_COLUMNS)
521 if model.get_value(iter, self.C_SEPARATOR):
522 return True
523 if self._view_mode == EpisodeListModel.VIEW_ALL:
524 return model.get_value(iter, self.C_HAS_EPISODES)
525 elif self._view_mode == EpisodeListModel.VIEW_UNDELETED:
526 return model.get_value(iter, self.C_VIEW_SHOW_UNDELETED)
527 elif self._view_mode == EpisodeListModel.VIEW_DOWNLOADED:
528 return model.get_value(iter, self.C_VIEW_SHOW_DOWNLOADED)
529 elif self._view_mode == EpisodeListModel.VIEW_UNPLAYED:
530 return model.get_value(iter, self.C_VIEW_SHOW_UNPLAYED)
532 return True
534 def get_filtered_model(self):
535 """Returns a filtered version of this episode model
537 The filtered version should be displayed in the UI,
538 as this model can have some filters set that should
539 be reflected in the UI.
541 return self._filter
543 def set_view_mode(self, new_mode):
544 """Sets a new view mode for this model
546 After setting the view mode, the filtered model
547 might be updated to reflect the new mode."""
548 if self._view_mode != new_mode:
549 self._view_mode = new_mode
550 self._filter.refilter()
552 def get_view_mode(self):
553 """Returns the currently-set view mode"""
554 return self._view_mode
556 def set_search_term(self, new_term):
557 if self._search_term != new_term:
558 self._search_term = new_term
559 self._filter.refilter()
561 def get_search_term(self):
562 return self._search_term
564 def enable_separators(self, channeltree):
565 channeltree.set_row_separator_func(self._show_row_separator)
567 def _show_row_separator(self, model, iter):
568 return model.get_value(iter, self.C_SEPARATOR)
570 def _resize_pixbuf_keep_ratio(self, url, pixbuf):
572 Resizes a GTK Pixbuf but keeps its aspect ratio.
573 Returns None if the pixbuf does not need to be
574 resized or the newly resized pixbuf if it does.
576 changed = False
577 result = None
579 if url in self._cover_cache:
580 return self._cover_cache[url]
582 # Resize if too wide
583 if pixbuf.get_width() > self._max_image_side:
584 f = float(self._max_image_side)/pixbuf.get_width()
585 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
586 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
587 changed = True
589 # Resize if too high
590 if pixbuf.get_height() > self._max_image_side:
591 f = float(self._max_image_side)/pixbuf.get_height()
592 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
593 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
594 changed = True
596 if changed:
597 self._cover_cache[url] = pixbuf
598 result = pixbuf
600 return result
602 def _resize_pixbuf(self, url, pixbuf):
603 if pixbuf is None:
604 return None
606 return self._resize_pixbuf_keep_ratio(url, pixbuf) or pixbuf
608 def _get_cover_image(self, channel):
609 if self._cover_downloader is None:
610 return None
612 pixbuf = self._cover_downloader.get_cover(channel, avoid_downloading=True)
613 return self._resize_pixbuf(channel.url, pixbuf)
615 def _get_pill_image(self, channel, count_downloaded, count_unplayed):
616 if count_unplayed > 0 or count_downloaded > 0:
617 return draw.draw_pill_pixbuf(str(count_unplayed), str(count_downloaded))
618 else:
619 return None
621 def _format_description(self, channel, total, deleted, \
622 new, downloaded, unplayed):
623 title_markup = xml.sax.saxutils.escape(channel.title)
624 description_markup = xml.sax.saxutils.escape(util.get_first_line(channel.description) or ' ')
625 d = []
626 if new:
627 d.append('<span weight="bold">')
628 d.append(title_markup)
629 if new:
630 d.append('</span>')
631 return ''.join(d+['\n', '<small>', description_markup, '</small>'])
633 def _format_error(self, channel):
634 if channel.parse_error:
635 return str(channel.parse_error)
636 else:
637 return None
639 def set_channels(self, db, config, channels):
640 # Clear the model and update the list of podcasts
641 self.clear()
643 if config.podcast_list_view_all:
644 all_episodes = PodcastChannelProxy(db, config, channels)
645 iter = self.append()
646 self.set(iter, \
647 self.C_URL, all_episodes.url, \
648 self.C_CHANNEL, all_episodes, \
649 self.C_COVER, self._get_cover_image(all_episodes), \
650 self.C_SEPARATOR, False)
651 self.update_by_iter(iter)
653 iter = self.append()
654 self.set(iter, self.C_SEPARATOR, True)
656 for channel in channels:
657 iter = self.append()
658 self.set(iter, \
659 self.C_URL, channel.url, \
660 self.C_CHANNEL, channel, \
661 self.C_COVER, self._get_cover_image(channel), \
662 self.C_SEPARATOR, False)
663 self.update_by_iter(iter)
665 def get_filter_path_from_url(self, url):
666 # Return the path of the filtered model for a given URL
667 child_path = self.get_path_from_url(url)
668 if child_path is None:
669 return None
670 else:
671 return self._filter.convert_child_path_to_path(child_path)
673 def get_path_from_url(self, url):
674 # Return the tree model path for a given URL
675 if url is None:
676 return None
678 for row in self:
679 if row[self.C_URL] == url:
680 return row.path
681 return None
683 def update_first_row(self):
684 # Update the first row in the model (for "all episodes" updates)
685 self.update_by_iter(self.get_iter_first())
687 def update_by_urls(self, urls):
688 # Given a list of URLs, update each matching row
689 for row in self:
690 if row[self.C_URL] in urls:
691 self.update_by_iter(row.iter)
693 def iter_is_first_row(self, iter):
694 iter = self._filter.convert_iter_to_child_iter(iter)
695 path = self.get_path(iter)
696 return (path == (0,))
698 def update_by_filter_iter(self, iter):
699 self.update_by_iter(self._filter.convert_iter_to_child_iter(iter))
701 def update_all(self):
702 for row in self:
703 self.update_by_iter(row.iter)
705 def update_by_iter(self, iter):
706 # Given a GtkTreeIter, update volatile information
707 channel = self.get_value(iter, self.C_CHANNEL)
708 if channel is None:
709 return
710 total, deleted, new, downloaded, unplayed = channel.get_statistics()
711 description = self._format_description(channel, total, deleted, new, \
712 downloaded, unplayed)
714 pill_image = self._get_pill_image(channel, downloaded, unplayed)
715 self.set(iter, \
716 self.C_TITLE, channel.title, \
717 self.C_DESCRIPTION, description, \
718 self.C_ERROR, self._format_error(channel), \
719 self.C_PILL, pill_image, \
720 self.C_PILL_VISIBLE, pill_image != None, \
721 self.C_VIEW_SHOW_UNDELETED, total - deleted > 0, \
722 self.C_VIEW_SHOW_DOWNLOADED, downloaded + new > 0, \
723 self.C_VIEW_SHOW_UNPLAYED, unplayed + new > 0, \
724 self.C_HAS_EPISODES, total > 0)
726 def add_cover_by_url(self, url, pixbuf):
727 # Resize and add the new cover image
728 pixbuf = self._resize_pixbuf(url, pixbuf)
729 for row in self:
730 if row[self.C_URL] == url:
731 row[self.C_COVER] = pixbuf
732 break
734 def delete_cover_by_url(self, url):
735 # Remove the cover from the model
736 for row in self:
737 if row[self.C_URL] == url:
738 row[self.C_COVER] = None
739 break
741 # Remove the cover from the cache
742 if url in self._cover_cache:
743 del self._cover_cache[url]