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_TIME_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 ''.join((a
, xml
.sax
.saxutils
.escape(episode
.title
), b
))
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_TIME_VISIBLE
, episode
.total_time
, \
369 self
.C_LOCKED
, episode
.is_locked
)
371 def _get_icon_from_image(self
,image_path
, icon_size
):
373 Load an local image file and transform it into an icon.
375 Return a pixbuf scaled to the desired size and may return None
376 if the icon creation is impossible (file not found etc).
378 if not os
.path
.exists(image_path
):
380 # load image from disc (code adapted from CoverDownloader
381 # except that no download is needed here)
382 loader
= gtk
.gdk
.PixbufLoader()
385 loader
.write(open(image_path
, 'rb').read())
387 pixbuf
= loader
.get_pixbuf()
389 log('Data error while loading image %s', image_path
, sender
=self
)
391 # Now scale the image with ratio (copied from _resize_pixbuf_keep_ratio)
393 if pixbuf
.get_width() > icon_size
:
394 f
= float(icon_size
)/pixbuf
.get_width()
395 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
396 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
398 if pixbuf
.get_height() > icon_size
:
399 f
= float(icon_size
)/pixbuf
.get_height()
400 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
401 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
405 def _get_tree_icon(self
, icon_name
, add_bullet
=False, \
406 add_padlock
=False, add_missing
=False, icon_size
=32, \
407 build_icon_from_file
= False):
409 Loads an icon from the current icon theme at the specified
410 size, suitable for display in a gtk.TreeView. Additional
411 emblems can be added on top of the icon.
413 Caching is used to speed up the icon lookup.
415 The `build_icon_from_file` argument indicates (when True) that
416 the icon has to be created on the fly from a given image
417 file. The `icon_name` argument is then interpreted as the path
418 to this file. Those specific icons will *not be cached*.
421 # Add all variables that modify the appearance of the icon, so
422 # our cache does not return the same icons for different requests
423 cache_id
= (icon_name
, add_bullet
, add_padlock
, add_missing
, icon_size
)
425 if cache_id
in self
._icon
_cache
:
426 return self
._icon
_cache
[cache_id
]
428 icon_theme
= gtk
.icon_theme_get_default()
431 if build_icon_from_file
:
432 icon
= self
._get
_icon
_from
_image
(icon_name
,icon_size
)
434 icon
= icon_theme
.load_icon(icon_name
, icon_size
, 0)
437 log('Missing icon in theme: %s', icon_name
, sender
=self
)
438 icon
= icon_theme
.load_icon(gtk
.STOCK_DIALOG_QUESTION
, \
441 log('Please install the GNOME icon theme.', sender
=self
)
442 icon
= gtk
.gdk
.Pixbuf(gtk
.gdk
.COLORSPACE_RGB
, \
443 True, 8, icon_size
, icon_size
)
445 if icon
and (add_bullet
or add_padlock
or add_missing
):
446 # We'll modify the icon, so use .copy()
450 # Desaturate the icon so it looks even more "missing"
451 icon
.saturate_and_pixelate(icon
, 0.0, False)
452 emblem
= icon_theme
.load_icon(self
.ICON_MISSING
, icon_size
/2, 0)
453 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
454 xpos
= icon
.get_width() - width
455 ypos
= icon
.get_height() - height
456 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
462 emblem
= icon_theme
.load_icon(self
.ICON_UNPLAYED
, icon_size
/2, 0)
463 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
464 xpos
= icon
.get_width() - width
465 ypos
= icon
.get_height() - height
466 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
472 emblem
= icon_theme
.load_icon(self
.ICON_LOCKED
, icon_size
/2, 0)
473 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
474 emblem
.composite(icon
, 0, 0, width
, height
, 0, 0, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
478 self
._icon
_cache
[cache_id
] = icon
482 class PodcastChannelProxy(object):
483 ALL_EPISODES_PROXY
= True
485 def __init__(self
, db
, config
, channels
):
487 self
._config
= config
488 self
.channels
= channels
489 self
.title
= _('All episodes')
490 self
.description
= _('from all podcasts')
491 self
.parse_error
= ''
494 self
._save
_dir
_size
_set
= False
495 self
.save_dir_size
= 0L
496 self
.cover_file
= os
.path
.join(gpodder
.images_folder
, 'podcast-all.png')
497 self
.feed_update_enabled
= True
499 def __getattribute__(self
, name
):
501 return object.__getattribute
__(self
, name
)
502 except AttributeError:
503 log('Unsupported method call (%s)', name
, sender
=self
)
505 def get_statistics(self
):
506 # Get the total statistics for all channels from the database
507 return self
._db
.get_total_count()
509 def get_all_episodes(self
):
510 """Returns a generator that yields every episode"""
511 channel_lookup_map
= dict((c
.id, c
) for c
in self
.channels
)
512 return self
._db
.load_all_episodes(channel_lookup_map
)
514 def request_save_dir_size(self
):
515 if not self
._save
_dir
_size
_set
:
516 self
.update_save_dir_size()
517 self
._save
_dir
_size
_set
= True
519 def update_save_dir_size(self
):
520 self
.save_dir_size
= util
.calculate_size(self
._config
.download_dir
)
523 class PodcastListModel(gtk
.ListStore
):
524 C_URL
, C_TITLE
, C_DESCRIPTION
, C_PILL
, C_CHANNEL
, \
525 C_COVER
, C_ERROR
, C_PILL_VISIBLE
, \
526 C_VIEW_SHOW_UNDELETED
, C_VIEW_SHOW_DOWNLOADED
, \
527 C_VIEW_SHOW_UNPLAYED
, C_HAS_EPISODES
, C_SEPARATOR
, \
528 C_DOWNLOADS
= range(14)
530 SEARCH_COLUMNS
= (C_TITLE
, C_DESCRIPTION
)
533 def row_separator_func(cls
, model
, iter):
534 return model
.get_value(iter, cls
.C_SEPARATOR
)
536 def __init__(self
, cover_downloader
):
537 gtk
.ListStore
.__init
__(self
, str, str, str, gtk
.gdk
.Pixbuf
, \
538 object, gtk
.gdk
.Pixbuf
, str, bool, bool, bool, bool, \
541 # Filter to allow hiding some episodes
542 self
._filter
= self
.filter_new()
544 self
._search
_term
= None
545 self
._filter
.set_visible_func(self
._filter
_visible
_func
)
547 self
._cover
_cache
= {}
548 if gpodder
.ui
.fremantle
:
549 self
._max
_image
_side
= 64
551 self
._max
_image
_side
= 40
552 self
._cover
_downloader
= cover_downloader
554 # "ICON" is used to mark icon names in source files
557 #self.ICON_DISABLED = ICON('emblem-unreadable')
558 self
.ICON_DISABLED
= ICON('gtk-media-pause')
560 def _filter_visible_func(self
, model
, iter):
561 # If searching is active, set visibility based on search text
562 if self
._search
_term
is not None:
563 key
= self
._search
_term
.lower()
564 columns
= (model
.get_value(iter, c
) for c
in self
.SEARCH_COLUMNS
)
565 return any((key
in c
.lower() for c
in columns
if c
is not None))
567 if model
.get_value(iter, self
.C_SEPARATOR
):
569 if self
._view
_mode
== EpisodeListModel
.VIEW_ALL
:
570 return model
.get_value(iter, self
.C_HAS_EPISODES
)
571 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNDELETED
:
572 return model
.get_value(iter, self
.C_VIEW_SHOW_UNDELETED
)
573 elif self
._view
_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
574 return model
.get_value(iter, self
.C_VIEW_SHOW_DOWNLOADED
)
575 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
576 return model
.get_value(iter, self
.C_VIEW_SHOW_UNPLAYED
)
580 def get_filtered_model(self
):
581 """Returns a filtered version of this episode model
583 The filtered version should be displayed in the UI,
584 as this model can have some filters set that should
585 be reflected in the UI.
589 def set_view_mode(self
, new_mode
):
590 """Sets a new view mode for this model
592 After setting the view mode, the filtered model
593 might be updated to reflect the new mode."""
594 if self
._view
_mode
!= new_mode
:
595 self
._view
_mode
= new_mode
596 self
._filter
.refilter()
598 def get_view_mode(self
):
599 """Returns the currently-set view mode"""
600 return self
._view
_mode
602 def set_search_term(self
, new_term
):
603 if self
._search
_term
!= new_term
:
604 self
._search
_term
= new_term
605 self
._filter
.refilter()
607 def get_search_term(self
):
608 return self
._search
_term
610 def enable_separators(self
, channeltree
):
611 channeltree
.set_row_separator_func(self
._show
_row
_separator
)
613 def _show_row_separator(self
, model
, iter):
614 return model
.get_value(iter, self
.C_SEPARATOR
)
616 def _resize_pixbuf_keep_ratio(self
, url
, pixbuf
):
618 Resizes a GTK Pixbuf but keeps its aspect ratio.
619 Returns None if the pixbuf does not need to be
620 resized or the newly resized pixbuf if it does.
625 if url
in self
._cover
_cache
:
626 return self
._cover
_cache
[url
]
629 if pixbuf
.get_width() > self
._max
_image
_side
:
630 f
= float(self
._max
_image
_side
)/pixbuf
.get_width()
631 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
632 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
636 if pixbuf
.get_height() > self
._max
_image
_side
:
637 f
= float(self
._max
_image
_side
)/pixbuf
.get_height()
638 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
639 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
643 self
._cover
_cache
[url
] = pixbuf
648 def _resize_pixbuf(self
, url
, pixbuf
):
652 return self
._resize
_pixbuf
_keep
_ratio
(url
, pixbuf
) or pixbuf
654 def _overlay_pixbuf(self
, pixbuf
, icon
):
656 icon_theme
= gtk
.icon_theme_get_default()
657 emblem
= icon_theme
.load_icon(icon
, self
._max
_image
_side
/2, 0)
658 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
659 xpos
= pixbuf
.get_width() - width
660 ypos
= pixbuf
.get_height() - height
662 # need to resize overlay for none standard icon size
663 emblem
= icon_theme
.load_icon(icon
, pixbuf
.get_height() - 1, 0)
664 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
665 xpos
= pixbuf
.get_width() - width
666 ypos
= pixbuf
.get_height() - height
667 emblem
.composite(pixbuf
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
673 def _get_cover_image(self
, channel
, add_overlay
=False):
674 if self
._cover
_downloader
is None:
677 pixbuf
= self
._cover
_downloader
.get_cover(channel
, avoid_downloading
=True)
678 pixbuf_overlay
= self
._resize
_pixbuf
(channel
.url
, pixbuf
)
679 if add_overlay
and not channel
.feed_update_enabled
:
680 pixbuf_overlay
= self
._overlay
_pixbuf
(pixbuf_overlay
, self
.ICON_DISABLED
)
681 pixbuf_overlay
.saturate_and_pixelate(pixbuf_overlay
, 0.0, False)
683 return pixbuf_overlay
685 def _get_pill_image(self
, channel
, count_downloaded
, count_unplayed
):
686 if count_unplayed
> 0 or count_downloaded
> 0:
687 return draw
.draw_pill_pixbuf(str(count_unplayed
), str(count_downloaded
))
691 def _format_description(self
, channel
, total
, deleted
, \
692 new
, downloaded
, unplayed
):
693 title_markup
= xml
.sax
.saxutils
.escape(channel
.title
)
694 if channel
.feed_update_enabled
:
695 description_markup
= xml
.sax
.saxutils
.escape(util
.get_first_line(channel
.description
) or ' ')
697 description_markup
= xml
.sax
.saxutils
.escape(_('Subscription paused'))
700 d
.append('<span weight="bold">')
701 d
.append(title_markup
)
704 return ''.join(d
+['\n', '<small>', description_markup
, '</small>'])
706 def _format_error(self
, channel
):
707 if channel
.parse_error
:
708 return str(channel
.parse_error
)
712 def set_channels(self
, db
, config
, channels
):
713 # Clear the model and update the list of podcasts
716 def channel_to_row(channel
, add_overlay
=False):
717 return (channel
.url
, '', '', None, channel
, \
718 self
._get
_cover
_image
(channel
, add_overlay
), '', True, True, True, \
719 True, True, False, 0)
721 if config
.podcast_list_view_all
and channels
:
722 all_episodes
= PodcastChannelProxy(db
, config
, channels
)
723 iter = self
.append(channel_to_row(all_episodes
))
724 self
.update_by_iter(iter)
727 self
.append(('', '', '', None, None, None, '', True, True, \
728 True, True, True, True, 0))
730 for channel
in channels
:
731 iter = self
.append(channel_to_row(channel
, True))
732 self
.update_by_iter(iter)
734 def get_filter_path_from_url(self
, url
):
735 # Return the path of the filtered model for a given URL
736 child_path
= self
.get_path_from_url(url
)
737 if child_path
is None:
740 return self
._filter
.convert_child_path_to_path(child_path
)
742 def get_path_from_url(self
, url
):
743 # Return the tree model path for a given URL
748 if row
[self
.C_URL
] == url
:
752 def update_first_row(self
):
753 # Update the first row in the model (for "all episodes" updates)
754 self
.update_by_iter(self
.get_iter_first())
756 def update_by_urls(self
, urls
):
757 # Given a list of URLs, update each matching row
759 if row
[self
.C_URL
] in urls
:
760 self
.update_by_iter(row
.iter)
762 def iter_is_first_row(self
, iter):
763 iter = self
._filter
.convert_iter_to_child_iter(iter)
764 path
= self
.get_path(iter)
765 return (path
== (0,))
767 def update_by_filter_iter(self
, iter):
768 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter))
770 def update_all(self
):
772 self
.update_by_iter(row
.iter)
774 def update_by_iter(self
, iter):
775 # Given a GtkTreeIter, update volatile information
777 channel
= self
.get_value(iter, self
.C_CHANNEL
)
778 except TypeError, te
:
782 total
, deleted
, new
, downloaded
, unplayed
= channel
.get_statistics()
783 description
= self
._format
_description
(channel
, total
, deleted
, new
, \
784 downloaded
, unplayed
)
786 if gpodder
.ui
.fremantle
:
787 # We don't display the pill, so don't generate it
790 pill_image
= self
._get
_pill
_image
(channel
, downloaded
, unplayed
)
793 self
.C_TITLE
, channel
.title
, \
794 self
.C_DESCRIPTION
, description
, \
795 self
.C_ERROR
, self
._format
_error
(channel
), \
796 self
.C_PILL
, pill_image
, \
797 self
.C_PILL_VISIBLE
, pill_image
!= None, \
798 self
.C_VIEW_SHOW_UNDELETED
, total
- deleted
> 0, \
799 self
.C_VIEW_SHOW_DOWNLOADED
, downloaded
+ new
> 0, \
800 self
.C_VIEW_SHOW_UNPLAYED
, unplayed
+ new
> 0, \
801 self
.C_HAS_EPISODES
, total
> 0, \
802 self
.C_DOWNLOADS
, downloaded
)
804 def add_cover_by_channel(self
, channel
, pixbuf
):
805 # Resize and add the new cover image
806 pixbuf
= self
._resize
_pixbuf
(channel
.url
, pixbuf
)
807 if not channel
.feed_update_enabled
:
808 pixbuf
= self
._overlay
_pixbuf
(pixbuf
, self
.ICON_DISABLED
)
809 pixbuf
.saturate_and_pixelate(pixbuf
, 0.0, False)
812 if row
[self
.C_URL
] == channel
.url
:
813 row
[self
.C_COVER
] = pixbuf
816 def delete_cover_by_url(self
, url
):
817 # Remove the cover from the model
819 if row
[self
.C_URL
] == url
:
820 row
[self
.C_COVER
] = None
823 # Remove the cover from the cache
824 if url
in self
._cover
_cache
:
825 del self
._cover
_cache
[url
]