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
import model
32 from gpodder
.liblogger
import log
34 from gpodder
.gtkui
import draw
38 import xml
.sax
.saxutils
46 class EpisodeListModel(gtk
.ListStore
):
47 C_URL
, C_TITLE
, C_FILESIZE_TEXT
, C_EPISODE
, C_STATUS_ICON
, \
48 C_PUBLISHED_TEXT
, C_DESCRIPTION
, C_TOOLTIP
, \
49 C_VIEW_SHOW_UNDELETED
, C_VIEW_SHOW_DOWNLOADED
, \
50 C_VIEW_SHOW_UNPLAYED
= range(11)
52 SEARCH_COLUMNS
= (C_TITLE
, C_DESCRIPTION
)
54 VIEW_ALL
, VIEW_UNDELETED
, VIEW_DOWNLOADED
, VIEW_UNPLAYED
= range(4)
56 # In which steps the UI is updated for "loading" animations
60 gtk
.ListStore
.__init
__(self
, str, str, str, object, \
61 gtk
.gdk
.Pixbuf
, str, str, str, bool, bool, bool)
63 # Update progress (if we're currently being updated)
64 self
._update
_progress
= 0.
65 self
._last
_redraw
_progress
= 0.
67 # Filter to allow hiding some episodes
68 self
._filter
= self
.filter_new()
69 self
._view
_mode
= self
.VIEW_ALL
70 self
._search
_term
= None
71 self
._filter
.set_visible_func(self
._filter
_visible
_func
)
73 # Are we currently showing the "all episodes" view?
74 self
._all
_episodes
_view
= False
76 # "ICON" is used to mark icon names in source files
80 self
.ICON_AUDIO_FILE
= ICON('audio-x-generic')
81 self
.ICON_VIDEO_FILE
= ICON('video-x-generic')
82 self
.ICON_IMAGE_FILE
= ICON('image-x-generic')
83 self
.ICON_GENERIC_FILE
= ICON('text-x-generic')
84 self
.ICON_DOWNLOADING
= gtk
.STOCK_GO_DOWN
85 self
.ICON_DELETED
= gtk
.STOCK_DELETE
86 self
.ICON_NEW
= gtk
.STOCK_ABOUT
87 self
.ICON_UNPLAYED
= ICON('emblem-new')
88 self
.ICON_LOCKED
= ICON('emblem-readonly')
89 self
.ICON_MISSING
= ICON('emblem-unreadable')
92 def _format_filesize(self
, episode
):
93 if episode
.length
> 0:
94 return util
.format_filesize(episode
.length
, 1)
99 def _filter_visible_func(self
, model
, iter):
100 # If searching is active, set visibility based on search text
101 if self
._search
_term
is not None:
102 key
= self
._search
_term
.lower()
103 return any((key
in (model
.get_value(iter, column
) or '').lower()) for column
in self
.SEARCH_COLUMNS
)
105 if self
._view
_mode
== self
.VIEW_ALL
:
107 elif self
._view
_mode
== self
.VIEW_UNDELETED
:
108 return model
.get_value(iter, self
.C_VIEW_SHOW_UNDELETED
)
109 elif self
._view
_mode
== self
.VIEW_DOWNLOADED
:
110 return model
.get_value(iter, self
.C_VIEW_SHOW_DOWNLOADED
)
111 elif self
._view
_mode
== self
.VIEW_UNPLAYED
:
112 return model
.get_value(iter, self
.C_VIEW_SHOW_UNPLAYED
)
116 def get_update_progress(self
):
117 return self
._update
_progress
119 def reset_update_progress(self
):
120 self
._update
_progress
= 0.
122 def get_filtered_model(self
):
123 """Returns a filtered version of this episode model
125 The filtered version should be displayed in the UI,
126 as this model can have some filters set that should
127 be reflected in the UI.
131 def set_view_mode(self
, new_mode
):
132 """Sets a new view mode for this model
134 After setting the view mode, the filtered model
135 might be updated to reflect the new mode."""
136 if self
._view
_mode
!= new_mode
:
137 self
._view
_mode
= new_mode
138 self
._filter
.refilter()
140 def get_view_mode(self
):
141 """Returns the currently-set view mode"""
142 return self
._view
_mode
144 def set_search_term(self
, new_term
):
145 if self
._search
_term
!= new_term
:
146 self
._search
_term
= new_term
147 self
._filter
.refilter()
149 def get_search_term(self
):
150 return self
._search
_term
152 def _format_description(self
, episode
, include_description
=False, is_downloading
=None):
153 if include_description
and self
._all
_episodes
_view
:
154 return '%s\n<small>%s</small>' % (xml
.sax
.saxutils
.escape(episode
.title
),
155 _('from %s') % xml
.sax
.saxutils
.escape(episode
.channel
.title
))
156 elif include_description
:
157 return '%s\n<small>%s</small>' % (xml
.sax
.saxutils
.escape(episode
.title
),
158 xml
.sax
.saxutils
.escape(episode
.one_line_description()))
160 return xml
.sax
.saxutils
.escape(episode
.title
)
162 def add_from_channel(self
, channel
, downloading
=None, \
163 include_description
=False, generate_thumbnails
=False, \
166 Add episode from the given channel to this model.
167 Downloading should be a callback.
168 include_description should be a boolean value (True if description
169 is to be added to the episode row, or False if not)
172 self
._update
_progress
= 0.
173 self
._last
_redraw
_progress
= 0.
174 if treeview
is not None:
175 util
.idle_add(treeview
.queue_draw
)
177 self
._all
_episodes
_view
= getattr(channel
, 'ALL_EPISODES_PROXY', False)
179 episodes
= list(channel
.get_all_episodes())
180 count
= len(episodes
)
182 for position
, episode
in enumerate(episodes
):
185 self
.C_URL
, episode
.url
, \
186 self
.C_TITLE
, episode
.title
, \
187 self
.C_FILESIZE_TEXT
, self
._format
_filesize
(episode
), \
188 self
.C_EPISODE
, episode
, \
189 self
.C_PUBLISHED_TEXT
, episode
.cute_pubdate())
190 self
.update_by_iter(iter, downloading
, include_description
, \
191 generate_thumbnails
, reload_from_db
=False)
193 self
._update
_progress
= float(position
+1)/count
194 if treeview
is not None and \
195 (self
._update
_progress
> self
._last
_redraw
_progress
+ self
._UI
_UPDATE
_STEP
or position
+1 == count
):
196 def in_gtk_main_thread():
197 treeview
.queue_draw()
198 while gtk
.events_pending():
199 gtk
.main_iteration(False)
200 util
.idle_add(in_gtk_main_thread
)
201 self
._last
_redraw
_progress
= self
._update
_progress
203 def update_all(self
, downloading
=None, include_description
=False, \
204 generate_thumbnails
=False):
206 self
.update_by_iter(row
.iter, downloading
, include_description
, \
209 def update_by_urls(self
, urls
, downloading
=None, include_description
=False, \
210 generate_thumbnails
=False):
212 if row
[self
.C_URL
] in urls
:
213 self
.update_by_iter(row
.iter, downloading
, include_description
, \
216 def update_by_filter_iter(self
, iter, downloading
=None, \
217 include_description
=False, generate_thumbnails
=False):
218 # Convenience function for use by "outside" methods that use iters
219 # from the filtered episode list model (i.e. all UI things normally)
220 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter), \
221 downloading
, include_description
, generate_thumbnails
)
223 def update_by_iter(self
, iter, downloading
=None, include_description
=False, \
224 generate_thumbnails
=False, reload_from_db
=True):
225 episode
= self
.get_value(iter, self
.C_EPISODE
)
227 episode
.reload_from_db()
229 if include_description
or gpodder
.ui
.maemo
:
238 status_icon_to_build_from_file
= False
240 view_show_undeleted
= True
241 view_show_downloaded
= False
242 view_show_unplayed
= False
243 icon_theme
= gtk
.icon_theme_get_default()
245 if downloading
is not None and downloading(episode
):
246 tooltip
= _('Downloading')
247 status_icon
= self
.ICON_DOWNLOADING
248 view_show_downloaded
= True
249 view_show_unplayed
= True
251 if episode
.state
== gpodder
.STATE_DELETED
:
252 tooltip
= _('Deleted')
253 status_icon
= self
.ICON_DELETED
254 view_show_undeleted
= False
255 elif episode
.state
== gpodder
.STATE_NORMAL
and \
256 not episode
.is_played
:
257 tooltip
= _('New episode')
258 status_icon
= self
.ICON_NEW
259 view_show_downloaded
= True
260 view_show_unplayed
= True
261 elif episode
.state
== gpodder
.STATE_DOWNLOADED
:
263 view_show_downloaded
= True
264 view_show_unplayed
= not episode
.is_played
265 show_bullet
= not episode
.is_played
266 show_padlock
= episode
.is_locked
267 show_missing
= not episode
.file_exists()
268 filename
= episode
.local_filename(create
=False, check_only
=True)
270 file_type
= episode
.file_type()
271 if file_type
== 'audio':
272 tooltip
.append(_('Downloaded episode'))
273 status_icon
= self
.ICON_AUDIO_FILE
274 elif file_type
== 'video':
275 tooltip
.append(_('Downloaded video episode'))
276 status_icon
= self
.ICON_VIDEO_FILE
277 elif file_type
== 'image':
278 tooltip
.append(_('Downloaded image'))
279 status_icon
= self
.ICON_IMAGE_FILE
281 # Optional thumbnailing for image downloads
282 if generate_thumbnails
:
283 if filename
is not None:
284 # set the status icon to the path itself (that
285 # should be a good identifier anyway)
286 status_icon
= filename
287 status_icon_to_build_from_file
= True
289 tooltip
.append(_('Downloaded file'))
290 status_icon
= self
.ICON_GENERIC_FILE
292 # Try to find a themed icon for this file
293 if filename
is not None and have_gio
:
294 file = gio
.File(filename
)
295 if file.query_exists():
296 file_info
= file.query_info('*')
297 icon
= file_info
.get_icon()
298 for icon_name
in icon
.get_names():
299 if icon_theme
.has_icon(icon_name
):
300 status_icon
= icon_name
304 tooltip
.append(_('missing file'))
307 if file_type
== 'image':
308 tooltip
.append(_('never displayed'))
309 elif file_type
in ('audio', 'video'):
310 tooltip
.append(_('never played'))
312 tooltip
.append(_('never opened'))
314 if file_type
== 'image':
315 tooltip
.append(_('displayed'))
316 elif file_type
in ('audio', 'video'):
317 tooltip
.append(_('played'))
319 tooltip
.append(_('opened'))
321 tooltip
.append(_('deletion prevented'))
323 tooltip
= ', '.join(tooltip
)
325 if status_icon
is not None:
326 status_icon
= self
._get
_tree
_icon
(status_icon
, show_bullet
, \
327 show_padlock
, show_missing
, icon_size
, status_icon_to_build_from_file
)
329 description
= self
._format
_description
(episode
, include_description
, downloading
)
331 self
.C_STATUS_ICON
, status_icon
, \
332 self
.C_VIEW_SHOW_UNDELETED
, view_show_undeleted
, \
333 self
.C_VIEW_SHOW_DOWNLOADED
, view_show_downloaded
, \
334 self
.C_VIEW_SHOW_UNPLAYED
, view_show_unplayed
, \
335 self
.C_DESCRIPTION
, description
, \
336 self
.C_TOOLTIP
, tooltip
)
338 def _get_icon_from_image(self
,image_path
, icon_size
):
340 Load an local image file and transform it into an icon.
342 Return a pixbuf scaled to the desired size and may return None
343 if the icon creation is impossible (file not found etc).
345 if not os
.path
.exists(image_path
):
347 # load image from disc (code adapted from CoverDownloader
348 # except that no download is needed here)
349 loader
= gtk
.gdk
.PixbufLoader()
352 loader
.write(open(image_path
, 'rb').read())
354 pixbuf
= loader
.get_pixbuf()
356 log('Data error while loading image %s', image_path
, sender
=self
)
358 # Now scale the image with ratio (copied from _resize_pixbuf_keep_ratio)
360 if pixbuf
.get_width() > icon_size
:
361 f
= float(icon_size
)/pixbuf
.get_width()
362 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
363 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
365 if pixbuf
.get_height() > icon_size
:
366 f
= float(icon_size
)/pixbuf
.get_height()
367 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
368 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
372 def _get_tree_icon(self
, icon_name
, add_bullet
=False, \
373 add_padlock
=False, add_missing
=False, icon_size
=32, \
374 build_icon_from_file
= False):
376 Loads an icon from the current icon theme at the specified
377 size, suitable for display in a gtk.TreeView. Additional
378 emblems can be added on top of the icon.
380 Caching is used to speed up the icon lookup.
382 The `build_icon_from_file` argument indicates (when True) that
383 the icon has to be created on the fly from a given image
384 file. The `icon_name` argument is then interpreted as the path
385 to this file. Those specific icons will *not be cached*.
388 # Add all variables that modify the appearance of the icon, so
389 # our cache does not return the same icons for different requests
390 cache_id
= (icon_name
, add_bullet
, add_padlock
, add_missing
, icon_size
)
392 if cache_id
in self
._icon
_cache
:
393 return self
._icon
_cache
[cache_id
]
395 icon_theme
= gtk
.icon_theme_get_default()
398 if build_icon_from_file
:
399 icon
= self
._get
_icon
_from
_image
(icon_name
,icon_size
)
401 icon
= icon_theme
.load_icon(icon_name
, icon_size
, 0)
403 icon
= icon_theme
.load_icon(gtk
.STOCK_DIALOG_QUESTION
, icon_size
, 0)
405 if icon
and (add_bullet
or add_padlock
or add_missing
):
406 # We'll modify the icon, so use .copy()
410 # Desaturate the icon so it looks even more "missing"
411 icon
.saturate_and_pixelate(icon
, 0.0, False)
412 emblem
= icon_theme
.load_icon(self
.ICON_MISSING
, icon_size
/2, 0)
413 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
414 xpos
= icon
.get_width() - width
415 ypos
= icon
.get_height() - height
416 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
422 emblem
= icon_theme
.load_icon(self
.ICON_UNPLAYED
, icon_size
/2, 0)
423 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
424 xpos
= icon
.get_width() - width
425 ypos
= icon
.get_height() - height
426 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
432 emblem
= icon_theme
.load_icon(self
.ICON_LOCKED
, icon_size
/2, 0)
433 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
434 emblem
.composite(icon
, 0, 0, width
, height
, 0, 0, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
438 self
._icon
_cache
[cache_id
] = icon
442 class PodcastChannelProxy(object):
443 ALL_EPISODES_PROXY
= True
445 def __init__(self
, db
, config
, channels
):
447 self
._config
= config
448 self
.channels
= channels
449 self
.title
= _('All episodes')
450 self
.description
= _('from all podcasts')
451 self
.parse_error
= ''
454 self
._save
_dir
_size
_set
= False
455 self
.save_dir_size
= 0L
456 self
.cover_file
= os
.path
.join(gpodder
.images_folder
, 'podcast-all.png')
458 def __getattribute__(self
, name
):
460 return object.__getattribute
__(self
, name
)
461 except AttributeError:
462 log('Unsupported method call (%s)', name
, sender
=self
)
464 def get_statistics(self
):
465 # Get the total statistics for all channels from the database
466 return self
._db
.get_total_count()
468 def get_all_episodes(self
):
469 """Returns a generator that yields every episode"""
471 for channel
in self
.channels
:
472 for episode
in channel
.get_all_episodes():
473 episode
._all
_episodes
_view
= True
475 return model
.PodcastEpisode
.sort_by_pubdate(all_episodes(), reverse
=True)
477 def request_save_dir_size(self
):
478 if not self
._save
_dir
_size
_set
:
479 self
.update_save_dir_size()
480 self
._save
_dir
_size
_set
= True
482 def update_save_dir_size(self
):
483 self
.save_dir_size
= util
.calculate_size(self
._config
.download_dir
)
486 class PodcastListModel(gtk
.ListStore
):
487 C_URL
, C_TITLE
, C_DESCRIPTION
, C_PILL
, C_CHANNEL
, \
488 C_COVER
, C_ERROR
, C_PILL_VISIBLE
, \
489 C_VIEW_SHOW_UNDELETED
, C_VIEW_SHOW_DOWNLOADED
, \
490 C_VIEW_SHOW_UNPLAYED
, C_HAS_EPISODES
, C_SEPARATOR
= range(13)
492 SEARCH_COLUMNS
= (C_TITLE
, C_DESCRIPTION
)
495 def row_separator_func(cls
, model
, iter):
496 return model
.get_value(iter, cls
.C_SEPARATOR
)
498 def __init__(self
, cover_downloader
):
499 gtk
.ListStore
.__init
__(self
, str, str, str, gtk
.gdk
.Pixbuf
, \
500 object, gtk
.gdk
.Pixbuf
, str, bool, bool, bool, bool, bool, bool)
502 # Filter to allow hiding some episodes
503 self
._filter
= self
.filter_new()
505 self
._search
_term
= None
506 self
._filter
.set_visible_func(self
._filter
_visible
_func
)
508 self
._cover
_cache
= {}
509 if gpodder
.ui
.fremantle
:
510 self
._max
_image
_side
= 64
512 self
._max
_image
_side
= 40
513 self
._cover
_downloader
= cover_downloader
515 def _filter_visible_func(self
, model
, iter):
516 # If searching is active, set visibility based on search text
517 if self
._search
_term
is not None:
518 key
= self
._search
_term
.lower()
519 return any((key
in model
.get_value(iter, column
).lower()) for column
in self
.SEARCH_COLUMNS
)
521 if model
.get_value(iter, self
.C_SEPARATOR
):
523 if self
._view
_mode
== EpisodeListModel
.VIEW_ALL
:
524 return model
.get_value(iter, self
.C_HAS_EPISODES
)
525 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNDELETED
:
526 return model
.get_value(iter, self
.C_VIEW_SHOW_UNDELETED
)
527 elif self
._view
_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
528 return model
.get_value(iter, self
.C_VIEW_SHOW_DOWNLOADED
)
529 elif self
._view
_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
530 return model
.get_value(iter, self
.C_VIEW_SHOW_UNPLAYED
)
534 def get_filtered_model(self
):
535 """Returns a filtered version of this episode model
537 The filtered version should be displayed in the UI,
538 as this model can have some filters set that should
539 be reflected in the UI.
543 def set_view_mode(self
, new_mode
):
544 """Sets a new view mode for this model
546 After setting the view mode, the filtered model
547 might be updated to reflect the new mode."""
548 if self
._view
_mode
!= new_mode
:
549 self
._view
_mode
= new_mode
550 self
._filter
.refilter()
552 def get_view_mode(self
):
553 """Returns the currently-set view mode"""
554 return self
._view
_mode
556 def set_search_term(self
, new_term
):
557 if self
._search
_term
!= new_term
:
558 self
._search
_term
= new_term
559 self
._filter
.refilter()
561 def get_search_term(self
):
562 return self
._search
_term
564 def enable_separators(self
, channeltree
):
565 channeltree
.set_row_separator_func(self
._show
_row
_separator
)
567 def _show_row_separator(self
, model
, iter):
568 return model
.get_value(iter, self
.C_SEPARATOR
)
570 def _resize_pixbuf_keep_ratio(self
, url
, pixbuf
):
572 Resizes a GTK Pixbuf but keeps its aspect ratio.
573 Returns None if the pixbuf does not need to be
574 resized or the newly resized pixbuf if it does.
579 if url
in self
._cover
_cache
:
580 return self
._cover
_cache
[url
]
583 if pixbuf
.get_width() > self
._max
_image
_side
:
584 f
= float(self
._max
_image
_side
)/pixbuf
.get_width()
585 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
586 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
590 if pixbuf
.get_height() > self
._max
_image
_side
:
591 f
= float(self
._max
_image
_side
)/pixbuf
.get_height()
592 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
593 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
597 self
._cover
_cache
[url
] = pixbuf
602 def _resize_pixbuf(self
, url
, pixbuf
):
606 return self
._resize
_pixbuf
_keep
_ratio
(url
, pixbuf
) or pixbuf
608 def _get_cover_image(self
, channel
):
609 if self
._cover
_downloader
is None:
612 pixbuf
= self
._cover
_downloader
.get_cover(channel
, avoid_downloading
=True)
613 return self
._resize
_pixbuf
(channel
.url
, pixbuf
)
615 def _get_pill_image(self
, channel
, count_downloaded
, count_unplayed
):
616 if count_unplayed
> 0 or count_downloaded
> 0:
617 return draw
.draw_pill_pixbuf(str(count_unplayed
), str(count_downloaded
))
621 def _format_description(self
, channel
, total
, deleted
, \
622 new
, downloaded
, unplayed
):
623 title_markup
= xml
.sax
.saxutils
.escape(channel
.title
)
624 description_markup
= xml
.sax
.saxutils
.escape(util
.get_first_line(channel
.description
) or ' ')
627 d
.append('<span weight="bold">')
628 d
.append(title_markup
)
631 return ''.join(d
+['\n', '<small>', description_markup
, '</small>'])
633 def _format_error(self
, channel
):
634 if channel
.parse_error
:
635 return str(channel
.parse_error
)
639 def set_channels(self
, db
, config
, channels
):
640 # Clear the model and update the list of podcasts
643 if config
.podcast_list_view_all
:
644 all_episodes
= PodcastChannelProxy(db
, config
, channels
)
647 self
.C_URL
, all_episodes
.url
, \
648 self
.C_CHANNEL
, all_episodes
, \
649 self
.C_COVER
, self
._get
_cover
_image
(all_episodes
), \
650 self
.C_SEPARATOR
, False)
651 self
.update_by_iter(iter)
654 self
.set(iter, self
.C_SEPARATOR
, True)
656 for channel
in channels
:
659 self
.C_URL
, channel
.url
, \
660 self
.C_CHANNEL
, channel
, \
661 self
.C_COVER
, self
._get
_cover
_image
(channel
), \
662 self
.C_SEPARATOR
, False)
663 self
.update_by_iter(iter)
665 def get_filter_path_from_url(self
, url
):
666 # Return the path of the filtered model for a given URL
667 child_path
= self
.get_path_from_url(url
)
668 if child_path
is None:
671 return self
._filter
.convert_child_path_to_path(child_path
)
673 def get_path_from_url(self
, url
):
674 # Return the tree model path for a given URL
679 if row
[self
.C_URL
] == url
:
683 def update_first_row(self
):
684 # Update the first row in the model (for "all episodes" updates)
685 self
.update_by_iter(self
.get_iter_first())
687 def update_by_urls(self
, urls
):
688 # Given a list of URLs, update each matching row
690 if row
[self
.C_URL
] in urls
:
691 self
.update_by_iter(row
.iter)
693 def iter_is_first_row(self
, iter):
694 iter = self
._filter
.convert_iter_to_child_iter(iter)
695 path
= self
.get_path(iter)
696 return (path
== (0,))
698 def update_by_filter_iter(self
, iter):
699 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter))
701 def update_all(self
):
703 self
.update_by_iter(row
.iter)
705 def update_by_iter(self
, iter):
706 # Given a GtkTreeIter, update volatile information
707 channel
= self
.get_value(iter, self
.C_CHANNEL
)
710 total
, deleted
, new
, downloaded
, unplayed
= channel
.get_statistics()
711 description
= self
._format
_description
(channel
, total
, deleted
, new
, \
712 downloaded
, unplayed
)
714 pill_image
= self
._get
_pill
_image
(channel
, downloaded
, unplayed
)
716 self
.C_TITLE
, channel
.title
, \
717 self
.C_DESCRIPTION
, description
, \
718 self
.C_ERROR
, self
._format
_error
(channel
), \
719 self
.C_PILL
, pill_image
, \
720 self
.C_PILL_VISIBLE
, pill_image
!= None, \
721 self
.C_VIEW_SHOW_UNDELETED
, total
- deleted
> 0, \
722 self
.C_VIEW_SHOW_DOWNLOADED
, downloaded
+ new
> 0, \
723 self
.C_VIEW_SHOW_UNPLAYED
, unplayed
+ new
> 0, \
724 self
.C_HAS_EPISODES
, total
> 0)
726 def add_cover_by_url(self
, url
, pixbuf
):
727 # Resize and add the new cover image
728 pixbuf
= self
._resize
_pixbuf
(url
, pixbuf
)
730 if row
[self
.C_URL
] == url
:
731 row
[self
.C_COVER
] = pixbuf
734 def delete_cover_by_url(self
, url
):
735 # Remove the cover from the model
737 if row
[self
.C_URL
] == url
:
738 row
[self
.C_COVER
] = None
741 # Remove the cover from the cache
742 if url
in self
._cover
_cache
:
743 del self
._cover
_cache
[url
]