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)
30 from gpodder
import util
31 from gpodder
.liblogger
import log
33 from gpodder
.gtkui
import draw
37 import xml
.sax
.saxutils
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
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
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)
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
:
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
)
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.
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()))
159 return xml
.sax
.saxutils
.escape(episode
.title
)
161 def add_from_channel(self
, channel
, downloading
=None, \
162 include_description
=False, generate_thumbnails
=False, \
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
):
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):
207 self
.update_by_iter(row
.iter, downloading
, include_description
, \
210 def update_by_urls(self
, urls
, downloading
=None, include_description
=False, \
211 generate_thumbnails
=False):
213 if row
[self
.C_URL
] in urls
:
214 self
.update_by_iter(row
.iter, downloading
, include_description
, \
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
)
228 episode
.reload_from_db()
230 if include_description
or gpodder
.ui
.maemo
:
239 status_icon_to_build_from_file
= False
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
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
:
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
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
305 tooltip
.append(_('missing file'))
308 if file_type
== 'image':
309 tooltip
.append(_('never displayed'))
310 elif file_type
in ('audio', 'video'):
311 tooltip
.append(_('never played'))
313 tooltip
.append(_('never opened'))
315 if file_type
== 'image':
316 tooltip
.append(_('displayed'))
317 elif file_type
in ('audio', 'video'):
318 tooltip
.append(_('played'))
320 tooltip
.append(_('opened'))
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
)
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
)
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
):
356 # load image from disc (code adapted from CoverDownloader
357 # except that no download is needed here)
358 loader
= gtk
.gdk
.PixbufLoader()
361 loader
.write(open(image_path
, 'rb').read())
363 pixbuf
= loader
.get_pixbuf()
365 log('Data error while loading image %s', image_path
, sender
=self
)
367 # Now scale the image with ratio (copied from _resize_pixbuf_keep_ratio)
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
)
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
)
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()
407 if build_icon_from_file
:
408 icon
= self
._get
_icon
_from
_image
(icon_name
,icon_size
)
410 icon
= icon_theme
.load_icon(icon_name
, icon_size
, 0)
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()
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)
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)
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)
447 self
._icon
_cache
[cache_id
] = icon
451 class PodcastChannelProxy(object):
452 ALL_EPISODES_PROXY
= True
454 def __init__(self
, db
, config
, channels
):
456 self
._config
= config
457 self
.channels
= channels
458 self
.title
= _('All episodes')
459 self
.description
= _('from all podcasts')
460 self
.parse_error
= ''
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
):
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
)
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()
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
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
):
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
)
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.
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.
585 if url
in self
._cover
_cache
:
586 return self
._cover
_cache
[url
]
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
)
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
)
603 self
._cover
_cache
[url
] = pixbuf
608 def _resize_pixbuf(self
, url
, pixbuf
):
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:
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
))
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 ' ')
633 d
.append('<span weight="bold">')
634 d
.append(title_markup
)
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
)
645 def set_channels(self
, db
, config
, channels
):
646 # Clear the model and update the list of podcasts
649 if config
.podcast_list_view_all
and channels
:
650 all_episodes
= PodcastChannelProxy(db
, config
, channels
)
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)
660 self
.set(iter, self
.C_SEPARATOR
, True)
662 for channel
in channels
:
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:
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
685 if row
[self
.C_URL
] == url
:
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
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
):
709 self
.update_by_iter(row
.iter)
711 def update_by_iter(self
, iter):
712 # Given a GtkTreeIter, update volatile information
714 channel
= self
.get_value(iter, self
.C_CHANNEL
)
715 except TypeError, te
:
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
)
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
)
739 if row
[self
.C_URL
] == url
:
740 row
[self
.C_COVER
] = pixbuf
743 def delete_cover_by_url(self
, url
):
744 # Remove the cover from the model
746 if row
[self
.C_URL
] == url
:
747 row
[self
.C_COVER
] = None
750 # Remove the cover from the cache
751 if url
in self
._cover
_cache
:
752 del self
._cover
_cache
[url
]