1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2013 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
import model
32 from gpodder
import query
33 from gpodder
import coverart
36 logger
= logging
.getLogger(__name__
)
38 from gpodder
.gtkui
import draw
39 from gpodder
.gtkui
import flattr
53 # ----------------------------------------------------------
55 class GEpisode(model
.PodcastEpisode
):
59 def title_markup(self
):
60 return '%s\n<small>%s</small>' % (cgi
.escape(self
.title
),
61 cgi
.escape(self
.channel
.title
))
64 def markup_new_episodes(self
):
65 if self
.file_size
> 0:
66 length_str
= '%s; ' % util
.format_filesize(self
.file_size
)
69 return ('<b>%s</b>\n<small>%s'+_('released %s')+ \
70 '; '+_('from %s')+'</small>') % (\
71 cgi
.escape(re
.sub('\s+', ' ', self
.title
)), \
72 cgi
.escape(length_str
), \
73 cgi
.escape(self
.pubdate_prop
), \
74 cgi
.escape(re
.sub('\s+', ' ', self
.channel
.title
)))
77 def markup_delete_episodes(self
):
78 if self
.total_time
and self
.current_position
:
79 played_string
= self
.get_play_info_string()
81 played_string
= _('played')
83 played_string
= _('unplayed')
84 downloaded_string
= self
.get_age_string()
85 if not downloaded_string
:
86 downloaded_string
= _('today')
87 return ('<b>%s</b>\n<small>%s; %s; '+_('downloaded %s')+ \
88 '; '+_('from %s')+'</small>') % (\
89 cgi
.escape(self
.title
), \
90 cgi
.escape(util
.format_filesize(self
.file_size
)), \
91 cgi
.escape(played_string
), \
92 cgi
.escape(downloaded_string
), \
93 cgi
.escape(self
.channel
.title
))
95 class GPodcast(model
.PodcastChannel
):
98 EpisodeClass
= GEpisode
100 class Model(model
.Model
):
101 PodcastClass
= GPodcast
103 # ----------------------------------------------------------
105 # Singleton indicator if a row is a section
106 class SeparatorMarker(object): pass
107 class SectionMarker(object): pass
109 class EpisodeListModel(gtk
.ListStore
):
110 C_URL
, C_TITLE
, C_FILESIZE_TEXT
, C_EPISODE
, C_STATUS_ICON
, \
111 C_PUBLISHED_TEXT
, C_DESCRIPTION
, C_TOOLTIP
, \
112 C_VIEW_SHOW_UNDELETED
, C_VIEW_SHOW_DOWNLOADED
, \
113 C_VIEW_SHOW_UNPLAYED
, C_FILESIZE
, C_PUBLISHED
, \
114 C_TIME
, C_TIME_VISIBLE
, C_TOTAL_TIME
, \
117 VIEW_ALL
, VIEW_UNDELETED
, VIEW_DOWNLOADED
, VIEW_UNPLAYED
= range(4)
119 # In which steps the UI is updated for "loading" animations
120 _UI_UPDATE_STEP
= .03
122 # Steps for the "downloading" icon progress
125 def __init__(self
, config
, on_filter_changed
=lambda has_episodes
: None):
126 gtk
.ListStore
.__init
__(self
, str, str, str, object, \
127 str, str, str, str, bool, bool, bool, \
128 gobject
.TYPE_INT64
, int, str, bool, int, bool)
130 self
._config
= config
132 # Callback for when the filter / list changes, gets one parameter
133 # (has_episodes) that is True if the list has any episodes
134 self
._on
_filter
_changed
= on_filter_changed
136 # Filter to allow hiding some episodes
137 self
._filter
= self
.filter_new()
138 self
._sorter
= gtk
.TreeModelSort(self
._filter
)
139 self
._view
_mode
= self
.VIEW_ALL
140 self
._search
_term
= None
141 self
._search
_term
_eql
= None
142 self
._filter
.set_visible_func(self
._filter
_visible
_func
)
144 # Are we currently showing the "all episodes" view?
145 self
._all
_episodes
_view
= False
147 self
.ICON_AUDIO_FILE
= 'audio-x-generic'
148 self
.ICON_VIDEO_FILE
= 'video-x-generic'
149 self
.ICON_IMAGE_FILE
= 'image-x-generic'
150 self
.ICON_GENERIC_FILE
= 'text-x-generic'
151 self
.ICON_DOWNLOADING
= gtk
.STOCK_GO_DOWN
152 self
.ICON_DELETED
= gtk
.STOCK_DELETE
154 if 'KDE_FULL_SESSION' in os
.environ
:
155 # Workaround until KDE adds all the freedesktop icons
156 # See https://bugs.kde.org/show_bug.cgi?id=233505 and
157 # http://gpodder.org/bug/553
158 self
.ICON_DELETED
= 'archive-remove'
161 def _format_filesize(self
, episode
):
162 if episode
.file_size
> 0:
163 return util
.format_filesize(episode
.file_size
, digits
=1)
167 def _filter_visible_func(self
, model
, iter):
168 # If searching is active, set visibility based on search text
169 if self
._search
_term
is not None:
170 episode
= model
.get_value(iter, self
.C_EPISODE
)
175 return self
._search
_term
_eql
.match(episode
)
179 if self
._view
_mode
== self
.VIEW_ALL
:
181 elif self
._view
_mode
== self
.VIEW_UNDELETED
:
182 return model
.get_value(iter, self
.C_VIEW_SHOW_UNDELETED
)
183 elif self
._view
_mode
== self
.VIEW_DOWNLOADED
:
184 return model
.get_value(iter, self
.C_VIEW_SHOW_DOWNLOADED
)
185 elif self
._view
_mode
== self
.VIEW_UNPLAYED
:
186 return model
.get_value(iter, self
.C_VIEW_SHOW_UNPLAYED
)
190 def get_filtered_model(self
):
191 """Returns a filtered version of this episode model
193 The filtered version should be displayed in the UI,
194 as this model can have some filters set that should
195 be reflected in the UI.
199 def has_episodes(self
):
200 """Returns True if episodes are visible (filtered)
202 If episodes are visible with the current filter
203 applied, return True (otherwise return False).
205 return bool(len(self
._filter
))
207 def set_view_mode(self
, new_mode
):
208 """Sets a new view mode for this model
210 After setting the view mode, the filtered model
211 might be updated to reflect the new mode."""
212 if self
._view
_mode
!= new_mode
:
213 self
._view
_mode
= new_mode
214 self
._filter
.refilter()
215 self
._on
_filter
_changed
(self
.has_episodes())
217 def get_view_mode(self
):
218 """Returns the currently-set view mode"""
219 return self
._view
_mode
221 def set_search_term(self
, new_term
):
222 if self
._search
_term
!= new_term
:
223 self
._search
_term
= new_term
224 self
._search
_term
_eql
= query
.UserEQL(new_term
)
225 self
._filter
.refilter()
226 self
._on
_filter
_changed
(self
.has_episodes())
228 def get_search_term(self
):
229 return self
._search
_term
231 def _format_description(self
, episode
, include_description
=False):
232 title
= episode
.trimmed_title
234 if episode
.state
!= gpodder
.STATE_DELETED
and episode
.is_new
:
236 if include_description
and self
._all
_episodes
_view
:
237 return '%s%s%s\n%s' % (a
, cgi
.escape(title
), b
,
238 _('from %s') % cgi
.escape(episode
.channel
.title
))
239 elif include_description
:
240 description
= episode
.one_line_description()
241 if description
.startswith(title
):
242 description
= description
[len(title
):].strip()
243 return '%s%s%s\n%s' % (a
, cgi
.escape(title
), b
,
244 cgi
.escape(description
))
246 return ''.join((a
, cgi
.escape(title
), b
))
248 def replace_from_channel(self
, channel
, include_description
=False,
251 Add episode from the given channel to this model.
252 Downloading should be a callback.
253 include_description should be a boolean value (True if description
254 is to be added to the episode row, or False if not)
257 # Remove old episodes in the list store
260 if treeview
is not None:
261 util
.idle_add(treeview
.queue_draw
)
263 self
._all
_episodes
_view
= getattr(channel
, 'ALL_EPISODES_PROXY', False)
265 # Avoid gPodder bug 1291
269 episodes
= channel
.get_all_episodes()
271 if not isinstance(episodes
, list):
272 episodes
= list(episodes
)
273 count
= len(episodes
)
275 for position
, episode
in enumerate(episodes
):
276 iter = self
.append((episode
.url
, \
278 self
._format
_filesize
(episode
), \
281 episode
.cute_pubdate(), \
289 episode
.get_play_info_string(), \
290 bool(episode
.total_time
), \
291 episode
.total_time
, \
294 self
.update_by_iter(iter, include_description
)
296 self
._on
_filter
_changed
(self
.has_episodes())
298 def update_all(self
, include_description
=False):
300 self
.update_by_iter(row
.iter, include_description
)
302 def update_by_urls(self
, urls
, include_description
=False):
304 if row
[self
.C_URL
] in urls
:
305 self
.update_by_iter(row
.iter, include_description
)
307 def update_by_filter_iter(self
, iter, include_description
=False):
308 # Convenience function for use by "outside" methods that use iters
309 # from the filtered episode list model (i.e. all UI things normally)
310 iter = self
._sorter
.convert_iter_to_child_iter(None, iter)
311 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter),
314 def update_by_iter(self
, iter, include_description
=False):
315 episode
= self
.get_value(iter, self
.C_EPISODE
)
322 view_show_undeleted
= True
323 view_show_downloaded
= False
324 view_show_unplayed
= False
325 icon_theme
= gtk
.icon_theme_get_default()
327 if episode
.downloading
:
328 tooltip
.append('%s %d%%' % (_('Downloading'),
329 int(episode
.download_task
.progress
*100)))
331 index
= int(self
.PROGRESS_STEPS
*episode
.download_task
.progress
)
332 status_icon
= 'gpodder-progress-%d' % index
334 view_show_downloaded
= True
335 view_show_unplayed
= True
337 if episode
.state
== gpodder
.STATE_DELETED
:
338 tooltip
.append(_('Deleted'))
339 status_icon
= self
.ICON_DELETED
340 view_show_undeleted
= False
341 elif episode
.state
== gpodder
.STATE_NORMAL
and \
343 tooltip
.append(_('New episode'))
344 view_show_downloaded
= True
345 view_show_unplayed
= True
346 elif episode
.state
== gpodder
.STATE_DOWNLOADED
:
348 view_show_downloaded
= True
349 view_show_unplayed
= episode
.is_new
350 show_bullet
= episode
.is_new
351 show_padlock
= episode
.archive
352 show_missing
= not episode
.file_exists()
353 filename
= episode
.local_filename(create
=False, check_only
=True)
355 file_type
= episode
.file_type()
356 if file_type
== 'audio':
357 tooltip
.append(_('Downloaded episode'))
358 status_icon
= self
.ICON_AUDIO_FILE
359 elif file_type
== 'video':
360 tooltip
.append(_('Downloaded video episode'))
361 status_icon
= self
.ICON_VIDEO_FILE
362 elif file_type
== 'image':
363 tooltip
.append(_('Downloaded image'))
364 status_icon
= self
.ICON_IMAGE_FILE
366 tooltip
.append(_('Downloaded file'))
367 status_icon
= self
.ICON_GENERIC_FILE
369 # Try to find a themed icon for this file
370 if filename
is not None and have_gio
:
371 file = gio
.File(filename
)
372 if file.query_exists():
373 file_info
= file.query_info('*')
374 icon
= file_info
.get_icon()
375 for icon_name
in icon
.get_names():
376 if icon_theme
.has_icon(icon_name
):
377 status_icon
= icon_name
381 tooltip
.append(_('missing file'))
384 if file_type
== 'image':
385 tooltip
.append(_('never displayed'))
386 elif file_type
in ('audio', 'video'):
387 tooltip
.append(_('never played'))
389 tooltip
.append(_('never opened'))
391 if file_type
== 'image':
392 tooltip
.append(_('displayed'))
393 elif file_type
in ('audio', 'video'):
394 tooltip
.append(_('played'))
396 tooltip
.append(_('opened'))
398 tooltip
.append(_('deletion prevented'))
400 if episode
.total_time
> 0 and episode
.current_position
:
401 tooltip
.append('%d%%' % (100.*float(episode
.current_position
)/float(episode
.total_time
),))
403 if episode
.total_time
:
404 total_time
= util
.format_time(episode
.total_time
)
406 tooltip
.append(total_time
)
408 tooltip
= ', '.join(tooltip
)
410 description
= self
._format
_description
(episode
, include_description
)
412 self
.C_STATUS_ICON
, status_icon
, \
413 self
.C_VIEW_SHOW_UNDELETED
, view_show_undeleted
, \
414 self
.C_VIEW_SHOW_DOWNLOADED
, view_show_downloaded
, \
415 self
.C_VIEW_SHOW_UNPLAYED
, view_show_unplayed
, \
416 self
.C_DESCRIPTION
, description
, \
417 self
.C_TOOLTIP
, tooltip
, \
418 self
.C_TIME
, episode
.get_play_info_string(duration_only
=True), \
419 self
.C_TIME_VISIBLE
, bool(episode
.total_time
), \
420 self
.C_TOTAL_TIME
, episode
.total_time
, \
421 self
.C_LOCKED
, episode
.archive
, \
422 self
.C_FILESIZE_TEXT
, self
._format
_filesize
(episode
), \
423 self
.C_FILESIZE
, episode
.file_size
)
426 class PodcastChannelProxy(object):
427 ALL_EPISODES_PROXY
= True
429 def __init__(self
, db
, config
, channels
):
431 self
._config
= config
432 self
.channels
= channels
433 self
.title
= _('All episodes')
434 self
.description
= _('from all podcasts')
435 #self.parse_error = ''
439 self
.cover_file
= coverart
.CoverDownloader
.ALL_EPISODES_ID
440 self
.cover_url
= None
441 self
.auth_username
= None
442 self
.auth_password
= None
443 self
.pause_subscription
= False
444 self
.sync_to_mp3_player
= False
445 self
.auto_archive_episodes
= False
447 def get_statistics(self
):
448 # Get the total statistics for all channels from the database
449 return self
._db
.get_podcast_statistics()
451 def get_all_episodes(self
):
452 """Returns a generator that yields every episode"""
453 return Model
.sort_episodes_by_pubdate((e
for c
in self
.channels
454 for e
in c
.get_all_episodes()), True)
457 class PodcastListModel(gtk
.ListStore
):
458 C_URL
, C_TITLE
, C_DESCRIPTION
, C_PILL
, C_CHANNEL
, \
459 C_COVER
, C_ERROR
, C_PILL_VISIBLE
, \
460 C_VIEW_SHOW_UNDELETED
, C_VIEW_SHOW_DOWNLOADED
, \
461 C_VIEW_SHOW_UNPLAYED
, C_HAS_EPISODES
, C_SEPARATOR
, \
462 C_DOWNLOADS
, C_COVER_VISIBLE
, C_SECTION
= range(16)
464 SEARCH_COLUMNS
= (C_TITLE
, C_DESCRIPTION
, C_SECTION
)
467 def row_separator_func(cls
, model
, iter):
468 return model
.get_value(iter, cls
.C_SEPARATOR
)
470 def __init__(self
, cover_downloader
):
471 gtk
.ListStore
.__init
__(self
, str, str, str, gtk
.gdk
.Pixbuf
, \
472 object, gtk
.gdk
.Pixbuf
, str, bool, bool, bool, bool, \
473 bool, bool, int, bool, str)
475 # Filter to allow hiding some episodes
476 self
._filter
= self
.filter_new()
478 self
._search
_term
= None
479 self
._filter
.set_visible_func(self
._filter
_visible
_func
)
481 self
._cover
_cache
= {}
482 self
._max
_image
_side
= 40
483 self
._cover
_downloader
= cover_downloader
485 self
.ICON_DISABLED
= 'gtk-media-pause'
487 def _filter_visible_func(self
, model
, iter):
488 # If searching is active, set visibility based on search text
489 if self
._search
_term
is not None:
490 if model
.get_value(iter, self
.C_CHANNEL
) == SectionMarker
:
492 key
= self
._search
_term
.lower()
493 columns
= (model
.get_value(iter, c
) for c
in self
.SEARCH_COLUMNS
)
494 return any((key
in c
.lower() for c
in columns
if c
is not None))
496 if model
.get_value(iter, self
.C_SEPARATOR
):
498 elif self
._view
_mode
== EpisodeListModel
.VIEW_ALL
:
499 return model
.get_value(iter, self
.C_HAS_EPISODES
)
500 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNDELETED
:
501 return model
.get_value(iter, self
.C_VIEW_SHOW_UNDELETED
)
502 elif self
._view
_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
503 return model
.get_value(iter, self
.C_VIEW_SHOW_DOWNLOADED
)
504 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
505 return model
.get_value(iter, self
.C_VIEW_SHOW_UNPLAYED
)
509 def get_filtered_model(self
):
510 """Returns a filtered version of this episode model
512 The filtered version should be displayed in the UI,
513 as this model can have some filters set that should
514 be reflected in the UI.
518 def set_view_mode(self
, new_mode
):
519 """Sets a new view mode for this model
521 After setting the view mode, the filtered model
522 might be updated to reflect the new mode."""
523 if self
._view
_mode
!= new_mode
:
524 self
._view
_mode
= new_mode
525 self
._filter
.refilter()
527 def get_view_mode(self
):
528 """Returns the currently-set view mode"""
529 return self
._view
_mode
531 def set_search_term(self
, new_term
):
532 if self
._search
_term
!= new_term
:
533 self
._search
_term
= new_term
534 self
._filter
.refilter()
536 def get_search_term(self
):
537 return self
._search
_term
539 def enable_separators(self
, channeltree
):
540 channeltree
.set_row_separator_func(self
._show
_row
_separator
)
542 def _show_row_separator(self
, model
, iter):
543 return model
.get_value(iter, self
.C_SEPARATOR
)
545 def _resize_pixbuf_keep_ratio(self
, url
, pixbuf
):
547 Resizes a GTK Pixbuf but keeps its aspect ratio.
548 Returns None if the pixbuf does not need to be
549 resized or the newly resized pixbuf if it does.
554 if url
in self
._cover
_cache
:
555 return self
._cover
_cache
[url
]
558 if pixbuf
.get_width() > self
._max
_image
_side
:
559 f
= float(self
._max
_image
_side
)/pixbuf
.get_width()
560 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
561 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
565 if pixbuf
.get_height() > self
._max
_image
_side
:
566 f
= float(self
._max
_image
_side
)/pixbuf
.get_height()
567 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
568 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
572 self
._cover
_cache
[url
] = pixbuf
577 def _resize_pixbuf(self
, url
, pixbuf
):
581 return self
._resize
_pixbuf
_keep
_ratio
(url
, pixbuf
) or pixbuf
583 def _overlay_pixbuf(self
, pixbuf
, icon
):
585 icon_theme
= gtk
.icon_theme_get_default()
586 emblem
= icon_theme
.load_icon(icon
, self
._max
_image
_side
/2, 0)
587 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
588 xpos
= pixbuf
.get_width() - width
589 ypos
= pixbuf
.get_height() - height
591 # need to resize overlay for none standard icon size
592 emblem
= icon_theme
.load_icon(icon
, pixbuf
.get_height() - 1, 0)
593 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
594 xpos
= pixbuf
.get_width() - width
595 ypos
= pixbuf
.get_height() - height
596 emblem
.composite(pixbuf
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
602 def _get_cover_image(self
, channel
, add_overlay
=False):
603 if self
._cover
_downloader
is None:
606 pixbuf
= self
._cover
_downloader
.get_cover(channel
, avoid_downloading
=True)
607 pixbuf_overlay
= self
._resize
_pixbuf
(channel
.url
, pixbuf
)
608 if add_overlay
and channel
.pause_subscription
:
609 pixbuf_overlay
= self
._overlay
_pixbuf
(pixbuf_overlay
, self
.ICON_DISABLED
)
610 pixbuf_overlay
.saturate_and_pixelate(pixbuf_overlay
, 0.0, False)
612 return pixbuf_overlay
614 def _get_pill_image(self
, channel
, count_downloaded
, count_unplayed
):
615 if count_unplayed
> 0 or count_downloaded
> 0:
616 return draw
.draw_pill_pixbuf(str(count_unplayed
), str(count_downloaded
))
620 def _format_description(self
, channel
, total
, deleted
, \
621 new
, downloaded
, unplayed
):
622 title_markup
= cgi
.escape(channel
.title
)
623 if not channel
.pause_subscription
:
624 description_markup
= cgi
.escape(util
.get_first_line(channel
.description
) or ' ')
626 description_markup
= cgi
.escape(_('Subscription paused'))
629 d
.append('<span weight="bold">')
630 d
.append(title_markup
)
634 if description_markup
.strip():
635 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)
646 def set_channels(self
, db
, config
, channels
):
647 # Clear the model and update the list of podcasts
650 def channel_to_row(channel
, add_overlay
=False):
651 return (channel
.url
, '', '', None, channel
,
652 self
._get
_cover
_image
(channel
, add_overlay
), '', True,
653 True, True, True, True, False, 0, True, '')
655 if config
.podcast_list_view_all
and channels
:
656 all_episodes
= PodcastChannelProxy(db
, config
, channels
)
657 iter = self
.append(channel_to_row(all_episodes
))
658 self
.update_by_iter(iter)
661 if not config
.podcast_list_sections
:
662 self
.append(('', '', '', None, SeparatorMarker
, None, '',
663 True, True, True, True, True, True, 0, False, ''))
666 section
, podcast
= pair
667 return (section
, model
.Model
.podcast_sort_key(podcast
))
669 if config
.podcast_list_sections
:
670 def convert(channels
):
671 for channel
in channels
:
672 yield (channel
.group_by
, channel
)
674 def convert(channels
):
675 for channel
in channels
:
676 yield (None, channel
)
680 for section
, channel
in sorted(convert(channels
), key
=key_func
):
681 if old_section
!= section
:
682 it
= self
.append(('-', section
, '', None, SectionMarker
, None,
683 '', True, True, True, True, True, False, 0, False, section
))
684 added_sections
.append(it
)
685 old_section
= section
687 iter = self
.append(channel_to_row(channel
, True))
688 self
.update_by_iter(iter)
690 # Update section header stats only after all podcasts
691 # have been added to the list to get the stats right
692 for it
in added_sections
:
693 self
.update_by_iter(it
)
695 def get_filter_path_from_url(self
, url
):
696 # Return the path of the filtered model for a given URL
697 child_path
= self
.get_path_from_url(url
)
698 if child_path
is None:
701 return self
._filter
.convert_child_path_to_path(child_path
)
703 def get_path_from_url(self
, url
):
704 # Return the tree model path for a given URL
709 if row
[self
.C_URL
] == url
:
713 def update_first_row(self
):
714 # Update the first row in the model (for "all episodes" updates)
715 self
.update_by_iter(self
.get_iter_first())
717 def update_by_urls(self
, urls
):
718 # Given a list of URLs, update each matching row
720 if row
[self
.C_URL
] in urls
:
721 self
.update_by_iter(row
.iter)
723 def iter_is_first_row(self
, iter):
724 iter = self
._filter
.convert_iter_to_child_iter(iter)
725 path
= self
.get_path(iter)
726 return (path
== (0,))
728 def update_by_filter_iter(self
, iter):
729 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter))
731 def update_all(self
):
733 self
.update_by_iter(row
.iter)
735 def update_sections(self
):
737 if row
[self
.C_CHANNEL
] is SectionMarker
:
738 self
.update_by_iter(row
.iter)
740 def update_by_iter(self
, iter):
744 # Given a GtkTreeIter, update volatile information
745 channel
= self
.get_value(iter, self
.C_CHANNEL
)
747 if channel
is SectionMarker
:
748 section
= self
.get_value(iter, self
.C_TITLE
)
750 # This row is a section header - update its visibility flags
751 channels
= [c
for c
in (row
[self
.C_CHANNEL
] for row
in self
)
752 if isinstance(c
, GPodcast
) and c
.section
== section
]
754 # Calculate the stats over all podcasts of this section
755 total
, deleted
, new
, downloaded
, unplayed
= map(sum,
756 zip(*[c
.get_statistics() for c
in channels
]))
758 # We could customized the section header here with the list
759 # of channels and their stats (i.e. add some "new" indicator)
760 description
= '<span size="16000"> </span><b>%s</b>' % (
764 self
.C_DESCRIPTION
, description
,
765 self
.C_SECTION
, section
,
766 self
.C_VIEW_SHOW_UNDELETED
, total
- deleted
> 0,
767 self
.C_VIEW_SHOW_DOWNLOADED
, downloaded
+ new
> 0,
768 self
.C_VIEW_SHOW_UNPLAYED
, unplayed
+ new
> 0)
770 if (not isinstance(channel
, GPodcast
) and
771 not isinstance(channel
, PodcastChannelProxy
)):
774 total
, deleted
, new
, downloaded
, unplayed
= channel
.get_statistics()
775 description
= self
._format
_description
(channel
, total
, deleted
, new
, \
776 downloaded
, unplayed
)
778 pill_image
= self
._get
_pill
_image
(channel
, downloaded
, unplayed
)
781 self
.C_TITLE
, channel
.title
, \
782 self
.C_DESCRIPTION
, description
, \
783 self
.C_SECTION
, channel
.section
, \
784 self
.C_ERROR
, self
._format
_error
(channel
), \
785 self
.C_PILL
, pill_image
, \
786 self
.C_PILL_VISIBLE
, pill_image
!= None, \
787 self
.C_VIEW_SHOW_UNDELETED
, total
- deleted
> 0, \
788 self
.C_VIEW_SHOW_DOWNLOADED
, downloaded
+ new
> 0, \
789 self
.C_VIEW_SHOW_UNPLAYED
, unplayed
+ new
> 0, \
790 self
.C_HAS_EPISODES
, total
> 0, \
791 self
.C_DOWNLOADS
, downloaded
)
793 def clear_cover_cache(self
, podcast_url
):
794 if podcast_url
in self
._cover
_cache
:
795 logger
.info('Clearing cover from cache: %s', podcast_url
)
796 del self
._cover
_cache
[podcast_url
]
798 def add_cover_by_channel(self
, channel
, pixbuf
):
799 # Resize and add the new cover image
800 pixbuf
= self
._resize
_pixbuf
(channel
.url
, pixbuf
)
801 if channel
.pause_subscription
:
802 pixbuf
= self
._overlay
_pixbuf
(pixbuf
, self
.ICON_DISABLED
)
803 pixbuf
.saturate_and_pixelate(pixbuf
, 0.0, False)
806 if row
[self
.C_URL
] == channel
.url
:
807 row
[self
.C_COVER
] = pixbuf