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
, C_FILESIZE
, C_PUBLISHED
, \
50 C_TIME
, C_TIME1_VISIBLE
, C_TIME2_VISIBLE
, \
53 SEARCH_COLUMNS
= (C_TITLE
, C_DESCRIPTION
)
55 VIEW_ALL
, VIEW_UNDELETED
, VIEW_DOWNLOADED
, VIEW_UNPLAYED
= range(4)
57 # In which steps the UI is updated for "loading" animations
60 def __init__(self
, on_filter_changed
=lambda has_episodes
: None):
61 gtk
.ListStore
.__init
__(self
, str, str, str, object, \
62 str, str, str, str, bool, bool, bool, \
63 int, int, str, bool, bool, bool)
65 # Callback for when the filter / list changes, gets one parameter
66 # (has_episodes) that is True if the list has any episodes
67 self
._on
_filter
_changed
= on_filter_changed
69 # Filter to allow hiding some episodes
70 self
._filter
= self
.filter_new()
71 self
._sorter
= gtk
.TreeModelSort(self
._filter
)
72 self
._view
_mode
= self
.VIEW_ALL
73 self
._search
_term
= None
74 self
._filter
.set_visible_func(self
._filter
_visible
_func
)
76 # Are we currently showing the "all episodes" view?
77 self
._all
_episodes
_view
= False
79 # "ICON" is used to mark icon names in source files
83 self
.ICON_AUDIO_FILE
= ICON('audio-x-generic')
84 self
.ICON_VIDEO_FILE
= ICON('video-x-generic')
85 self
.ICON_IMAGE_FILE
= ICON('image-x-generic')
86 self
.ICON_GENERIC_FILE
= ICON('text-x-generic')
87 self
.ICON_DOWNLOADING
= gtk
.STOCK_GO_DOWN
88 self
.ICON_DELETED
= gtk
.STOCK_DELETE
89 self
.ICON_NEW
= gtk
.STOCK_ABOUT
90 self
.ICON_UNPLAYED
= ICON('emblem-new')
91 self
.ICON_LOCKED
= ICON('emblem-readonly')
92 self
.ICON_MISSING
= ICON('emblem-unreadable')
94 if 'KDE_FULL_SESSION' in os
.environ
:
95 # Workaround until KDE adds all the freedesktop icons
96 # See https://bugs.kde.org/show_bug.cgi?id=233505 and
97 # http://gpodder.org/bug/553
98 self
.ICON_DELETED
= ICON('archive-remove')
99 self
.ICON_UNPLAYED
= ICON('vcs-locally-modified')
100 self
.ICON_LOCKED
= ICON('emblem-locked')
101 self
.ICON_MISSING
= ICON('vcs-conflicting')
104 def _format_filesize(self
, episode
):
105 if episode
.length
> 0:
106 return util
.format_filesize(episode
.length
, 1)
111 def _filter_visible_func(self
, model
, iter):
112 # If searching is active, set visibility based on search text
113 if self
._search
_term
is not None:
114 key
= self
._search
_term
.lower()
115 return any((key
in (model
.get_value(iter, column
) or '').lower()) for column
in self
.SEARCH_COLUMNS
)
117 if self
._view
_mode
== self
.VIEW_ALL
:
119 elif self
._view
_mode
== self
.VIEW_UNDELETED
:
120 return model
.get_value(iter, self
.C_VIEW_SHOW_UNDELETED
)
121 elif self
._view
_mode
== self
.VIEW_DOWNLOADED
:
122 return model
.get_value(iter, self
.C_VIEW_SHOW_DOWNLOADED
)
123 elif self
._view
_mode
== self
.VIEW_UNPLAYED
:
124 return model
.get_value(iter, self
.C_VIEW_SHOW_UNPLAYED
)
128 def get_filtered_model(self
):
129 """Returns a filtered version of this episode model
131 The filtered version should be displayed in the UI,
132 as this model can have some filters set that should
133 be reflected in the UI.
137 def has_episodes(self
):
138 """Returns True if episodes are visible (filtered)
140 If episodes are visible with the current filter
141 applied, return True (otherwise return False).
143 return bool(len(self
._filter
))
145 def set_view_mode(self
, new_mode
):
146 """Sets a new view mode for this model
148 After setting the view mode, the filtered model
149 might be updated to reflect the new mode."""
150 if self
._view
_mode
!= new_mode
:
151 self
._view
_mode
= new_mode
152 self
._filter
.refilter()
153 self
._on
_filter
_changed
(self
.has_episodes())
155 def get_view_mode(self
):
156 """Returns the currently-set view mode"""
157 return self
._view
_mode
159 def set_search_term(self
, new_term
):
160 if self
._search
_term
!= new_term
:
161 self
._search
_term
= new_term
162 self
._filter
.refilter()
163 self
._on
_filter
_changed
(self
.has_episodes())
165 def get_search_term(self
):
166 return self
._search
_term
168 def _format_description(self
, episode
, include_description
=False, is_downloading
=None):
170 if episode
.state
!= gpodder
.STATE_DELETED
and not episode
.is_played
:
172 if include_description
and self
._all
_episodes
_view
:
173 return '%s%s%s\n<small>%s</small>' % (a
, xml
.sax
.saxutils
.escape(episode
.title
), b
,
174 _('from %s') % xml
.sax
.saxutils
.escape(episode
.channel
.title
))
175 elif include_description
:
176 return '%s%s%s\n<small>%s</small>' % (a
, xml
.sax
.saxutils
.escape(episode
.title
), b
,
177 xml
.sax
.saxutils
.escape(episode
.one_line_description()))
179 return xml
.sax
.saxutils
.escape(episode
.title
)
181 def replace_from_channel(self
, channel
, downloading
=None, \
182 include_description
=False, generate_thumbnails
=False, \
185 Add episode from the given channel to this model.
186 Downloading should be a callback.
187 include_description should be a boolean value (True if description
188 is to be added to the episode row, or False if not)
191 # Remove old episodes in the list store
194 if treeview
is not None:
195 util
.idle_add(treeview
.queue_draw
)
197 self
._all
_episodes
_view
= getattr(channel
, 'ALL_EPISODES_PROXY', False)
199 episodes
= channel
.get_all_episodes()
200 if not isinstance(episodes
, list):
201 episodes
= list(episodes
)
202 count
= len(episodes
)
204 for position
, episode
in enumerate(episodes
):
205 iter = self
.append((episode
.url
, \
207 self
._format
_filesize
(episode
), \
210 episode
.cute_pubdate(), \
218 episode
.get_play_info_string(), \
219 episode
.total_time
and not episode
.current_position
, \
220 episode
.total_time
and episode
.current_position
, \
223 self
.update_by_iter(iter, downloading
, include_description
, \
224 generate_thumbnails
, reload_from_db
=False)
226 self
._on
_filter
_changed
(self
.has_episodes())
228 def update_all(self
, downloading
=None, include_description
=False, \
229 generate_thumbnails
=False):
231 self
.update_by_iter(row
.iter, downloading
, include_description
, \
234 def update_by_urls(self
, urls
, downloading
=None, include_description
=False, \
235 generate_thumbnails
=False):
237 if row
[self
.C_URL
] in urls
:
238 self
.update_by_iter(row
.iter, downloading
, include_description
, \
241 def update_by_filter_iter(self
, iter, downloading
=None, \
242 include_description
=False, generate_thumbnails
=False):
243 # Convenience function for use by "outside" methods that use iters
244 # from the filtered episode list model (i.e. all UI things normally)
245 iter = self
._sorter
.convert_iter_to_child_iter(None, iter)
246 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter), \
247 downloading
, include_description
, generate_thumbnails
)
249 def update_by_iter(self
, iter, downloading
=None, include_description
=False, \
250 generate_thumbnails
=False, reload_from_db
=True):
251 episode
= self
.get_value(iter, self
.C_EPISODE
)
253 episode
.reload_from_db()
255 if include_description
or gpodder
.ui
.maemo
:
264 status_icon_to_build_from_file
= False
266 view_show_undeleted
= True
267 view_show_downloaded
= False
268 view_show_unplayed
= False
269 icon_theme
= gtk
.icon_theme_get_default()
271 if downloading
is not None and downloading(episode
):
272 tooltip
.append(_('Downloading'))
273 status_icon
= self
.ICON_DOWNLOADING
274 view_show_downloaded
= True
275 view_show_unplayed
= True
277 if episode
.state
== gpodder
.STATE_DELETED
:
278 tooltip
.append(_('Deleted'))
279 status_icon
= self
.ICON_DELETED
280 view_show_undeleted
= False
281 elif episode
.state
== gpodder
.STATE_NORMAL
and \
282 not episode
.is_played
:
283 tooltip
.append(_('New episode'))
284 status_icon
= self
.ICON_NEW
285 view_show_downloaded
= True
286 view_show_unplayed
= True
287 elif episode
.state
== gpodder
.STATE_DOWNLOADED
:
289 view_show_downloaded
= True
290 view_show_unplayed
= not episode
.is_played
291 show_bullet
= not episode
.is_played
292 show_padlock
= episode
.is_locked
293 show_missing
= not episode
.file_exists()
294 filename
= episode
.local_filename(create
=False, check_only
=True)
296 file_type
= episode
.file_type()
297 if file_type
== 'audio':
298 tooltip
.append(_('Downloaded episode'))
299 status_icon
= self
.ICON_AUDIO_FILE
300 elif file_type
== 'video':
301 tooltip
.append(_('Downloaded video episode'))
302 status_icon
= self
.ICON_VIDEO_FILE
303 elif file_type
== 'image':
304 tooltip
.append(_('Downloaded image'))
305 status_icon
= self
.ICON_IMAGE_FILE
307 # Optional thumbnailing for image downloads
308 if generate_thumbnails
:
309 if filename
is not None:
310 # set the status icon to the path itself (that
311 # should be a good identifier anyway)
312 status_icon
= filename
313 status_icon_to_build_from_file
= True
315 tooltip
.append(_('Downloaded file'))
316 status_icon
= self
.ICON_GENERIC_FILE
318 # Try to find a themed icon for this file
319 if filename
is not None and have_gio
:
320 file = gio
.File(filename
)
321 if file.query_exists():
322 file_info
= file.query_info('*')
323 icon
= file_info
.get_icon()
324 for icon_name
in icon
.get_names():
325 if icon_theme
.has_icon(icon_name
):
326 status_icon
= icon_name
330 tooltip
.append(_('missing file'))
333 if file_type
== 'image':
334 tooltip
.append(_('never displayed'))
335 elif file_type
in ('audio', 'video'):
336 tooltip
.append(_('never played'))
338 tooltip
.append(_('never opened'))
340 if file_type
== 'image':
341 tooltip
.append(_('displayed'))
342 elif file_type
in ('audio', 'video'):
343 tooltip
.append(_('played'))
345 tooltip
.append(_('opened'))
347 tooltip
.append(_('deletion prevented'))
349 if episode
.total_time
> 0 and episode
.current_position
:
350 tooltip
.append('%d%%' % (100.*float(episode
.current_position
)/float(episode
.total_time
),))
352 if episode
.total_time
:
353 total_time
= util
.format_time(episode
.total_time
)
355 tooltip
.append(total_time
)
357 tooltip
= ', '.join(tooltip
)
359 description
= self
._format
_description
(episode
, include_description
, downloading
)
361 self
.C_STATUS_ICON
, status_icon
, \
362 self
.C_VIEW_SHOW_UNDELETED
, view_show_undeleted
, \
363 self
.C_VIEW_SHOW_DOWNLOADED
, view_show_downloaded
, \
364 self
.C_VIEW_SHOW_UNPLAYED
, view_show_unplayed
, \
365 self
.C_DESCRIPTION
, description
, \
366 self
.C_TOOLTIP
, tooltip
, \
367 self
.C_TIME
, episode
.get_play_info_string(), \
368 self
.C_TIME1_VISIBLE
, episode
.total_time
and not episode
.current_position
, \
369 self
.C_TIME2_VISIBLE
, episode
.total_time
and episode
.current_position
, \
370 self
.C_LOCKED
, episode
.is_locked
)
372 def _get_icon_from_image(self
,image_path
, icon_size
):
374 Load an local image file and transform it into an icon.
376 Return a pixbuf scaled to the desired size and may return None
377 if the icon creation is impossible (file not found etc).
379 if not os
.path
.exists(image_path
):
381 # load image from disc (code adapted from CoverDownloader
382 # except that no download is needed here)
383 loader
= gtk
.gdk
.PixbufLoader()
386 loader
.write(open(image_path
, 'rb').read())
388 pixbuf
= loader
.get_pixbuf()
390 log('Data error while loading image %s', image_path
, sender
=self
)
392 # Now scale the image with ratio (copied from _resize_pixbuf_keep_ratio)
394 if pixbuf
.get_width() > icon_size
:
395 f
= float(icon_size
)/pixbuf
.get_width()
396 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
397 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
399 if pixbuf
.get_height() > icon_size
:
400 f
= float(icon_size
)/pixbuf
.get_height()
401 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
402 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
406 def _get_tree_icon(self
, icon_name
, add_bullet
=False, \
407 add_padlock
=False, add_missing
=False, icon_size
=32, \
408 build_icon_from_file
= False):
410 Loads an icon from the current icon theme at the specified
411 size, suitable for display in a gtk.TreeView. Additional
412 emblems can be added on top of the icon.
414 Caching is used to speed up the icon lookup.
416 The `build_icon_from_file` argument indicates (when True) that
417 the icon has to be created on the fly from a given image
418 file. The `icon_name` argument is then interpreted as the path
419 to this file. Those specific icons will *not be cached*.
422 # Add all variables that modify the appearance of the icon, so
423 # our cache does not return the same icons for different requests
424 cache_id
= (icon_name
, add_bullet
, add_padlock
, add_missing
, icon_size
)
426 if cache_id
in self
._icon
_cache
:
427 return self
._icon
_cache
[cache_id
]
429 icon_theme
= gtk
.icon_theme_get_default()
432 if build_icon_from_file
:
433 icon
= self
._get
_icon
_from
_image
(icon_name
,icon_size
)
435 icon
= icon_theme
.load_icon(icon_name
, icon_size
, 0)
438 log('Missing icon in theme: %s', icon_name
, sender
=self
)
439 icon
= icon_theme
.load_icon(gtk
.STOCK_DIALOG_QUESTION
, \
442 log('Please install the GNOME icon theme.', sender
=self
)
443 icon
= gtk
.gdk
.Pixbuf(gtk
.gdk
.COLORSPACE_RGB
, \
444 True, 8, icon_size
, icon_size
)
446 if icon
and (add_bullet
or add_padlock
or add_missing
):
447 # We'll modify the icon, so use .copy()
451 # Desaturate the icon so it looks even more "missing"
452 icon
.saturate_and_pixelate(icon
, 0.0, False)
453 emblem
= icon_theme
.load_icon(self
.ICON_MISSING
, icon_size
/2, 0)
454 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
455 xpos
= icon
.get_width() - width
456 ypos
= icon
.get_height() - height
457 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
463 emblem
= icon_theme
.load_icon(self
.ICON_UNPLAYED
, icon_size
/2, 0)
464 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
465 xpos
= icon
.get_width() - width
466 ypos
= icon
.get_height() - height
467 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
473 emblem
= icon_theme
.load_icon(self
.ICON_LOCKED
, icon_size
/2, 0)
474 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
475 emblem
.composite(icon
, 0, 0, width
, height
, 0, 0, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
479 self
._icon
_cache
[cache_id
] = icon
483 class PodcastChannelProxy(object):
484 ALL_EPISODES_PROXY
= True
486 def __init__(self
, db
, config
, channels
):
488 self
._config
= config
489 self
.channels
= channels
490 self
.title
= _('All episodes')
491 self
.description
= _('from all podcasts')
492 self
.parse_error
= ''
495 self
._save
_dir
_size
_set
= False
496 self
.save_dir_size
= 0L
497 self
.cover_file
= os
.path
.join(gpodder
.images_folder
, 'podcast-all.png')
498 self
.feed_update_enabled
= True
500 def __getattribute__(self
, name
):
502 return object.__getattribute
__(self
, name
)
503 except AttributeError:
504 log('Unsupported method call (%s)', name
, sender
=self
)
506 def get_statistics(self
):
507 # Get the total statistics for all channels from the database
508 return self
._db
.get_total_count()
510 def get_all_episodes(self
):
511 """Returns a generator that yields every episode"""
512 channel_lookup_map
= dict((c
.id, c
) for c
in self
.channels
)
513 return self
._db
.load_all_episodes(channel_lookup_map
)
515 def request_save_dir_size(self
):
516 if not self
._save
_dir
_size
_set
:
517 self
.update_save_dir_size()
518 self
._save
_dir
_size
_set
= True
520 def update_save_dir_size(self
):
521 self
.save_dir_size
= util
.calculate_size(self
._config
.download_dir
)
524 class PodcastListModel(gtk
.ListStore
):
525 C_URL
, C_TITLE
, C_DESCRIPTION
, C_PILL
, C_CHANNEL
, \
526 C_COVER
, C_ERROR
, C_PILL_VISIBLE
, \
527 C_VIEW_SHOW_UNDELETED
, C_VIEW_SHOW_DOWNLOADED
, \
528 C_VIEW_SHOW_UNPLAYED
, C_HAS_EPISODES
, C_SEPARATOR
, \
529 C_DOWNLOADS
= range(14)
531 SEARCH_COLUMNS
= (C_TITLE
, C_DESCRIPTION
)
534 def row_separator_func(cls
, model
, iter):
535 return model
.get_value(iter, cls
.C_SEPARATOR
)
537 def __init__(self
, cover_downloader
):
538 gtk
.ListStore
.__init
__(self
, str, str, str, gtk
.gdk
.Pixbuf
, \
539 object, gtk
.gdk
.Pixbuf
, str, bool, bool, bool, bool, \
542 # Filter to allow hiding some episodes
543 self
._filter
= self
.filter_new()
545 self
._search
_term
= None
546 self
._filter
.set_visible_func(self
._filter
_visible
_func
)
548 self
._cover
_cache
= {}
549 if gpodder
.ui
.fremantle
:
550 self
._max
_image
_side
= 64
552 self
._max
_image
_side
= 40
553 self
._cover
_downloader
= cover_downloader
555 # "ICON" is used to mark icon names in source files
558 #self.ICON_DISABLED = ICON('emblem-unreadable')
559 self
.ICON_DISABLED
= ICON('gtk-media-pause')
561 def _filter_visible_func(self
, model
, iter):
562 # If searching is active, set visibility based on search text
563 if self
._search
_term
is not None:
564 key
= self
._search
_term
.lower()
565 columns
= (model
.get_value(iter, c
) for c
in self
.SEARCH_COLUMNS
)
566 return any((key
in c
.lower() for c
in columns
if c
is not None))
568 if model
.get_value(iter, self
.C_SEPARATOR
):
570 if self
._view
_mode
== EpisodeListModel
.VIEW_ALL
:
571 return model
.get_value(iter, self
.C_HAS_EPISODES
)
572 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNDELETED
:
573 return model
.get_value(iter, self
.C_VIEW_SHOW_UNDELETED
)
574 elif self
._view
_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
575 return model
.get_value(iter, self
.C_VIEW_SHOW_DOWNLOADED
)
576 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
577 return model
.get_value(iter, self
.C_VIEW_SHOW_UNPLAYED
)
581 def get_filtered_model(self
):
582 """Returns a filtered version of this episode model
584 The filtered version should be displayed in the UI,
585 as this model can have some filters set that should
586 be reflected in the UI.
590 def set_view_mode(self
, new_mode
):
591 """Sets a new view mode for this model
593 After setting the view mode, the filtered model
594 might be updated to reflect the new mode."""
595 if self
._view
_mode
!= new_mode
:
596 self
._view
_mode
= new_mode
597 self
._filter
.refilter()
599 def get_view_mode(self
):
600 """Returns the currently-set view mode"""
601 return self
._view
_mode
603 def set_search_term(self
, new_term
):
604 if self
._search
_term
!= new_term
:
605 self
._search
_term
= new_term
606 self
._filter
.refilter()
608 def get_search_term(self
):
609 return self
._search
_term
611 def enable_separators(self
, channeltree
):
612 channeltree
.set_row_separator_func(self
._show
_row
_separator
)
614 def _show_row_separator(self
, model
, iter):
615 return model
.get_value(iter, self
.C_SEPARATOR
)
617 def _resize_pixbuf_keep_ratio(self
, url
, pixbuf
):
619 Resizes a GTK Pixbuf but keeps its aspect ratio.
620 Returns None if the pixbuf does not need to be
621 resized or the newly resized pixbuf if it does.
626 if url
in self
._cover
_cache
:
627 return self
._cover
_cache
[url
]
630 if pixbuf
.get_width() > self
._max
_image
_side
:
631 f
= float(self
._max
_image
_side
)/pixbuf
.get_width()
632 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
633 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
637 if pixbuf
.get_height() > self
._max
_image
_side
:
638 f
= float(self
._max
_image
_side
)/pixbuf
.get_height()
639 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
640 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
644 self
._cover
_cache
[url
] = pixbuf
649 def _resize_pixbuf(self
, url
, pixbuf
):
653 return self
._resize
_pixbuf
_keep
_ratio
(url
, pixbuf
) or pixbuf
655 def _overlay_pixbuf(self
, pixbuf
, icon
):
657 icon_theme
= gtk
.icon_theme_get_default()
658 emblem
= icon_theme
.load_icon(icon
, self
._max
_image
_side
/2, 0)
659 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
660 xpos
= pixbuf
.get_width() - width
661 ypos
= pixbuf
.get_height() - height
663 # need to resize overlay for none standard icon size
664 emblem
= icon_theme
.load_icon(icon
, pixbuf
.get_height() - 1, 0)
665 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
666 xpos
= pixbuf
.get_width() - width
667 ypos
= pixbuf
.get_height() - height
668 emblem
.composite(pixbuf
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
674 def _get_cover_image(self
, channel
, add_overlay
=False):
675 if self
._cover
_downloader
is None:
678 pixbuf
= self
._cover
_downloader
.get_cover(channel
, avoid_downloading
=True)
679 pixbuf_overlay
= self
._resize
_pixbuf
(channel
.url
, pixbuf
)
680 if add_overlay
and not channel
.feed_update_enabled
:
681 pixbuf_overlay
= self
._overlay
_pixbuf
(pixbuf_overlay
, self
.ICON_DISABLED
)
682 pixbuf_overlay
.saturate_and_pixelate(pixbuf_overlay
, 0.0, False)
684 return pixbuf_overlay
686 def _get_pill_image(self
, channel
, count_downloaded
, count_unplayed
):
687 if count_unplayed
> 0 or count_downloaded
> 0:
688 return draw
.draw_pill_pixbuf(str(count_unplayed
), str(count_downloaded
))
692 def _format_description(self
, channel
, total
, deleted
, \
693 new
, downloaded
, unplayed
):
694 title_markup
= xml
.sax
.saxutils
.escape(channel
.title
)
695 if channel
.feed_update_enabled
:
696 description_markup
= xml
.sax
.saxutils
.escape(util
.get_first_line(channel
.description
) or ' ')
698 description_markup
= xml
.sax
.saxutils
.escape(_('Subscription paused.'))
701 d
.append('<span weight="bold">')
702 d
.append(title_markup
)
705 return ''.join(d
+['\n', '<small>', description_markup
, '</small>'])
707 def _format_error(self
, channel
):
708 if channel
.parse_error
:
709 return str(channel
.parse_error
)
713 def set_channels(self
, db
, config
, channels
):
714 # Clear the model and update the list of podcasts
717 def channel_to_row(channel
, add_overlay
=False):
718 return (channel
.url
, '', '', None, channel
, \
719 self
._get
_cover
_image
(channel
, add_overlay
), '', True, True, True, \
720 True, True, False, 0)
722 if config
.podcast_list_view_all
and channels
:
723 all_episodes
= PodcastChannelProxy(db
, config
, channels
)
724 iter = self
.append(channel_to_row(all_episodes
))
725 self
.update_by_iter(iter)
728 self
.append(('', '', '', None, None, None, '', True, True, \
729 True, True, True, True, 0))
731 for channel
in channels
:
732 iter = self
.append(channel_to_row(channel
, True))
733 self
.update_by_iter(iter)
735 def get_filter_path_from_url(self
, url
):
736 # Return the path of the filtered model for a given URL
737 child_path
= self
.get_path_from_url(url
)
738 if child_path
is None:
741 return self
._filter
.convert_child_path_to_path(child_path
)
743 def get_path_from_url(self
, url
):
744 # Return the tree model path for a given URL
749 if row
[self
.C_URL
] == url
:
753 def update_first_row(self
):
754 # Update the first row in the model (for "all episodes" updates)
755 self
.update_by_iter(self
.get_iter_first())
757 def update_by_urls(self
, urls
):
758 # Given a list of URLs, update each matching row
760 if row
[self
.C_URL
] in urls
:
761 self
.update_by_iter(row
.iter)
763 def iter_is_first_row(self
, iter):
764 iter = self
._filter
.convert_iter_to_child_iter(iter)
765 path
= self
.get_path(iter)
766 return (path
== (0,))
768 def update_by_filter_iter(self
, iter):
769 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter))
771 def update_all(self
):
773 self
.update_by_iter(row
.iter)
775 def update_by_iter(self
, iter):
776 # Given a GtkTreeIter, update volatile information
778 channel
= self
.get_value(iter, self
.C_CHANNEL
)
779 except TypeError, te
:
783 total
, deleted
, new
, downloaded
, unplayed
= channel
.get_statistics()
784 description
= self
._format
_description
(channel
, total
, deleted
, new
, \
785 downloaded
, unplayed
)
787 if gpodder
.ui
.fremantle
:
788 # We don't display the pill, so don't generate it
791 pill_image
= self
._get
_pill
_image
(channel
, downloaded
, unplayed
)
794 self
.C_TITLE
, channel
.title
, \
795 self
.C_DESCRIPTION
, description
, \
796 self
.C_ERROR
, self
._format
_error
(channel
), \
797 self
.C_PILL
, pill_image
, \
798 self
.C_PILL_VISIBLE
, pill_image
!= None, \
799 self
.C_VIEW_SHOW_UNDELETED
, total
- deleted
> 0, \
800 self
.C_VIEW_SHOW_DOWNLOADED
, downloaded
+ new
> 0, \
801 self
.C_VIEW_SHOW_UNPLAYED
, unplayed
+ new
> 0, \
802 self
.C_HAS_EPISODES
, total
> 0, \
803 self
.C_DOWNLOADS
, downloaded
)
805 def add_cover_by_channel(self
, channel
, pixbuf
):
806 # Resize and add the new cover image
807 pixbuf
= self
._resize
_pixbuf
(channel
.url
, pixbuf
)
808 if not channel
.feed_update_enabled
:
809 pixbuf
= self
._overlay
_pixbuf
(pixbuf
, self
.ICON_DISABLED
)
810 pixbuf
.saturate_and_pixelate(pixbuf
, 0.0, False)
813 if row
[self
.C_URL
] == channel
.url
:
814 row
[self
.C_COVER
] = pixbuf
817 def delete_cover_by_url(self
, url
):
818 # Remove the cover from the model
820 if row
[self
.C_URL
] == url
:
821 row
[self
.C_COVER
] = None
824 # Remove the cover from the cache
825 if url
in self
._cover
_cache
:
826 del self
._cover
_cache
[url
]