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
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 # Update progress (if we're currently being updated)
66 self
._update
_progress
= 0.
67 self
._last
_redraw
_progress
= 0.
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_update_progress(self
):
129 return self
._update
_progress
131 def reset_update_progress(self
):
132 self
._update
_progress
= 0.
134 def get_filtered_model(self
):
135 """Returns a filtered version of this episode model
137 The filtered version should be displayed in the UI,
138 as this model can have some filters set that should
139 be reflected in the UI.
143 def set_view_mode(self
, new_mode
):
144 """Sets a new view mode for this model
146 After setting the view mode, the filtered model
147 might be updated to reflect the new mode."""
148 if self
._view
_mode
!= new_mode
:
149 self
._view
_mode
= new_mode
150 self
._filter
.refilter()
152 def get_view_mode(self
):
153 """Returns the currently-set view mode"""
154 return self
._view
_mode
156 def set_search_term(self
, new_term
):
157 if self
._search
_term
!= new_term
:
158 self
._search
_term
= new_term
159 self
._filter
.refilter()
161 def get_search_term(self
):
162 return self
._search
_term
164 def _format_description(self
, episode
, include_description
=False, is_downloading
=None):
166 if episode
.state
!= gpodder
.STATE_DELETED
and not episode
.is_played
:
168 if include_description
and self
._all
_episodes
_view
:
169 return '%s%s%s\n<small>%s</small>' % (a
, xml
.sax
.saxutils
.escape(episode
.title
), b
,
170 _('from %s') % xml
.sax
.saxutils
.escape(episode
.channel
.title
))
171 elif include_description
:
172 return '%s%s%s\n<small>%s</small>' % (a
, xml
.sax
.saxutils
.escape(episode
.title
), b
,
173 xml
.sax
.saxutils
.escape(episode
.one_line_description()))
175 return xml
.sax
.saxutils
.escape(episode
.title
)
177 def replace_from_channel(self
, channel
, downloading
=None, \
178 include_description
=False, generate_thumbnails
=False, \
181 Add episode from the given channel to this model.
182 Downloading should be a callback.
183 include_description should be a boolean value (True if description
184 is to be added to the episode row, or False if not)
187 # Remove old episodes in the list store
190 self
._update
_progress
= 0.
191 self
._last
_redraw
_progress
= 0.
192 if treeview
is not None:
193 util
.idle_add(treeview
.queue_draw
)
195 self
._all
_episodes
_view
= getattr(channel
, 'ALL_EPISODES_PROXY', False)
197 episodes
= channel
.get_all_episodes()
198 if not isinstance(episodes
, list):
199 episodes
= list(episodes
)
200 count
= len(episodes
)
202 for position
, episode
in enumerate(episodes
):
203 iter = self
.append((episode
.url
, \
205 self
._format
_filesize
(episode
), \
208 episode
.cute_pubdate(), \
216 episode
.get_play_info_string(), \
217 episode
.total_time
and not episode
.current_position
, \
218 episode
.total_time
and episode
.current_position
, \
221 self
.update_by_iter(iter, downloading
, include_description
, \
222 generate_thumbnails
, reload_from_db
=False)
224 self
._update
_progress
= float(position
+1)/count
225 if treeview
is not None and \
226 (self
._update
_progress
> self
._last
_redraw
_progress
+ self
._UI
_UPDATE
_STEP
or position
+1 == count
):
227 def in_gtk_main_thread():
228 treeview
.queue_draw()
229 while gtk
.events_pending():
230 gtk
.main_iteration(False)
231 util
.idle_add(in_gtk_main_thread
)
232 self
._last
_redraw
_progress
= self
._update
_progress
234 def update_all(self
, downloading
=None, include_description
=False, \
235 generate_thumbnails
=False):
237 self
.update_by_iter(row
.iter, downloading
, include_description
, \
240 def update_by_urls(self
, urls
, downloading
=None, include_description
=False, \
241 generate_thumbnails
=False):
243 if row
[self
.C_URL
] in urls
:
244 self
.update_by_iter(row
.iter, downloading
, include_description
, \
247 def update_by_filter_iter(self
, iter, downloading
=None, \
248 include_description
=False, generate_thumbnails
=False):
249 # Convenience function for use by "outside" methods that use iters
250 # from the filtered episode list model (i.e. all UI things normally)
251 iter = self
._sorter
.convert_iter_to_child_iter(None, iter)
252 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter), \
253 downloading
, include_description
, generate_thumbnails
)
255 def update_by_iter(self
, iter, downloading
=None, include_description
=False, \
256 generate_thumbnails
=False, reload_from_db
=True):
257 episode
= self
.get_value(iter, self
.C_EPISODE
)
259 episode
.reload_from_db()
261 if include_description
or gpodder
.ui
.maemo
:
270 status_icon_to_build_from_file
= False
272 view_show_undeleted
= True
273 view_show_downloaded
= False
274 view_show_unplayed
= False
275 icon_theme
= gtk
.icon_theme_get_default()
277 if downloading
is not None and downloading(episode
):
278 tooltip
.append(_('Downloading'))
279 status_icon
= self
.ICON_DOWNLOADING
280 view_show_downloaded
= True
281 view_show_unplayed
= True
283 if episode
.state
== gpodder
.STATE_DELETED
:
284 tooltip
.append(_('Deleted'))
285 status_icon
= self
.ICON_DELETED
286 view_show_undeleted
= False
287 elif episode
.state
== gpodder
.STATE_NORMAL
and \
288 not episode
.is_played
:
289 tooltip
.append(_('New episode'))
290 status_icon
= self
.ICON_NEW
291 view_show_downloaded
= True
292 view_show_unplayed
= True
293 elif episode
.state
== gpodder
.STATE_DOWNLOADED
:
295 view_show_downloaded
= True
296 view_show_unplayed
= not episode
.is_played
297 show_bullet
= not episode
.is_played
298 show_padlock
= episode
.is_locked
299 show_missing
= not episode
.file_exists()
300 filename
= episode
.local_filename(create
=False, check_only
=True)
302 file_type
= episode
.file_type()
303 if file_type
== 'audio':
304 tooltip
.append(_('Downloaded episode'))
305 status_icon
= self
.ICON_AUDIO_FILE
306 elif file_type
== 'video':
307 tooltip
.append(_('Downloaded video episode'))
308 status_icon
= self
.ICON_VIDEO_FILE
309 elif file_type
== 'image':
310 tooltip
.append(_('Downloaded image'))
311 status_icon
= self
.ICON_IMAGE_FILE
313 # Optional thumbnailing for image downloads
314 if generate_thumbnails
:
315 if filename
is not None:
316 # set the status icon to the path itself (that
317 # should be a good identifier anyway)
318 status_icon
= filename
319 status_icon_to_build_from_file
= True
321 tooltip
.append(_('Downloaded file'))
322 status_icon
= self
.ICON_GENERIC_FILE
324 # Try to find a themed icon for this file
325 if filename
is not None and have_gio
:
326 file = gio
.File(filename
)
327 if file.query_exists():
328 file_info
= file.query_info('*')
329 icon
= file_info
.get_icon()
330 for icon_name
in icon
.get_names():
331 if icon_theme
.has_icon(icon_name
):
332 status_icon
= icon_name
336 tooltip
.append(_('missing file'))
339 if file_type
== 'image':
340 tooltip
.append(_('never displayed'))
341 elif file_type
in ('audio', 'video'):
342 tooltip
.append(_('never played'))
344 tooltip
.append(_('never opened'))
346 if file_type
== 'image':
347 tooltip
.append(_('displayed'))
348 elif file_type
in ('audio', 'video'):
349 tooltip
.append(_('played'))
351 tooltip
.append(_('opened'))
353 tooltip
.append(_('deletion prevented'))
355 if episode
.total_time
> 0 and episode
.current_position
:
356 tooltip
.append('%d%%' % (100.*float(episode
.current_position
)/float(episode
.total_time
),))
358 if episode
.total_time
:
359 total_time
= util
.format_time(episode
.total_time
)
361 tooltip
.append(total_time
)
363 tooltip
= ', '.join(tooltip
)
365 description
= self
._format
_description
(episode
, include_description
, downloading
)
367 self
.C_STATUS_ICON
, status_icon
, \
368 self
.C_VIEW_SHOW_UNDELETED
, view_show_undeleted
, \
369 self
.C_VIEW_SHOW_DOWNLOADED
, view_show_downloaded
, \
370 self
.C_VIEW_SHOW_UNPLAYED
, view_show_unplayed
, \
371 self
.C_DESCRIPTION
, description
, \
372 self
.C_TOOLTIP
, tooltip
, \
373 self
.C_TIME
, episode
.get_play_info_string(), \
374 self
.C_TIME1_VISIBLE
, episode
.total_time
and not episode
.current_position
, \
375 self
.C_TIME2_VISIBLE
, episode
.total_time
and episode
.current_position
, \
376 self
.C_LOCKED
, episode
.is_locked
)
378 def _get_icon_from_image(self
,image_path
, icon_size
):
380 Load an local image file and transform it into an icon.
382 Return a pixbuf scaled to the desired size and may return None
383 if the icon creation is impossible (file not found etc).
385 if not os
.path
.exists(image_path
):
387 # load image from disc (code adapted from CoverDownloader
388 # except that no download is needed here)
389 loader
= gtk
.gdk
.PixbufLoader()
392 loader
.write(open(image_path
, 'rb').read())
394 pixbuf
= loader
.get_pixbuf()
396 log('Data error while loading image %s', image_path
, sender
=self
)
398 # Now scale the image with ratio (copied from _resize_pixbuf_keep_ratio)
400 if pixbuf
.get_width() > icon_size
:
401 f
= float(icon_size
)/pixbuf
.get_width()
402 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
403 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
405 if pixbuf
.get_height() > icon_size
:
406 f
= float(icon_size
)/pixbuf
.get_height()
407 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
408 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
412 def _get_tree_icon(self
, icon_name
, add_bullet
=False, \
413 add_padlock
=False, add_missing
=False, icon_size
=32, \
414 build_icon_from_file
= False):
416 Loads an icon from the current icon theme at the specified
417 size, suitable for display in a gtk.TreeView. Additional
418 emblems can be added on top of the icon.
420 Caching is used to speed up the icon lookup.
422 The `build_icon_from_file` argument indicates (when True) that
423 the icon has to be created on the fly from a given image
424 file. The `icon_name` argument is then interpreted as the path
425 to this file. Those specific icons will *not be cached*.
428 # Add all variables that modify the appearance of the icon, so
429 # our cache does not return the same icons for different requests
430 cache_id
= (icon_name
, add_bullet
, add_padlock
, add_missing
, icon_size
)
432 if cache_id
in self
._icon
_cache
:
433 return self
._icon
_cache
[cache_id
]
435 icon_theme
= gtk
.icon_theme_get_default()
438 if build_icon_from_file
:
439 icon
= self
._get
_icon
_from
_image
(icon_name
,icon_size
)
441 icon
= icon_theme
.load_icon(icon_name
, icon_size
, 0)
444 log('Missing icon in theme: %s', icon_name
, sender
=self
)
445 icon
= icon_theme
.load_icon(gtk
.STOCK_DIALOG_QUESTION
, \
448 log('Please install the GNOME icon theme.', sender
=self
)
449 icon
= gtk
.gdk
.Pixbuf(gtk
.gdk
.COLORSPACE_RGB
, \
450 True, 8, icon_size
, icon_size
)
452 if icon
and (add_bullet
or add_padlock
or add_missing
):
453 # We'll modify the icon, so use .copy()
457 # Desaturate the icon so it looks even more "missing"
458 icon
.saturate_and_pixelate(icon
, 0.0, False)
459 emblem
= icon_theme
.load_icon(self
.ICON_MISSING
, icon_size
/2, 0)
460 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
461 xpos
= icon
.get_width() - width
462 ypos
= icon
.get_height() - height
463 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
469 emblem
= icon_theme
.load_icon(self
.ICON_UNPLAYED
, icon_size
/2, 0)
470 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
471 xpos
= icon
.get_width() - width
472 ypos
= icon
.get_height() - height
473 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
479 emblem
= icon_theme
.load_icon(self
.ICON_LOCKED
, icon_size
/2, 0)
480 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
481 emblem
.composite(icon
, 0, 0, width
, height
, 0, 0, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
485 self
._icon
_cache
[cache_id
] = icon
489 class PodcastChannelProxy(object):
490 ALL_EPISODES_PROXY
= True
492 def __init__(self
, db
, config
, channels
):
494 self
._config
= config
495 self
.channels
= channels
496 self
.title
= _('All episodes')
497 self
.description
= _('from all podcasts')
498 self
.parse_error
= ''
501 self
._save
_dir
_size
_set
= False
502 self
.save_dir_size
= 0L
503 self
.cover_file
= os
.path
.join(gpodder
.images_folder
, 'podcast-all.png')
504 self
.feed_update_enabled
= True
506 def __getattribute__(self
, name
):
508 return object.__getattribute
__(self
, name
)
509 except AttributeError:
510 log('Unsupported method call (%s)', name
, sender
=self
)
512 def get_statistics(self
):
513 # Get the total statistics for all channels from the database
514 return self
._db
.get_total_count()
516 def get_all_episodes(self
):
517 """Returns a generator that yields every episode"""
518 channel_lookup_map
= dict((c
.id, c
) for c
in self
.channels
)
519 return self
._db
.load_all_episodes(channel_lookup_map
)
521 def request_save_dir_size(self
):
522 if not self
._save
_dir
_size
_set
:
523 self
.update_save_dir_size()
524 self
._save
_dir
_size
_set
= True
526 def update_save_dir_size(self
):
527 self
.save_dir_size
= util
.calculate_size(self
._config
.download_dir
)
530 class PodcastListModel(gtk
.ListStore
):
531 C_URL
, C_TITLE
, C_DESCRIPTION
, C_PILL
, C_CHANNEL
, \
532 C_COVER
, C_ERROR
, C_PILL_VISIBLE
, \
533 C_VIEW_SHOW_UNDELETED
, C_VIEW_SHOW_DOWNLOADED
, \
534 C_VIEW_SHOW_UNPLAYED
, C_HAS_EPISODES
, C_SEPARATOR
, \
535 C_DOWNLOADS
= range(14)
537 SEARCH_COLUMNS
= (C_TITLE
, C_DESCRIPTION
)
540 def row_separator_func(cls
, model
, iter):
541 return model
.get_value(iter, cls
.C_SEPARATOR
)
543 def __init__(self
, cover_downloader
):
544 gtk
.ListStore
.__init
__(self
, str, str, str, gtk
.gdk
.Pixbuf
, \
545 object, gtk
.gdk
.Pixbuf
, str, bool, bool, bool, bool, \
548 # Filter to allow hiding some episodes
549 self
._filter
= self
.filter_new()
551 self
._search
_term
= None
552 self
._filter
.set_visible_func(self
._filter
_visible
_func
)
554 self
._cover
_cache
= {}
555 if gpodder
.ui
.fremantle
:
556 self
._max
_image
_side
= 64
558 self
._max
_image
_side
= 40
559 self
._cover
_downloader
= cover_downloader
561 # "ICON" is used to mark icon names in source files
564 #self.ICON_DISABLED = ICON('emblem-unreadable')
565 self
.ICON_DISABLED
= ICON('gtk-media-pause')
567 def _filter_visible_func(self
, model
, iter):
568 # If searching is active, set visibility based on search text
569 if self
._search
_term
is not None:
570 key
= self
._search
_term
.lower()
571 columns
= (model
.get_value(iter, c
) for c
in self
.SEARCH_COLUMNS
)
572 return any((key
in c
.lower() for c
in columns
if c
is not None))
574 if model
.get_value(iter, self
.C_SEPARATOR
):
576 if self
._view
_mode
== EpisodeListModel
.VIEW_ALL
:
577 return model
.get_value(iter, self
.C_HAS_EPISODES
)
578 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNDELETED
:
579 return model
.get_value(iter, self
.C_VIEW_SHOW_UNDELETED
)
580 elif self
._view
_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
581 return model
.get_value(iter, self
.C_VIEW_SHOW_DOWNLOADED
)
582 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
583 return model
.get_value(iter, self
.C_VIEW_SHOW_UNPLAYED
)
587 def get_filtered_model(self
):
588 """Returns a filtered version of this episode model
590 The filtered version should be displayed in the UI,
591 as this model can have some filters set that should
592 be reflected in the UI.
596 def set_view_mode(self
, new_mode
):
597 """Sets a new view mode for this model
599 After setting the view mode, the filtered model
600 might be updated to reflect the new mode."""
601 if self
._view
_mode
!= new_mode
:
602 self
._view
_mode
= new_mode
603 self
._filter
.refilter()
605 def get_view_mode(self
):
606 """Returns the currently-set view mode"""
607 return self
._view
_mode
609 def set_search_term(self
, new_term
):
610 if self
._search
_term
!= new_term
:
611 self
._search
_term
= new_term
612 self
._filter
.refilter()
614 def get_search_term(self
):
615 return self
._search
_term
617 def enable_separators(self
, channeltree
):
618 channeltree
.set_row_separator_func(self
._show
_row
_separator
)
620 def _show_row_separator(self
, model
, iter):
621 return model
.get_value(iter, self
.C_SEPARATOR
)
623 def _resize_pixbuf_keep_ratio(self
, url
, pixbuf
):
625 Resizes a GTK Pixbuf but keeps its aspect ratio.
626 Returns None if the pixbuf does not need to be
627 resized or the newly resized pixbuf if it does.
632 if url
in self
._cover
_cache
:
633 return self
._cover
_cache
[url
]
636 if pixbuf
.get_width() > self
._max
_image
_side
:
637 f
= float(self
._max
_image
_side
)/pixbuf
.get_width()
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 if pixbuf
.get_height() > self
._max
_image
_side
:
644 f
= float(self
._max
_image
_side
)/pixbuf
.get_height()
645 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
646 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
650 self
._cover
_cache
[url
] = pixbuf
655 def _resize_pixbuf(self
, url
, pixbuf
):
659 return self
._resize
_pixbuf
_keep
_ratio
(url
, pixbuf
) or pixbuf
661 def _overlay_pixbuf(self
, pixbuf
, icon
):
663 icon_theme
= gtk
.icon_theme_get_default()
664 emblem
= icon_theme
.load_icon(icon
, self
._max
_image
_side
/2, 0)
665 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
666 xpos
= pixbuf
.get_width() - width
667 ypos
= pixbuf
.get_height() - height
669 # need to resize overlay for none standard icon size
670 emblem
= icon_theme
.load_icon(icon
, pixbuf
.get_height() - 1, 0)
671 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
672 xpos
= pixbuf
.get_width() - width
673 ypos
= pixbuf
.get_height() - height
674 emblem
.composite(pixbuf
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
680 def _get_cover_image(self
, channel
, add_overlay
=False):
681 if self
._cover
_downloader
is None:
684 pixbuf
= self
._cover
_downloader
.get_cover(channel
, avoid_downloading
=True)
685 pixbuf_overlay
= self
._resize
_pixbuf
(channel
.url
, pixbuf
)
686 if add_overlay
and not channel
.feed_update_enabled
:
687 pixbuf_overlay
= self
._overlay
_pixbuf
(pixbuf_overlay
, self
.ICON_DISABLED
)
688 pixbuf_overlay
.saturate_and_pixelate(pixbuf_overlay
, 0.0, False)
690 return pixbuf_overlay
692 def _get_pill_image(self
, channel
, count_downloaded
, count_unplayed
):
693 if count_unplayed
> 0 or count_downloaded
> 0:
694 return draw
.draw_pill_pixbuf(str(count_unplayed
), str(count_downloaded
))
698 def _format_description(self
, channel
, total
, deleted
, \
699 new
, downloaded
, unplayed
):
700 title_markup
= xml
.sax
.saxutils
.escape(channel
.title
)
701 if channel
.feed_update_enabled
:
702 description_markup
= xml
.sax
.saxutils
.escape(util
.get_first_line(channel
.description
) or ' ')
704 description_markup
= xml
.sax
.saxutils
.escape(_('Subscription paused.'))
707 d
.append('<span weight="bold">')
708 d
.append(title_markup
)
711 return ''.join(d
+['\n', '<small>', description_markup
, '</small>'])
713 def _format_error(self
, channel
):
714 if channel
.parse_error
:
715 return str(channel
.parse_error
)
719 def set_channels(self
, db
, config
, channels
):
720 # Clear the model and update the list of podcasts
723 def channel_to_row(channel
, add_overlay
=False):
724 return (channel
.url
, '', '', None, channel
, \
725 self
._get
_cover
_image
(channel
, add_overlay
), '', True, True, True, \
726 True, True, False, 0)
728 if config
.podcast_list_view_all
and channels
:
729 all_episodes
= PodcastChannelProxy(db
, config
, channels
)
730 iter = self
.append(channel_to_row(all_episodes
))
731 self
.update_by_iter(iter)
734 self
.append(('', '', '', None, None, None, '', True, True, \
735 True, True, True, True, 0))
737 for channel
in channels
:
738 iter = self
.append(channel_to_row(channel
, True))
739 self
.update_by_iter(iter)
741 def get_filter_path_from_url(self
, url
):
742 # Return the path of the filtered model for a given URL
743 child_path
= self
.get_path_from_url(url
)
744 if child_path
is None:
747 return self
._filter
.convert_child_path_to_path(child_path
)
749 def get_path_from_url(self
, url
):
750 # Return the tree model path for a given URL
755 if row
[self
.C_URL
] == url
:
759 def update_first_row(self
):
760 # Update the first row in the model (for "all episodes" updates)
761 self
.update_by_iter(self
.get_iter_first())
763 def update_by_urls(self
, urls
):
764 # Given a list of URLs, update each matching row
766 if row
[self
.C_URL
] in urls
:
767 self
.update_by_iter(row
.iter)
769 def iter_is_first_row(self
, iter):
770 iter = self
._filter
.convert_iter_to_child_iter(iter)
771 path
= self
.get_path(iter)
772 return (path
== (0,))
774 def update_by_filter_iter(self
, iter):
775 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter))
777 def update_all(self
):
779 self
.update_by_iter(row
.iter)
781 def update_by_iter(self
, iter):
782 # Given a GtkTreeIter, update volatile information
784 channel
= self
.get_value(iter, self
.C_CHANNEL
)
785 except TypeError, te
:
789 total
, deleted
, new
, downloaded
, unplayed
= channel
.get_statistics()
790 description
= self
._format
_description
(channel
, total
, deleted
, new
, \
791 downloaded
, unplayed
)
793 if gpodder
.ui
.fremantle
:
794 # We don't display the pill, so don't generate it
797 pill_image
= self
._get
_pill
_image
(channel
, downloaded
, unplayed
)
800 self
.C_TITLE
, channel
.title
, \
801 self
.C_DESCRIPTION
, description
, \
802 self
.C_ERROR
, self
._format
_error
(channel
), \
803 self
.C_PILL
, pill_image
, \
804 self
.C_PILL_VISIBLE
, pill_image
!= None, \
805 self
.C_VIEW_SHOW_UNDELETED
, total
- deleted
> 0, \
806 self
.C_VIEW_SHOW_DOWNLOADED
, downloaded
+ new
> 0, \
807 self
.C_VIEW_SHOW_UNPLAYED
, unplayed
+ new
> 0, \
808 self
.C_HAS_EPISODES
, total
> 0, \
809 self
.C_DOWNLOADS
, downloaded
)
811 def add_cover_by_channel(self
, channel
, pixbuf
):
812 # Resize and add the new cover image
813 pixbuf
= self
._resize
_pixbuf
(channel
.url
, pixbuf
)
814 if not channel
.feed_update_enabled
:
815 pixbuf
= self
._overlay
_pixbuf
(pixbuf
, self
.ICON_DISABLED
)
816 pixbuf
.saturate_and_pixelate(pixbuf
, 0.0, False)
819 if row
[self
.C_URL
] == channel
.url
:
820 row
[self
.C_COVER
] = pixbuf
823 def delete_cover_by_url(self
, url
):
824 # Remove the cover from the model
826 if row
[self
.C_URL
] == url
:
827 row
[self
.C_COVER
] = None
830 # Remove the cover from the cache
831 if url
in self
._cover
_cache
:
832 del self
._cover
_cache
[url
]