~ubuntu-branches/ubuntu/vivid/gpodder/vivid

« back to all changes in this revision

Viewing changes to .pc/debian-changes-2.15-1/src/gpodder/gui.py

  • Committer: Bazaar Package Importer
  • Author(s): tony mancill
  • Date: 2011-05-31 22:05:31 UTC
  • mfrom: (5.2.18 sid)
  • Revision ID: james.westby@ubuntu.com-20110531220531-f3gt49fypbmuair8
Tags: 2.15-2
This time without a patch that reverts the source to 2.14.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
#
3
 
# gPodder - A media aggregator and podcast client
4
 
# Copyright (c) 2005-2011 Thomas Perl and the gPodder Team
5
 
#
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.
10
 
#
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.
15
 
#
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/>.
18
 
#
19
 
 
20
 
import os
21
 
import platform
22
 
import gtk
23
 
import gtk.gdk
24
 
import gobject
25
 
import pango
26
 
import random
27
 
import sys
28
 
import shutil
29
 
import subprocess
30
 
import glob
31
 
import time
32
 
import tempfile
33
 
import collections
34
 
import threading
35
 
import urllib
36
 
 
37
 
from xml.sax import saxutils
38
 
 
39
 
import gpodder
40
 
 
41
 
try:
42
 
    import dbus
43
 
    import dbus.service
44
 
    import dbus.mainloop
45
 
    import dbus.glib
46
 
except ImportError:
47
 
    # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
48
 
    class dbus:
49
 
        class SessionBus:
50
 
            def __init__(self, *args, **kwargs):
51
 
                pass
52
 
            def add_signal_receiver(self, *args, **kwargs):
53
 
                pass
54
 
        class glib:
55
 
            class DBusGMainLoop:
56
 
                def __init__(self, *args, **kwargs):
57
 
                    pass
58
 
        class service:
59
 
            @staticmethod
60
 
            def method(*args, **kwargs):
61
 
                return lambda x: x
62
 
            class BusName:
63
 
                def __init__(self, *args, **kwargs):
64
 
                    pass
65
 
            class Object:
66
 
                def __init__(self, *args, **kwargs):
67
 
                    pass
68
 
 
69
 
 
70
 
from gpodder import feedcore
71
 
from gpodder import util
72
 
from gpodder import opml
73
 
from gpodder import download
74
 
from gpodder import my
75
 
from gpodder import youtube
76
 
from gpodder import player
77
 
from gpodder.liblogger import log
78
 
 
79
 
_ = gpodder.gettext
80
 
N_ = gpodder.ngettext
81
 
 
82
 
from gpodder.model import PodcastChannel
83
 
from gpodder.model import PodcastEpisode
84
 
from gpodder.dbsqlite import Database
85
 
 
86
 
from gpodder.gtkui.model import PodcastListModel
87
 
from gpodder.gtkui.model import EpisodeListModel
88
 
from gpodder.gtkui.config import UIConfig
89
 
from gpodder.gtkui.services import CoverDownloader
90
 
from gpodder.gtkui.widgets import SimpleMessageArea
91
 
from gpodder.gtkui.desktopfile import UserAppsReader
92
 
 
93
 
from gpodder.gtkui.draw import draw_text_box_centered
94
 
 
95
 
from gpodder.gtkui.interface.common import BuilderWidget
96
 
from gpodder.gtkui.interface.common import TreeViewHelper
97
 
from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
98
 
 
99
 
if gpodder.ui.desktop:
100
 
    from gpodder.gtkui.download import DownloadStatusModel
101
 
 
102
 
    from gpodder.gtkui.desktop.sync import gPodderSyncUI
103
 
 
104
 
    from gpodder.gtkui.desktop.channel import gPodderChannel
105
 
    from gpodder.gtkui.desktop.preferences import gPodderPreferences
106
 
    from gpodder.gtkui.desktop.shownotes import gPodderShownotes
107
 
    from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
108
 
    from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
109
 
    from gpodder.gtkui.desktop.dependencymanager import gPodderDependencyManager
110
 
    from gpodder.gtkui.interface.progress import ProgressIndicator
111
 
    try:
112
 
        from gpodder.gtkui.desktop.trayicon import GPodderStatusIcon
113
 
        have_trayicon = True
114
 
    except Exception, exc:
115
 
        log('Warning: Could not import gpodder.trayicon.', traceback=True)
116
 
        log('Warning: This probably means your PyGTK installation is too old!')
117
 
        have_trayicon = False
118
 
elif gpodder.ui.diablo:
119
 
    from gpodder.gtkui.download import DownloadStatusModel
120
 
 
121
 
    from gpodder.gtkui.maemo.channel import gPodderChannel
122
 
    from gpodder.gtkui.maemo.preferences import gPodderPreferences
123
 
    from gpodder.gtkui.maemo.shownotes import gPodderShownotes
124
 
    from gpodder.gtkui.maemo.episodeselector import gPodderEpisodeSelector
125
 
    from gpodder.gtkui.maemo.podcastdirectory import gPodderPodcastDirectory
126
 
    from gpodder.gtkui.maemo.mygpodder import MygPodderSettings
127
 
    from gpodder.gtkui.interface.progress import ProgressIndicator
128
 
    have_trayicon = False
129
 
elif gpodder.ui.fremantle:
130
 
    from gpodder.gtkui.frmntl.model import DownloadStatusModel
131
 
    from gpodder.gtkui.frmntl.model import EpisodeListModel
132
 
    from gpodder.gtkui.frmntl.model import PodcastListModel
133
 
 
134
 
    from gpodder.gtkui.maemo.channel import gPodderChannel
135
 
    from gpodder.gtkui.frmntl.preferences import gPodderPreferences
136
 
    from gpodder.gtkui.frmntl.shownotes import gPodderShownotes
137
 
    from gpodder.gtkui.frmntl.episodeselector import gPodderEpisodeSelector
138
 
    from gpodder.gtkui.frmntl.podcastdirectory import gPodderPodcastDirectory
139
 
    from gpodder.gtkui.frmntl.episodes import gPodderEpisodes
140
 
    from gpodder.gtkui.frmntl.downloads import gPodderDownloads
141
 
    from gpodder.gtkui.frmntl.progress import ProgressIndicator
142
 
    from gpodder.gtkui.frmntl.widgets import FancyProgressBar
143
 
    have_trayicon = False
144
 
 
145
 
    from gpodder.gtkui.frmntl.portrait import FremantleRotation
146
 
    from gpodder.gtkui.frmntl.mafw import MafwPlaybackMonitor
147
 
    from gpodder.gtkui.frmntl.hints import HINT_STRINGS
148
 
    from gpodder.gtkui.frmntl.network import NetworkManager
149
 
 
150
 
from gpodder.gtkui.interface.common import Orientation
151
 
 
152
 
from gpodder.gtkui.interface.welcome import gPodderWelcome
153
 
 
154
 
if gpodder.ui.maemo:
155
 
    import hildon
156
 
 
157
 
from gpodder.dbusproxy import DBusPodcastsProxy
158
 
from gpodder import hooks
159
 
 
160
 
class gPodder(BuilderWidget, dbus.service.Object):
161
 
    finger_friendly_widgets = ['btnCleanUpDownloads', 'button_search_episodes_clear', 'label2', 'labelDownloads', 'btnUpdateFeeds']
162
 
 
163
 
    ICON_GENERAL_ADD = 'general_add'
164
 
    ICON_GENERAL_REFRESH = 'general_refresh'
165
 
 
166
 
    # Delay until live search is started after typing stop
167
 
    LIVE_SEARCH_DELAY = 200
168
 
 
169
 
    def __init__(self, bus_name, config):
170
 
        dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
171
 
        self.podcasts_proxy = DBusPodcastsProxy(lambda: self.channels, \
172
 
                self.on_itemUpdate_activate, \
173
 
                self.playback_episodes, \
174
 
                self.download_episode_list, \
175
 
                self.episode_object_by_uri, \
176
 
                bus_name)
177
 
        self.db = Database(gpodder.database_file)
178
 
        self.config = config
179
 
        BuilderWidget.__init__(self, None)
180
 
    
181
 
    def new(self):
182
 
        if gpodder.ui.diablo:
183
 
            import hildon
184
 
            self.app = hildon.Program()
185
 
            self.app.add_window(self.main_window)
186
 
            self.main_window.add_toolbar(self.toolbar)
187
 
            menu = gtk.Menu()
188
 
            for child in self.main_menu.get_children():
189
 
                child.reparent(menu)
190
 
            self.main_window.set_menu(self.set_finger_friendly(menu))
191
 
            self._last_orientation = Orientation.LANDSCAPE
192
 
        elif gpodder.ui.fremantle:
193
 
            import hildon
194
 
            self.app = hildon.Program()
195
 
            self.app.add_window(self.main_window)
196
 
 
197
 
            appmenu = hildon.AppMenu()
198
 
 
199
 
            for filter in (self.item_view_podcasts_all, \
200
 
                           self.item_view_podcasts_downloaded, \
201
 
                           self.item_view_podcasts_unplayed):
202
 
                button = gtk.ToggleButton()
203
 
                filter.connect_proxy(button)
204
 
                appmenu.add_filter(button)
205
 
 
206
 
            for action in (self.itemPreferences, \
207
 
                    self.item_downloads, \
208
 
                    self.itemRemoveOldEpisodes, \
209
 
                    self.item_unsubscribe, \
210
 
                    self.itemAbout):
211
 
                button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
212
 
                        hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
213
 
                action.connect_proxy(button)
214
 
                if action == self.item_downloads:
215
 
                    button.set_title(_('Downloads'))
216
 
                    button.set_value(_('Idle'))
217
 
                    self.button_downloads = button
218
 
                appmenu.append(button)
219
 
 
220
 
            def show_hint(button):
221
 
                self.show_message(random.choice(HINT_STRINGS), important=True)
222
 
 
223
 
            button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
224
 
                    hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
225
 
            button.set_title(_('Hint of the day'))
226
 
            button.connect('clicked', show_hint)
227
 
            appmenu.append(button)
228
 
 
229
 
            appmenu.show_all()
230
 
            self.main_window.set_app_menu(appmenu)
231
 
 
232
 
            # Initialize portrait mode / rotation manager
233
 
            self._fremantle_rotation = FremantleRotation('gPodder', \
234
 
                    self.main_window, \
235
 
                    gpodder.__version__, \
236
 
                    self.config.rotation_mode)
237
 
 
238
 
            # Initialize the Fremantle network manager
239
 
            self.network_manager = NetworkManager()
240
 
 
241
 
            if self.config.rotation_mode == FremantleRotation.ALWAYS:
242
 
                util.idle_add(self.on_window_orientation_changed, \
243
 
                        Orientation.PORTRAIT)
244
 
                self._last_orientation = Orientation.PORTRAIT
245
 
            else:
246
 
                self._last_orientation = Orientation.LANDSCAPE
247
 
 
248
 
            # Flag set when a notification is being shown (Maemo bug 11235)
249
 
            self._fremantle_notification_visible = False
250
 
        else:
251
 
            self._last_orientation = Orientation.LANDSCAPE
252
 
            self.toolbar.set_property('visible', self.config.show_toolbar)
253
 
 
254
 
        self.bluetooth_available = util.bluetooth_available()
255
 
 
256
 
        self.config.connect_gtk_window(self.gPodder, 'main_window')
257
 
        if not gpodder.ui.fremantle:
258
 
            self.config.connect_gtk_paned('paned_position', self.channelPaned)
259
 
        self.main_window.show()
260
 
 
261
 
        self.player_receiver = player.MediaPlayerDBusReceiver(self.on_played)
262
 
 
263
 
        if gpodder.ui.fremantle:
264
 
            # Create a D-Bus monitoring object that takes care of
265
 
            # tracking MAFW (Nokia Media Player) playback events
266
 
            # and sends episode playback status events via D-Bus
267
 
            self.mafw_monitor = MafwPlaybackMonitor(gpodder.dbus_session_bus)
268
 
 
269
 
        self.gPodder.connect('key-press-event', self.on_key_press)
270
 
 
271
 
        self.preferences_dialog = None
272
 
        self.config.add_observer(self.on_config_changed)
273
 
 
274
 
        self.tray_icon = None
275
 
        self.episode_shownotes_window = None
276
 
        self.new_episodes_window = None
277
 
 
278
 
        if gpodder.ui.desktop:
279
 
            # Mac OS X-specific UI tweaks: Native main menu integration
280
 
            # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
281
 
            if getattr(gtk.gdk, 'WINDOWING', 'x11') == 'quartz':
282
 
                try:
283
 
                    import igemacintegration as igemi
284
 
 
285
 
                    # Move the menu bar from the window to the Mac menu bar
286
 
                    self.mainMenu.hide()
287
 
                    igemi.ige_mac_menu_set_menu_bar(self.mainMenu)
288
 
 
289
 
                    # Reparent some items to the "Application" menu
290
 
                    for widget in ('/mainMenu/menuHelp/itemAbout', \
291
 
                                   '/mainMenu/menuPodcasts/itemPreferences'):
292
 
                        item = self.uimanager1.get_widget(widget)
293
 
                        group = igemi.ige_mac_menu_add_app_menu_group()
294
 
                        igemi.ige_mac_menu_add_app_menu_item(group, item, None)
295
 
 
296
 
                    quit_widget = '/mainMenu/menuPodcasts/itemQuit'
297
 
                    quit_item = self.uimanager1.get_widget(quit_widget)
298
 
                    igemi.ige_mac_menu_set_quit_menu_item(quit_item)
299
 
                except ImportError:
300
 
                    print >>sys.stderr, """
301
 
                    Warning: ige-mac-integration not found - no native menus.
302
 
                    """
303
 
 
304
 
            self.sync_ui = gPodderSyncUI(self.config, self.notification, \
305
 
                    self.main_window, self.show_confirmation, \
306
 
                    self.update_episode_list_icons, \
307
 
                    self.update_podcast_list_model, self.toolPreferences, \
308
 
                    gPodderEpisodeSelector, \
309
 
                    self.commit_changes_to_database)
310
 
        else:
311
 
            self.sync_ui = None
312
 
 
313
 
        self.download_status_model = DownloadStatusModel()
314
 
        self.download_queue_manager = download.DownloadQueueManager(self.config)
315
 
 
316
 
        if gpodder.ui.desktop:
317
 
            self.show_hide_tray_icon()
318
 
            self.itemShowAllEpisodes.set_active(self.config.podcast_list_view_all)
319
 
            self.itemShowToolbar.set_active(self.config.show_toolbar)
320
 
            self.itemShowDescription.set_active(self.config.episode_list_descriptions)
321
 
 
322
 
        if not gpodder.ui.fremantle:
323
 
            self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
324
 
            self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
325
 
            self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
326
 
            self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
327
 
 
328
 
            # When the amount of maximum downloads changes, notify the queue manager
329
 
            changed_cb = lambda spinbutton: self.download_queue_manager.spawn_threads()
330
 
            self.spinMaxDownloads.connect('value-changed', changed_cb)
331
 
 
332
 
        self.default_title = 'gPodder'
333
 
        if gpodder.__version__.rfind('git') != -1:
334
 
            self.set_title('gPodder %s' % gpodder.__version__)
335
 
        else:
336
 
            title = self.gPodder.get_title()
337
 
            if title is not None:
338
 
                self.set_title(title)
339
 
            else:
340
 
                self.set_title(_('gPodder'))
341
 
 
342
 
        self.cover_downloader = CoverDownloader()
343
 
 
344
 
        # Generate list models for podcasts and their episodes
345
 
        self.podcast_list_model = PodcastListModel(self.cover_downloader)
346
 
 
347
 
        self.cover_downloader.register('cover-available', self.cover_download_finished)
348
 
        self.cover_downloader.register('cover-removed', self.cover_file_removed)
349
 
 
350
 
        if gpodder.ui.fremantle:
351
 
            # Work around Maemo bug #4718
352
 
            self.button_refresh.set_name('HildonButton-finger')
353
 
            self.button_subscribe.set_name('HildonButton-finger')
354
 
 
355
 
            self.button_refresh.set_sensitive(False)
356
 
            self.button_subscribe.set_sensitive(False)
357
 
 
358
 
            self.button_subscribe.set_image(gtk.image_new_from_icon_name(\
359
 
                    self.ICON_GENERAL_ADD, gtk.ICON_SIZE_BUTTON))
360
 
            self.button_refresh.set_image(gtk.image_new_from_icon_name(\
361
 
                    self.ICON_GENERAL_REFRESH, gtk.ICON_SIZE_BUTTON))
362
 
 
363
 
            # Make the button scroll together with the TreeView contents
364
 
            action_area_box = self.treeChannels.get_action_area_box()
365
 
            for child in self.buttonbox:
366
 
                child.reparent(action_area_box)
367
 
            self.vbox.remove(self.buttonbox)
368
 
            self.treeChannels.set_action_area_visible(True)
369
 
 
370
 
            # Set up a very nice progress bar setup
371
 
            self.fancy_progress_bar = FancyProgressBar(self.main_window, \
372
 
                    self.on_btnCancelFeedUpdate_clicked)
373
 
            self.pbFeedUpdate = self.fancy_progress_bar.progress_bar
374
 
            self.pbFeedUpdate.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
375
 
            self.vbox.pack_start(self.fancy_progress_bar.event_box, False)
376
 
 
377
 
            from gpodder.gtkui.frmntl import style
378
 
            sub_font = style.get_font_desc('SmallSystemFont')
379
 
            sub_color = style.get_color('SecondaryTextColor')
380
 
            sub = (sub_font.to_string(), sub_color.to_string())
381
 
            sub = '<span font_desc="%s" foreground="%s">%%s</span>' % sub
382
 
            self.label_footer.set_markup(sub % gpodder.__copyright__)
383
 
 
384
 
            hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
385
 
            while gtk.events_pending():
386
 
                gtk.main_iteration(False)
387
 
 
388
 
            try:
389
 
                # Try to get the real package version from dpkg
390
 
                p = subprocess.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout=subprocess.PIPE)
391
 
                version, _stderr = p.communicate()
392
 
                del _stderr
393
 
                del p
394
 
            except:
395
 
                version = gpodder.__version__
396
 
            self.label_footer.set_markup(sub % ('v %s' % version))
397
 
            self.label_footer.hide()
398
 
 
399
 
            self.episodes_window = gPodderEpisodes(self.main_window, \
400
 
                    on_treeview_expose_event=self.on_treeview_expose_event, \
401
 
                    show_episode_shownotes=self.show_episode_shownotes, \
402
 
                    update_podcast_list_model=self.update_podcast_list_model, \
403
 
                    on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
404
 
                    item_view_episodes_all=self.item_view_episodes_all, \
405
 
                    item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
406
 
                    item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
407
 
                    item_view_episodes_undeleted=self.item_view_episodes_undeleted, \
408
 
                    on_entry_search_episodes_changed=self.on_entry_search_episodes_changed, \
409
 
                    on_entry_search_episodes_key_press=self.on_entry_search_episodes_key_press, \
410
 
                    hide_episode_search=self.hide_episode_search, \
411
 
                    on_itemUpdateChannel_activate=self.on_itemUpdateChannel_activate, \
412
 
                    playback_episodes=self.playback_episodes, \
413
 
                    delete_episode_list=self.delete_episode_list, \
414
 
                    episode_list_status_changed=self.episode_list_status_changed, \
415
 
                    download_episode_list=self.download_episode_list, \
416
 
                    episode_is_downloading=self.episode_is_downloading, \
417
 
                    show_episode_in_download_manager=self.show_episode_in_download_manager, \
418
 
                    add_download_task_monitor=self.add_download_task_monitor, \
419
 
                    remove_download_task_monitor=self.remove_download_task_monitor, \
420
 
                    for_each_episode_set_task_status=self.for_each_episode_set_task_status, \
421
 
                    on_itemUpdate_activate=self.on_itemUpdate_activate, \
422
 
                    show_delete_episodes_window=self.show_delete_episodes_window, \
423
 
                    cover_downloader=self.cover_downloader)
424
 
 
425
 
            # Expose objects for episode list type-ahead find
426
 
            self.hbox_search_episodes = self.episodes_window.hbox_search_episodes
427
 
            self.entry_search_episodes = self.episodes_window.entry_search_episodes
428
 
            self.button_search_episodes_clear = self.episodes_window.button_search_episodes_clear
429
 
 
430
 
            self.downloads_window = gPodderDownloads(self.main_window, \
431
 
                    on_treeview_expose_event=self.on_treeview_expose_event, \
432
 
                    cleanup_downloads=self.cleanup_downloads, \
433
 
                    _for_each_task_set_status=self._for_each_task_set_status, \
434
 
                    downloads_list_get_selection=self.downloads_list_get_selection, \
435
 
                    _config=self.config)
436
 
 
437
 
            self.treeAvailable = self.episodes_window.treeview
438
 
            self.treeDownloads = self.downloads_window.treeview
439
 
 
440
 
        # Source IDs for timeouts for search-as-you-type
441
 
        self._podcast_list_search_timeout = None
442
 
        self._episode_list_search_timeout = None
443
 
 
444
 
        # Init the treeviews that we use
445
 
        self.init_podcast_list_treeview()
446
 
        self.init_episode_list_treeview()
447
 
        self.init_download_list_treeview()
448
 
 
449
 
        if self.config.podcast_list_hide_boring:
450
 
            self.item_view_hide_boring_podcasts.set_active(True)
451
 
 
452
 
        self.currently_updating = False
453
 
 
454
 
        if gpodder.ui.maemo or self.config.enable_fingerscroll:
455
 
            self.context_menu_mouse_button = 1
456
 
        else:
457
 
            self.context_menu_mouse_button = 3
458
 
 
459
 
        if self.config.start_iconified:
460
 
            self.iconify_main_window()
461
 
 
462
 
        self.download_tasks_seen = set()
463
 
        self.download_list_update_enabled = False
464
 
        self.download_task_monitors = set()
465
 
 
466
 
        # Subscribed channels
467
 
        self.active_channel = None
468
 
        self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
469
 
        self.channel_list_changed = True
470
 
        self.update_podcasts_tab()
471
 
 
472
 
        # load list of user applications for audio playback
473
 
        self.user_apps_reader = UserAppsReader(['audio', 'video'])
474
 
        threading.Thread(target=self.user_apps_reader.read).start()
475
 
 
476
 
        # Set the "Device" menu item for the first time
477
 
        if gpodder.ui.desktop:
478
 
            self.update_item_device()
479
 
 
480
 
        # Set up the first instance of MygPoClient
481
 
        self.mygpo_client = my.MygPoClient(self.config)
482
 
 
483
 
        # Now, update the feed cache, when everything's in place
484
 
        if not gpodder.ui.fremantle:
485
 
            self.btnUpdateFeeds.show()
486
 
        self.updating_feed_cache = False
487
 
        self.feed_cache_update_cancelled = False
488
 
        self.update_feed_cache(force_update=self.config.update_on_startup)
489
 
 
490
 
        self.message_area = None
491
 
 
492
 
        def find_partial_downloads():
493
 
            # Look for partial file downloads
494
 
            partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
495
 
            count = len(partial_files)
496
 
            resumable_episodes = []
497
 
            if count:
498
 
                if not gpodder.ui.fremantle:
499
 
                    util.idle_add(self.wNotebook.set_current_page, 1)
500
 
                indicator = ProgressIndicator(_('Loading incomplete downloads'), \
501
 
                        _('Some episodes have not finished downloading in a previous session.'), \
502
 
                        False, self.get_dialog_parent())
503
 
                indicator.on_message(N_('%(count)d partial file', '%(count)d partial files', count) % {'count':count})
504
 
 
505
 
                candidates = [f[:-len('.partial')] for f in partial_files]
506
 
                found = 0
507
 
 
508
 
                for c in self.channels:
509
 
                    for e in c.get_all_episodes():
510
 
                        filename = e.local_filename(create=False, check_only=True)
511
 
                        if filename in candidates:
512
 
                            log('Found episode: %s', e.title, sender=self)
513
 
                            found += 1
514
 
                            indicator.on_message(e.title)
515
 
                            indicator.on_progress(float(found)/count)
516
 
                            candidates.remove(filename)
517
 
                            partial_files.remove(filename+'.partial')
518
 
 
519
 
                            if os.path.exists(filename):
520
 
                                # The file has already been downloaded;
521
 
                                # remove the leftover partial file
522
 
                                util.delete_file(filename+'.partial')
523
 
                            else:
524
 
                                resumable_episodes.append(e)
525
 
 
526
 
                        if not candidates:
527
 
                            break
528
 
 
529
 
                    if not candidates:
530
 
                        break
531
 
 
532
 
                for f in partial_files:
533
 
                    log('Partial file without episode: %s', f, sender=self)
534
 
                    util.delete_file(f)
535
 
 
536
 
                util.idle_add(indicator.on_finished)
537
 
 
538
 
                if len(resumable_episodes):
539
 
                    def offer_resuming():
540
 
                        self.download_episode_list_paused(resumable_episodes)
541
 
                        if not gpodder.ui.fremantle:
542
 
                            resume_all = gtk.Button(_('Resume all'))
543
 
                            #resume_all.set_border_width(0)
544
 
                            def on_resume_all(button):
545
 
                                selection = self.treeDownloads.get_selection()
546
 
                                selection.select_all()
547
 
                                selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
548
 
                                selection.unselect_all()
549
 
                                self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
550
 
                                self.message_area.hide()
551
 
                            resume_all.connect('clicked', on_resume_all)
552
 
 
553
 
                            self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
554
 
                            self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
555
 
                            self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
556
 
                            self.message_area.show_all()
557
 
                        self.clean_up_downloads(delete_partial=False)
558
 
                    util.idle_add(offer_resuming)
559
 
                elif not gpodder.ui.fremantle:
560
 
                    util.idle_add(self.wNotebook.set_current_page, 0)
561
 
            else:
562
 
                util.idle_add(self.clean_up_downloads, True)
563
 
        threading.Thread(target=find_partial_downloads).start()
564
 
 
565
 
        # Start the auto-update procedure
566
 
        self._auto_update_timer_source_id = None
567
 
        if self.config.auto_update_feeds:
568
 
            self.restart_auto_update_timer()
569
 
 
570
 
        # Delete old episodes if the user wishes to
571
 
        if self.config.auto_remove_played_episodes and \
572
 
                self.config.episode_old_age > 0:
573
 
            old_episodes = list(self.get_expired_episodes())
574
 
            if len(old_episodes) > 0:
575
 
                self.delete_episode_list(old_episodes, confirm=False)
576
 
                self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
577
 
 
578
 
        if gpodder.ui.fremantle:
579
 
            hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
580
 
            self.button_refresh.set_sensitive(True)
581
 
            self.button_subscribe.set_sensitive(True)
582
 
            self.main_window.set_title(_('gPodder'))
583
 
            hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
584
 
 
585
 
        # Do the initial sync with the web service
586
 
        util.idle_add(self.mygpo_client.flush, True)
587
 
 
588
 
        # First-time users should be asked if they want to see the OPML
589
 
        if not self.channels and not gpodder.ui.fremantle:
590
 
            util.idle_add(self.on_itemUpdate_activate)
591
 
 
592
 
    def episode_object_by_uri(self, uri):
593
 
        """Get an episode object given a local or remote URI
594
 
 
595
 
        This can be used to quickly access an episode object
596
 
        when all we have is its download filename or episode
597
 
        URL (e.g. from external D-Bus calls / signals, etc..)
598
 
        """
599
 
        if uri.startswith('/'):
600
 
            uri = 'file://' + urllib.quote(uri)
601
 
 
602
 
        prefix = 'file://' + urllib.quote(self.config.download_dir)
603
 
 
604
 
        if uri.startswith(prefix):
605
 
            # File is on the local filesystem in the download folder
606
 
            filename = urllib.unquote(uri[len(prefix):])
607
 
            file_parts = [x for x in filename.split(os.sep) if x]
608
 
 
609
 
            if len(file_parts) == 2:
610
 
                dir_name, filename = file_parts
611
 
                channels = [c for c in self.channels if c.foldername == dir_name]
612
 
                if len(channels) == 1:
613
 
                    channel = channels[0]
614
 
                    return channel.get_episode_by_filename(filename)
615
 
        else:
616
 
            # Possibly remote file - search the database for a podcast
617
 
            channel_id = self.db.get_channel_id_from_episode_url(uri)
618
 
 
619
 
            if channel_id is not None:
620
 
                channels = [c for c in self.channels if c.id == channel_id]
621
 
                if len(channels) == 1:
622
 
                    channel = channels[0]
623
 
                    return channel.get_episode_by_url(uri)
624
 
 
625
 
        return None
626
 
 
627
 
    def on_played(self, start, end, total, file_uri):
628
 
        """Handle the "played" signal from a media player"""
629
 
        if start == 0 and end == 0 and total == 0:
630
 
            # Ignore bogus play event
631
 
            return
632
 
        elif end < start + 5:
633
 
            # Ignore "less than five seconds" segments,
634
 
            # as they can happen with seeking, etc...
635
 
            return
636
 
 
637
 
        log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
638
 
        episode = self.episode_object_by_uri(file_uri)
639
 
 
640
 
        if episode is not None:
641
 
            file_type = episode.file_type()
642
 
 
643
 
            now = time.time()
644
 
            if total > 0:
645
 
                episode.total_time = total
646
 
            elif total == 0:
647
 
                # Assume the episode's total time for the action
648
 
                total = episode.total_time
649
 
            if episode.current_position_updated is None or \
650
 
                    now > episode.current_position_updated:
651
 
                episode.current_position = end
652
 
                episode.current_position_updated = now
653
 
            episode.mark(is_played=True)
654
 
            episode.save()
655
 
            self.db.commit()
656
 
            self.update_episode_list_icons([episode.url])
657
 
            self.update_podcast_list_model([episode.channel.url])
658
 
 
659
 
            # Submit this action to the webservice
660
 
            self.mygpo_client.on_playback_full(episode, \
661
 
                    start, end, total)
662
 
 
663
 
    def on_add_remove_podcasts_mygpo(self):
664
 
        actions = self.mygpo_client.get_received_actions()
665
 
        if not actions:
666
 
            return False
667
 
 
668
 
        existing_urls = [c.url for c in self.channels]
669
 
 
670
 
        # Columns for the episode selector window - just one...
671
 
        columns = (
672
 
            ('description', None, None, _('Action')),
673
 
        )
674
 
 
675
 
        # A list of actions that have to be chosen from
676
 
        changes = []
677
 
 
678
 
        # Actions that are ignored (already carried out)
679
 
        ignored = []
680
 
 
681
 
        for action in actions:
682
 
            if action.is_add and action.url not in existing_urls:
683
 
                changes.append(my.Change(action))
684
 
            elif action.is_remove and action.url in existing_urls:
685
 
                podcast_object = None
686
 
                for podcast in self.channels:
687
 
                    if podcast.url == action.url:
688
 
                        podcast_object = podcast
689
 
                        break
690
 
                changes.append(my.Change(action, podcast_object))
691
 
            else:
692
 
                log('Ignoring action: %s', action, sender=self)
693
 
                ignored.append(action)
694
 
 
695
 
        # Confirm all ignored changes
696
 
        self.mygpo_client.confirm_received_actions(ignored)
697
 
 
698
 
        def execute_podcast_actions(selected):
699
 
            add_list = [c.action.url for c in selected if c.action.is_add]
700
 
            remove_list = [c.podcast for c in selected if c.action.is_remove]
701
 
 
702
 
            # Apply the accepted changes locally
703
 
            self.add_podcast_list(add_list)
704
 
            self.remove_podcast_list(remove_list, confirm=False)
705
 
 
706
 
            # All selected items are now confirmed
707
 
            self.mygpo_client.confirm_received_actions(c.action for c in selected)
708
 
 
709
 
            # Revert the changes on the server
710
 
            rejected = [c.action for c in changes if c not in selected]
711
 
            self.mygpo_client.reject_received_actions(rejected)
712
 
 
713
 
        def ask():
714
 
            # We're abusing the Episode Selector again ;) -- thp
715
 
            gPodderEpisodeSelector(self.main_window, \
716
 
                    title=_('Confirm changes from gpodder.net'), \
717
 
                    instructions=_('Select the actions you want to carry out.'), \
718
 
                    episodes=changes, \
719
 
                    columns=columns, \
720
 
                    size_attribute=None, \
721
 
                    stock_ok_button=gtk.STOCK_APPLY, \
722
 
                    callback=execute_podcast_actions, \
723
 
                    _config=self.config)
724
 
 
725
 
        # There are some actions that need the user's attention
726
 
        if changes:
727
 
            util.idle_add(ask)
728
 
            return True
729
 
 
730
 
        # We have no remaining actions - no selection happens
731
 
        return False
732
 
 
733
 
    def rewrite_urls_mygpo(self):
734
 
        # Check if we have to rewrite URLs since the last add
735
 
        rewritten_urls = self.mygpo_client.get_rewritten_urls()
736
 
 
737
 
        for rewritten_url in rewritten_urls:
738
 
            if not rewritten_url.new_url:
739
 
                continue
740
 
 
741
 
            for channel in self.channels:
742
 
                if channel.url == rewritten_url.old_url:
743
 
                    log('Updating URL of %s to %s', channel, \
744
 
                            rewritten_url.new_url, sender=self)
745
 
                    channel.url = rewritten_url.new_url
746
 
                    channel.save()
747
 
                    self.channel_list_changed = True
748
 
                    util.idle_add(self.update_episode_list_model)
749
 
                    break
750
 
 
751
 
    def on_send_full_subscriptions(self):
752
 
        # Send the full subscription list to the gpodder.net client
753
 
        # (this will overwrite the subscription list on the server)
754
 
        indicator = ProgressIndicator(_('Uploading subscriptions'), \
755
 
                _('Your subscriptions are being uploaded to the server.'), \
756
 
                False, self.get_dialog_parent())
757
 
 
758
 
        try:
759
 
            self.mygpo_client.set_subscriptions([c.url for c in self.channels])
760
 
            util.idle_add(self.show_message, _('List uploaded successfully.'))
761
 
        except Exception, e:
762
 
            def show_error(e):
763
 
                message = str(e)
764
 
                if not message:
765
 
                    message = e.__class__.__name__
766
 
                self.show_message(message, \
767
 
                        _('Error while uploading'), \
768
 
                        important=True)
769
 
            util.idle_add(show_error, e)
770
 
 
771
 
        util.idle_add(indicator.on_finished)
772
 
 
773
 
    def on_podcast_selected(self, treeview, path, column):
774
 
        # for Maemo 5's UI
775
 
        model = treeview.get_model()
776
 
        channel = model.get_value(model.get_iter(path), \
777
 
                PodcastListModel.C_CHANNEL)
778
 
        self.active_channel = channel
779
 
        self.update_episode_list_model()
780
 
        self.episodes_window.channel = self.active_channel
781
 
        self.episodes_window.show()
782
 
 
783
 
    def on_button_subscribe_clicked(self, button):
784
 
        self.on_itemImportChannels_activate(button)
785
 
 
786
 
    def on_button_downloads_clicked(self, widget):
787
 
        self.downloads_window.show()
788
 
 
789
 
    def show_episode_in_download_manager(self, episode):
790
 
        self.downloads_window.show()
791
 
        model = self.treeDownloads.get_model()
792
 
        selection = self.treeDownloads.get_selection()
793
 
        selection.unselect_all()
794
 
        it = model.get_iter_first()
795
 
        while it is not None:
796
 
            task = model.get_value(it, DownloadStatusModel.C_TASK)
797
 
            if task.episode.url == episode.url:
798
 
                selection.select_iter(it)
799
 
                # FIXME: Scroll to selection in pannable area
800
 
                break
801
 
            it = model.iter_next(it)
802
 
 
803
 
    def for_each_episode_set_task_status(self, episodes, status):
804
 
        episode_urls = set(episode.url for episode in episodes)
805
 
        model = self.treeDownloads.get_model()
806
 
        selected_tasks = [(gtk.TreeRowReference(model, row.path), \
807
 
                           model.get_value(row.iter, \
808
 
                           DownloadStatusModel.C_TASK)) for row in model \
809
 
                           if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
810
 
                           in episode_urls]
811
 
        self._for_each_task_set_status(selected_tasks, status)
812
 
 
813
 
    def on_window_orientation_changed(self, orientation):
814
 
        self._last_orientation = orientation
815
 
        if self.preferences_dialog is not None:
816
 
            self.preferences_dialog.on_window_orientation_changed(orientation)
817
 
 
818
 
        treeview = self.treeChannels
819
 
        if orientation == Orientation.PORTRAIT:
820
 
            treeview.set_action_area_orientation(gtk.ORIENTATION_VERTICAL)
821
 
            # Work around Maemo bug #4718
822
 
            self.button_subscribe.set_name('HildonButton-thumb')
823
 
            self.button_refresh.set_name('HildonButton-thumb')
824
 
        else:
825
 
            treeview.set_action_area_orientation(gtk.ORIENTATION_HORIZONTAL)
826
 
            # Work around Maemo bug #4718
827
 
            self.button_subscribe.set_name('HildonButton-finger')
828
 
            self.button_refresh.set_name('HildonButton-finger')
829
 
 
830
 
        if gpodder.ui.fremantle:
831
 
            self.fancy_progress_bar.relayout()
832
 
 
833
 
    def on_treeview_podcasts_selection_changed(self, selection):
834
 
        model, iter = selection.get_selected()
835
 
        if iter is None:
836
 
            self.active_channel = None
837
 
            self.episode_list_model.clear()
838
 
 
839
 
    def on_treeview_button_pressed(self, treeview, event):
840
 
        if event.window != treeview.get_bin_window():
841
 
            return False
842
 
 
843
 
        TreeViewHelper.save_button_press_event(treeview, event)
844
 
 
845
 
        if getattr(treeview, TreeViewHelper.ROLE) == \
846
 
                TreeViewHelper.ROLE_PODCASTS:
847
 
            return self.currently_updating
848
 
 
849
 
        return event.button == self.context_menu_mouse_button and \
850
 
                gpodder.ui.desktop
851
 
 
852
 
    def on_treeview_podcasts_button_released(self, treeview, event):
853
 
        if event.window != treeview.get_bin_window():
854
 
            return False
855
 
 
856
 
        if gpodder.ui.maemo:
857
 
            return self.treeview_channels_handle_gestures(treeview, event)
858
 
        return self.treeview_channels_show_context_menu(treeview, event)
859
 
 
860
 
    def on_treeview_episodes_button_released(self, treeview, event):
861
 
        if event.window != treeview.get_bin_window():
862
 
            return False
863
 
 
864
 
        if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
865
 
            return self.treeview_available_handle_gestures(treeview, event)
866
 
 
867
 
        return self.treeview_available_show_context_menu(treeview, event)
868
 
 
869
 
    def on_treeview_downloads_button_released(self, treeview, event):
870
 
        if event.window != treeview.get_bin_window():
871
 
            return False
872
 
 
873
 
        return self.treeview_downloads_show_context_menu(treeview, event)
874
 
 
875
 
    def on_entry_search_podcasts_changed(self, editable):
876
 
        if self.hbox_search_podcasts.get_property('visible'):
877
 
            def set_search_term(self, text):
878
 
                self.podcast_list_model.set_search_term(text)
879
 
                self._podcast_list_search_timeout = None
880
 
                return False
881
 
 
882
 
            if self._podcast_list_search_timeout is not None:
883
 
                gobject.source_remove(self._podcast_list_search_timeout)
884
 
            self._podcast_list_search_timeout = gobject.timeout_add(\
885
 
                    self.LIVE_SEARCH_DELAY, \
886
 
                    set_search_term, self, editable.get_chars(0, -1))
887
 
 
888
 
    def on_entry_search_podcasts_key_press(self, editable, event):
889
 
        if event.keyval == gtk.keysyms.Escape:
890
 
            self.hide_podcast_search()
891
 
            return True
892
 
 
893
 
    def hide_podcast_search(self, *args):
894
 
        if self._podcast_list_search_timeout is not None:
895
 
            gobject.source_remove(self._podcast_list_search_timeout)
896
 
            self._podcast_list_search_timeout = None
897
 
        self.hbox_search_podcasts.hide()
898
 
        self.entry_search_podcasts.set_text('')
899
 
        self.podcast_list_model.set_search_term(None)
900
 
        self.treeChannels.grab_focus()
901
 
 
902
 
    def show_podcast_search(self, input_char):
903
 
        self.hbox_search_podcasts.show()
904
 
        self.entry_search_podcasts.insert_text(input_char, -1)
905
 
        self.entry_search_podcasts.grab_focus()
906
 
        self.entry_search_podcasts.set_position(-1)
907
 
 
908
 
    def init_podcast_list_treeview(self):
909
 
        # Set up podcast channel tree view widget
910
 
        if gpodder.ui.fremantle:
911
 
            if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
912
 
                self.item_view_podcasts_downloaded.set_active(True)
913
 
            elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
914
 
                self.item_view_podcasts_unplayed.set_active(True)
915
 
            else:
916
 
                self.item_view_podcasts_all.set_active(True)
917
 
            self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
918
 
 
919
 
        iconcolumn = gtk.TreeViewColumn('')
920
 
        iconcell = gtk.CellRendererPixbuf()
921
 
        iconcolumn.pack_start(iconcell, False)
922
 
        iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
923
 
        self.treeChannels.append_column(iconcolumn)
924
 
 
925
 
        namecolumn = gtk.TreeViewColumn('')
926
 
        namecell = gtk.CellRendererText()
927
 
        namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
928
 
        namecolumn.pack_start(namecell, True)
929
 
        namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
930
 
 
931
 
        if gpodder.ui.fremantle:
932
 
            countcell = gtk.CellRendererText()
933
 
            from gpodder.gtkui.frmntl import style
934
 
            countcell.set_property('font-desc', style.get_font_desc('EmpSystemFont'))
935
 
            countcell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
936
 
            countcell.set_property('alignment', pango.ALIGN_RIGHT)
937
 
            countcell.set_property('xalign', 1.)
938
 
            countcell.set_property('xpad', 5)
939
 
            namecolumn.pack_start(countcell, False)
940
 
            namecolumn.add_attribute(countcell, 'text', PodcastListModel.C_DOWNLOADS)
941
 
            namecolumn.add_attribute(countcell, 'visible', PodcastListModel.C_DOWNLOADS)
942
 
        else:
943
 
            iconcell = gtk.CellRendererPixbuf()
944
 
            iconcell.set_property('xalign', 1.0)
945
 
            namecolumn.pack_start(iconcell, False)
946
 
            namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
947
 
            namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
948
 
 
949
 
        self.treeChannels.append_column(namecolumn)
950
 
 
951
 
        self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
952
 
 
953
 
        # When no podcast is selected, clear the episode list model
954
 
        selection = self.treeChannels.get_selection()
955
 
        selection.connect('changed', self.on_treeview_podcasts_selection_changed)
956
 
 
957
 
        # Set up type-ahead find for the podcast list
958
 
        def on_key_press(treeview, event):
959
 
            if event.keyval == gtk.keysyms.Escape:
960
 
                self.hide_podcast_search()
961
 
            elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
962
 
                self.hide_podcast_search()
963
 
            elif event.state & gtk.gdk.CONTROL_MASK:
964
 
                # Don't handle type-ahead when control is pressed (so shortcuts
965
 
                # with the Ctrl key still work, e.g. Ctrl+A, ...)
966
 
                return True
967
 
            else:
968
 
                unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
969
 
                if unicode_char_id == 0:
970
 
                    return False
971
 
                input_char = unichr(unicode_char_id)
972
 
                self.show_podcast_search(input_char)
973
 
            return True
974
 
        self.treeChannels.connect('key-press-event', on_key_press)
975
 
 
976
 
        # Enable separators to the podcast list to separate special podcasts
977
 
        # from others (this is used for the "all episodes" view)
978
 
        self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
979
 
 
980
 
        TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
981
 
 
982
 
    def on_entry_search_episodes_changed(self, editable):
983
 
        if self.hbox_search_episodes.get_property('visible'):
984
 
            def set_search_term(self, text):
985
 
                self.episode_list_model.set_search_term(text)
986
 
                self._episode_list_search_timeout = None
987
 
                return False
988
 
 
989
 
            if self._episode_list_search_timeout is not None:
990
 
                gobject.source_remove(self._episode_list_search_timeout)
991
 
            self._episode_list_search_timeout = gobject.timeout_add(\
992
 
                    self.LIVE_SEARCH_DELAY, \
993
 
                    set_search_term, self, editable.get_chars(0, -1))
994
 
 
995
 
    def on_entry_search_episodes_key_press(self, editable, event):
996
 
        if event.keyval == gtk.keysyms.Escape:
997
 
            self.hide_episode_search()
998
 
            return True
999
 
 
1000
 
    def hide_episode_search(self, *args):
1001
 
        if self._episode_list_search_timeout is not None:
1002
 
            gobject.source_remove(self._episode_list_search_timeout)
1003
 
            self._episode_list_search_timeout = None
1004
 
        self.hbox_search_episodes.hide()
1005
 
        self.entry_search_episodes.set_text('')
1006
 
        self.episode_list_model.set_search_term(None)
1007
 
        self.treeAvailable.grab_focus()
1008
 
 
1009
 
    def show_episode_search(self, input_char):
1010
 
        self.hbox_search_episodes.show()
1011
 
        self.entry_search_episodes.insert_text(input_char, -1)
1012
 
        self.entry_search_episodes.grab_focus()
1013
 
        self.entry_search_episodes.set_position(-1)
1014
 
 
1015
 
    def init_episode_list_treeview(self):
1016
 
        # For loading the list model
1017
 
        self.episode_list_model = EpisodeListModel(self.on_episode_list_filter_changed)
1018
 
 
1019
 
        if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
1020
 
            self.item_view_episodes_undeleted.set_active(True)
1021
 
        elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
1022
 
            self.item_view_episodes_downloaded.set_active(True)
1023
 
        elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
1024
 
            self.item_view_episodes_unplayed.set_active(True)
1025
 
        else:
1026
 
            self.item_view_episodes_all.set_active(True)
1027
 
 
1028
 
        self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
1029
 
 
1030
 
        self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
1031
 
 
1032
 
        TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
1033
 
 
1034
 
        iconcell = gtk.CellRendererPixbuf()
1035
 
        iconcell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1036
 
        if gpodder.ui.maemo:
1037
 
            iconcell.set_fixed_size(50, 50)
1038
 
        else:
1039
 
            iconcell.set_fixed_size(40, -1)
1040
 
 
1041
 
        namecell = gtk.CellRendererText()
1042
 
        namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
1043
 
        namecolumn = gtk.TreeViewColumn(_('Episode'))
1044
 
        namecolumn.pack_start(iconcell, False)
1045
 
        namecolumn.add_attribute(iconcell, 'icon-name', EpisodeListModel.C_STATUS_ICON)
1046
 
        namecolumn.pack_start(namecell, True)
1047
 
        namecolumn.add_attribute(namecell, 'markup', EpisodeListModel.C_DESCRIPTION)
1048
 
        if gpodder.ui.fremantle:
1049
 
            namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1050
 
        else:
1051
 
            namecolumn.set_sort_column_id(EpisodeListModel.C_DESCRIPTION)
1052
 
            namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1053
 
            namecolumn.set_resizable(True)
1054
 
        namecolumn.set_expand(True)
1055
 
 
1056
 
        if gpodder.ui.fremantle:
1057
 
            from gpodder.gtkui.frmntl import style
1058
 
            timecell = gtk.CellRendererText()
1059
 
            timecell.set_property('font-desc', style.get_font_desc('SmallSystemFont'))
1060
 
            timecell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
1061
 
            timecell.set_property('alignment', pango.ALIGN_RIGHT)
1062
 
            timecell.set_property('xalign', 1.)
1063
 
            timecell.set_property('xpad', 5)
1064
 
            timecell.set_property('yalign', .85)
1065
 
            namecolumn.pack_start(timecell, False)
1066
 
            namecolumn.add_attribute(timecell, 'text', EpisodeListModel.C_TIME)
1067
 
            namecolumn.add_attribute(timecell, 'visible', EpisodeListModel.C_TIME_VISIBLE)
1068
 
        else:
1069
 
            lockcell = gtk.CellRendererPixbuf()
1070
 
            lockcell.set_fixed_size(40, -1)
1071
 
            lockcell.set_property('stock-size', gtk.ICON_SIZE_MENU)
1072
 
            lockcell.set_property('icon-name', 'emblem-readonly')
1073
 
            namecolumn.pack_start(lockcell, False)
1074
 
            namecolumn.add_attribute(lockcell, 'visible', EpisodeListModel.C_LOCKED)
1075
 
 
1076
 
        sizecell = gtk.CellRendererText()
1077
 
        sizecell.set_property('xalign', 1)
1078
 
        sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
1079
 
        sizecolumn.set_sort_column_id(EpisodeListModel.C_FILESIZE)
1080
 
 
1081
 
        releasecell = gtk.CellRendererText()
1082
 
        releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
1083
 
        releasecolumn.set_sort_column_id(EpisodeListModel.C_PUBLISHED)
1084
 
 
1085
 
        namecolumn.set_reorderable(True)
1086
 
        self.treeAvailable.append_column(namecolumn)
1087
 
 
1088
 
        if not gpodder.ui.maemo:
1089
 
            for itemcolumn in (sizecolumn, releasecolumn):
1090
 
                itemcolumn.set_reorderable(True)
1091
 
                self.treeAvailable.append_column(itemcolumn)
1092
 
 
1093
 
        # Set up type-ahead find for the episode list
1094
 
        def on_key_press(treeview, event):
1095
 
            if event.keyval == gtk.keysyms.Escape:
1096
 
                self.hide_episode_search()
1097
 
            elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
1098
 
                self.hide_episode_search()
1099
 
            elif event.state & gtk.gdk.CONTROL_MASK:
1100
 
                # Don't handle type-ahead when control is pressed (so shortcuts
1101
 
                # with the Ctrl key still work, e.g. Ctrl+A, ...)
1102
 
                return False
1103
 
            else:
1104
 
                unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
1105
 
                if unicode_char_id == 0:
1106
 
                    return False
1107
 
                input_char = unichr(unicode_char_id)
1108
 
                self.show_episode_search(input_char)
1109
 
            return True
1110
 
        self.treeAvailable.connect('key-press-event', on_key_press)
1111
 
 
1112
 
        if gpodder.ui.desktop and not self.config.enable_fingerscroll:
1113
 
            self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
1114
 
                    (('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
1115
 
            def drag_data_get(tree, context, selection_data, info, timestamp):
1116
 
                if self.config.on_drag_mark_played:
1117
 
                    for episode in self.get_selected_episodes():
1118
 
                        episode.mark(is_played=True)
1119
 
                    self.on_selected_episodes_status_changed()
1120
 
                uris = ['file://'+e.local_filename(create=False) \
1121
 
                        for e in self.get_selected_episodes() \
1122
 
                        if e.was_downloaded(and_exists=True)]
1123
 
                uris.append('') # for the trailing '\r\n'
1124
 
                selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
1125
 
            self.treeAvailable.connect('drag-data-get', drag_data_get)
1126
 
 
1127
 
        selection = self.treeAvailable.get_selection()
1128
 
        if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
1129
 
            selection.set_mode(gtk.SELECTION_SINGLE)
1130
 
        elif gpodder.ui.fremantle:
1131
 
            selection.set_mode(gtk.SELECTION_SINGLE)
1132
 
        else:
1133
 
            selection.set_mode(gtk.SELECTION_MULTIPLE)
1134
 
            # Update the sensitivity of the toolbar buttons on the Desktop
1135
 
            selection.connect('changed', lambda s: self.play_or_download())
1136
 
 
1137
 
        if gpodder.ui.diablo:
1138
 
            # Set up the tap-and-hold context menu for podcasts
1139
 
            menu = gtk.Menu()
1140
 
            menu.append(self.itemUpdateChannel.create_menu_item())
1141
 
            menu.append(self.itemEditChannel.create_menu_item())
1142
 
            menu.append(gtk.SeparatorMenuItem())
1143
 
            menu.append(self.itemRemoveChannel.create_menu_item())
1144
 
            menu.append(gtk.SeparatorMenuItem())
1145
 
            item = gtk.ImageMenuItem(_('Close this menu'))
1146
 
            item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
1147
 
                    gtk.ICON_SIZE_MENU))
1148
 
            menu.append(item)
1149
 
            menu.show_all()
1150
 
            menu = self.set_finger_friendly(menu)
1151
 
            self.treeChannels.tap_and_hold_setup(menu)
1152
 
 
1153
 
 
1154
 
    def init_download_list_treeview(self):
1155
 
        # enable multiple selection support
1156
 
        self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1157
 
        self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
1158
 
 
1159
 
        # columns and renderers for "download progress" tab
1160
 
        # First column: [ICON] Episodename
1161
 
        column = gtk.TreeViewColumn(_('Episode'))
1162
 
 
1163
 
        cell = gtk.CellRendererPixbuf()
1164
 
        if gpodder.ui.maemo:
1165
 
            cell.set_fixed_size(50, 50)
1166
 
        cell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1167
 
        column.pack_start(cell, expand=False)
1168
 
        column.add_attribute(cell, 'icon-name', \
1169
 
                DownloadStatusModel.C_ICON_NAME)
1170
 
 
1171
 
        cell = gtk.CellRendererText()
1172
 
        cell.set_property('ellipsize', pango.ELLIPSIZE_END)
1173
 
        column.pack_start(cell, expand=True)
1174
 
        column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
1175
 
        column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1176
 
        column.set_expand(True)
1177
 
        self.treeDownloads.append_column(column)
1178
 
 
1179
 
        # Second column: Progress
1180
 
        cell = gtk.CellRendererProgress()
1181
 
        cell.set_property('yalign', .5)
1182
 
        cell.set_property('ypad', 6)
1183
 
        column = gtk.TreeViewColumn(_('Progress'), cell,
1184
 
                value=DownloadStatusModel.C_PROGRESS, \
1185
 
                text=DownloadStatusModel.C_PROGRESS_TEXT)
1186
 
        column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1187
 
        column.set_expand(False)
1188
 
        self.treeDownloads.append_column(column)
1189
 
        if gpodder.ui.maemo:
1190
 
            column.set_property('min-width', 200)
1191
 
            column.set_property('max-width', 200)
1192
 
        else:
1193
 
            column.set_property('min-width', 150)
1194
 
            column.set_property('max-width', 150)
1195
 
 
1196
 
        self.treeDownloads.set_model(self.download_status_model)
1197
 
        TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
1198
 
 
1199
 
    def on_treeview_expose_event(self, treeview, event):
1200
 
        if event.window == treeview.get_bin_window():
1201
 
            model = treeview.get_model()
1202
 
            if (model is not None and model.get_iter_first() is not None):
1203
 
                return False
1204
 
 
1205
 
            role = getattr(treeview, TreeViewHelper.ROLE, None)
1206
 
            if role is None:
1207
 
                return False
1208
 
 
1209
 
            ctx = event.window.cairo_create()
1210
 
            ctx.rectangle(event.area.x, event.area.y,
1211
 
                    event.area.width, event.area.height)
1212
 
            ctx.clip()
1213
 
 
1214
 
            x, y, width, height, depth = event.window.get_geometry()
1215
 
            progress = None
1216
 
 
1217
 
            if role == TreeViewHelper.ROLE_EPISODES:
1218
 
                if self.currently_updating:
1219
 
                    text = _('Loading episodes')
1220
 
                elif self.config.episode_list_view_mode != \
1221
 
                        EpisodeListModel.VIEW_ALL:
1222
 
                    text = _('No episodes in current view')
1223
 
                else:
1224
 
                    text = _('No episodes available')
1225
 
            elif role == TreeViewHelper.ROLE_PODCASTS:
1226
 
                if self.config.episode_list_view_mode != \
1227
 
                        EpisodeListModel.VIEW_ALL and \
1228
 
                        self.config.podcast_list_hide_boring and \
1229
 
                        len(self.channels) > 0:
1230
 
                    text = _('No podcasts in this view')
1231
 
                else:
1232
 
                    text = _('No subscriptions')
1233
 
            elif role == TreeViewHelper.ROLE_DOWNLOADS:
1234
 
                text = _('No active downloads')
1235
 
            else:
1236
 
                raise Exception('on_treeview_expose_event: unknown role')
1237
 
 
1238
 
            if gpodder.ui.fremantle:
1239
 
                from gpodder.gtkui.frmntl import style
1240
 
                font_desc = style.get_font_desc('LargeSystemFont')
1241
 
            else:
1242
 
                font_desc = None
1243
 
 
1244
 
            draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
1245
 
 
1246
 
        return False
1247
 
 
1248
 
    def enable_download_list_update(self):
1249
 
        if not self.download_list_update_enabled:
1250
 
            self.update_downloads_list()
1251
 
            gobject.timeout_add(1500, self.update_downloads_list)
1252
 
            self.download_list_update_enabled = True
1253
 
 
1254
 
    def cleanup_downloads(self):
1255
 
        model = self.download_status_model
1256
 
 
1257
 
        all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
1258
 
        changed_episode_urls = set()
1259
 
        for row_reference, task in all_tasks:
1260
 
            if task.status in (task.DONE, task.CANCELLED):
1261
 
                model.remove(model.get_iter(row_reference.get_path()))
1262
 
                try:
1263
 
                    # We don't "see" this task anymore - remove it;
1264
 
                    # this is needed, so update_episode_list_icons()
1265
 
                    # below gets the correct list of "seen" tasks
1266
 
                    self.download_tasks_seen.remove(task)
1267
 
                except KeyError, key_error:
1268
 
                    log('Cannot remove task from "seen" list: %s', task, sender=self)
1269
 
                changed_episode_urls.add(task.url)
1270
 
                # Tell the task that it has been removed (so it can clean up)
1271
 
                task.removed_from_list()
1272
 
 
1273
 
        # Tell the podcasts tab to update icons for our removed podcasts
1274
 
        self.update_episode_list_icons(changed_episode_urls)
1275
 
 
1276
 
        # Tell the shownotes window that we have removed the episode
1277
 
        if self.episode_shownotes_window is not None and \
1278
 
                self.episode_shownotes_window.episode is not None and \
1279
 
                self.episode_shownotes_window.episode.url in changed_episode_urls:
1280
 
            self.episode_shownotes_window._download_status_changed(None)
1281
 
 
1282
 
        # Update the downloads list one more time
1283
 
        self.update_downloads_list(can_call_cleanup=False)
1284
 
 
1285
 
    def on_tool_downloads_toggled(self, toolbutton):
1286
 
        if toolbutton.get_active():
1287
 
            self.wNotebook.set_current_page(1)
1288
 
        else:
1289
 
            self.wNotebook.set_current_page(0)
1290
 
 
1291
 
    def add_download_task_monitor(self, monitor):
1292
 
        self.download_task_monitors.add(monitor)
1293
 
        model = self.download_status_model
1294
 
        if model is None:
1295
 
            model = ()
1296
 
        for row in model:
1297
 
            task = row[self.download_status_model.C_TASK]
1298
 
            monitor.task_updated(task)
1299
 
 
1300
 
    def remove_download_task_monitor(self, monitor):
1301
 
        self.download_task_monitors.remove(monitor)
1302
 
 
1303
 
    def update_downloads_list(self, can_call_cleanup=True):
1304
 
        try:
1305
 
            model = self.download_status_model
1306
 
 
1307
 
            downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
1308
 
            total_speed, total_size, done_size = 0, 0, 0
1309
 
 
1310
 
            # Keep a list of all download tasks that we've seen
1311
 
            download_tasks_seen = set()
1312
 
 
1313
 
            # Remember the DownloadTask object for the episode that
1314
 
            # has been opened in the episode shownotes dialog (if any)
1315
 
            if self.episode_shownotes_window is not None:
1316
 
                shownotes_episode = self.episode_shownotes_window.episode
1317
 
                shownotes_task = None
1318
 
            else:
1319
 
                shownotes_episode = None
1320
 
                shownotes_task = None
1321
 
 
1322
 
            # Do not go through the list of the model is not (yet) available
1323
 
            if model is None:
1324
 
                model = ()
1325
 
 
1326
 
            for row in model:
1327
 
                self.download_status_model.request_update(row.iter)
1328
 
 
1329
 
                task = row[self.download_status_model.C_TASK]
1330
 
                speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
1331
 
 
1332
 
                # Let the download task monitors know of changes
1333
 
                for monitor in self.download_task_monitors:
1334
 
                    monitor.task_updated(task)
1335
 
 
1336
 
                total_size += size
1337
 
                done_size += size*progress
1338
 
 
1339
 
                if shownotes_episode is not None and \
1340
 
                        shownotes_episode.url == task.episode.url:
1341
 
                    shownotes_task = task
1342
 
 
1343
 
                download_tasks_seen.add(task)
1344
 
 
1345
 
                if status == download.DownloadTask.DOWNLOADING:
1346
 
                    downloading += 1
1347
 
                    total_speed += speed
1348
 
                elif status == download.DownloadTask.FAILED:
1349
 
                    failed += 1
1350
 
                elif status == download.DownloadTask.DONE:
1351
 
                    finished += 1
1352
 
                elif status == download.DownloadTask.QUEUED:
1353
 
                    queued += 1
1354
 
                elif status == download.DownloadTask.PAUSED:
1355
 
                    paused += 1
1356
 
                else:
1357
 
                    others += 1
1358
 
 
1359
 
            # Remember which tasks we have seen after this run
1360
 
            self.download_tasks_seen = download_tasks_seen
1361
 
 
1362
 
            if gpodder.ui.desktop:
1363
 
                text = [_('Downloads')]
1364
 
                if downloading + failed + queued > 0:
1365
 
                    s = []
1366
 
                    if downloading > 0:
1367
 
                        s.append(N_('%(count)d active', '%(count)d active', downloading) % {'count':downloading})
1368
 
                    if failed > 0:
1369
 
                        s.append(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1370
 
                    if queued > 0:
1371
 
                        s.append(N_('%(count)d queued', '%(count)d queued', queued) % {'count':queued})
1372
 
                    text.append(' (' + ', '.join(s)+')')
1373
 
                self.labelDownloads.set_text(''.join(text))
1374
 
            elif gpodder.ui.diablo:
1375
 
                sum = downloading + failed + finished + queued + paused + others
1376
 
                if sum:
1377
 
                    self.tool_downloads.set_label(_('Downloads (%d)') % sum)
1378
 
                else:
1379
 
                    self.tool_downloads.set_label(_('Downloads'))
1380
 
            elif gpodder.ui.fremantle:
1381
 
                if downloading + queued > 0:
1382
 
                    self.button_downloads.set_value(N_('%(count)d active', '%(count)d active', downloading+queued) % {'count':(downloading+queued)})
1383
 
                elif failed > 0:
1384
 
                    self.button_downloads.set_value(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1385
 
                elif paused > 0:
1386
 
                    self.button_downloads.set_value(N_('%(count)d paused', '%(count)d paused', paused) % {'count':paused})
1387
 
                else:
1388
 
                    self.button_downloads.set_value(_('Idle'))
1389
 
 
1390
 
            title = [self.default_title]
1391
 
 
1392
 
            # We have to update all episodes/channels for which the status has
1393
 
            # changed. Accessing task.status_changed has the side effect of
1394
 
            # re-setting the changed flag, so we need to get the "changed" list
1395
 
            # of tuples first and split it into two lists afterwards
1396
 
            changed = [(task.url, task.podcast_url) for task in \
1397
 
                    self.download_tasks_seen if task.status_changed]
1398
 
            episode_urls = [episode_url for episode_url, channel_url in changed]
1399
 
            channel_urls = [channel_url for episode_url, channel_url in changed]
1400
 
 
1401
 
            count = downloading + queued
1402
 
            if count > 0:
1403
 
                title.append(N_('downloading %(count)d file', 'downloading %(count)d files', count) % {'count':count})
1404
 
 
1405
 
                if total_size > 0:
1406
 
                    percentage = 100.0*done_size/total_size
1407
 
                else:
1408
 
                    percentage = 0.0
1409
 
                total_speed = util.format_filesize(total_speed)
1410
 
                title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
1411
 
                if self.tray_icon is not None:
1412
 
                    # Update the tray icon status and progress bar
1413
 
                    self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
1414
 
                    self.tray_icon.draw_progress_bar(percentage/100.)
1415
 
            else:
1416
 
                if self.tray_icon is not None:
1417
 
                    # Update the tray icon status
1418
 
                    self.tray_icon.set_status()
1419
 
                if gpodder.ui.desktop:
1420
 
                    self.downloads_finished(self.download_tasks_seen)
1421
 
                if gpodder.ui.diablo:
1422
 
                    hildon.hildon_banner_show_information(self.gPodder, '', 'gPodder: %s' % _('All downloads finished'))
1423
 
                log('All downloads have finished.', sender=self)
1424
 
                if self.config.cmd_all_downloads_complete:
1425
 
                    util.run_external_command(self.config.cmd_all_downloads_complete)
1426
 
 
1427
 
                if gpodder.ui.fremantle:
1428
 
                    message = '\n'.join(['%s: %s' % (str(task), \
1429
 
                            task.error_message) for task in self.download_tasks_seen if task.notify_as_failed()])
1430
 
                    if message:
1431
 
                        self.show_message(message, _('Downloads failed'), important=True)
1432
 
 
1433
 
                # Remove finished episodes
1434
 
                if self.config.auto_cleanup_downloads and can_call_cleanup:
1435
 
                    self.cleanup_downloads()
1436
 
 
1437
 
                # Stop updating the download list here
1438
 
                self.download_list_update_enabled = False
1439
 
 
1440
 
            if not gpodder.ui.fremantle:
1441
 
                self.gPodder.set_title(' - '.join(title))
1442
 
 
1443
 
            self.update_episode_list_icons(episode_urls)
1444
 
            if self.episode_shownotes_window is not None:
1445
 
                if (shownotes_task and shownotes_task.url in episode_urls) or \
1446
 
                        shownotes_task != self.episode_shownotes_window.task:
1447
 
                    self.episode_shownotes_window._download_status_changed(shownotes_task)
1448
 
                self.episode_shownotes_window._download_status_progress()
1449
 
            self.play_or_download()
1450
 
            if channel_urls:
1451
 
                self.update_podcast_list_model(channel_urls)
1452
 
 
1453
 
            return self.download_list_update_enabled
1454
 
        except Exception, e:
1455
 
            log('Exception happened while updating download list.', sender=self, traceback=True)
1456
 
            self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
1457
 
            # We return False here, so the update loop won't be called again,
1458
 
            # that's why we require the restart of gPodder in the message.
1459
 
            return False
1460
 
 
1461
 
    def on_config_changed(self, *args):
1462
 
        util.idle_add(self._on_config_changed, *args)
1463
 
 
1464
 
    def _on_config_changed(self, name, old_value, new_value):
1465
 
        if name == 'show_toolbar' and gpodder.ui.desktop:
1466
 
            self.toolbar.set_property('visible', new_value)
1467
 
        elif name == 'episode_list_descriptions':
1468
 
            self.update_episode_list_model()
1469
 
        elif name == 'episode_list_thumbnails':
1470
 
            self.update_episode_list_icons(all=True)
1471
 
        elif name == 'rotation_mode':
1472
 
            self._fremantle_rotation.set_mode(new_value)
1473
 
        elif name in ('auto_update_feeds', 'auto_update_frequency'):
1474
 
            self.restart_auto_update_timer()
1475
 
        elif name == 'podcast_list_view_all':
1476
 
            # Force a update of the podcast list model
1477
 
            self.channel_list_changed = True
1478
 
            if gpodder.ui.fremantle:
1479
 
                hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
1480
 
                while gtk.events_pending():
1481
 
                    gtk.main_iteration(False)
1482
 
            self.update_podcast_list_model()
1483
 
            if gpodder.ui.fremantle:
1484
 
                hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1485
 
 
1486
 
    def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1487
 
        # With get_bin_window, we get the window that contains the rows without
1488
 
        # the header. The Y coordinate of this window will be the height of the
1489
 
        # treeview header. This is the amount we have to subtract from the
1490
 
        # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1491
 
        (x_bin, y_bin) = treeview.get_bin_window().get_position()
1492
 
        y -= x_bin
1493
 
        y -= y_bin
1494
 
        (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1495
 
 
1496
 
        if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or x > 50 or (column is not None and column != treeview.get_columns()[0]):
1497
 
            setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1498
 
            return False
1499
 
 
1500
 
        if path is not None:
1501
 
            model = treeview.get_model()
1502
 
            iter = model.get_iter(path)
1503
 
            role = getattr(treeview, TreeViewHelper.ROLE)
1504
 
 
1505
 
            if role == TreeViewHelper.ROLE_EPISODES:
1506
 
                id = model.get_value(iter, EpisodeListModel.C_URL)
1507
 
            elif role == TreeViewHelper.ROLE_PODCASTS:
1508
 
                id = model.get_value(iter, PodcastListModel.C_URL)
1509
 
 
1510
 
            last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1511
 
            if last_tooltip is not None and last_tooltip != id:
1512
 
                setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1513
 
                return False
1514
 
            setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1515
 
 
1516
 
            if role == TreeViewHelper.ROLE_EPISODES:
1517
 
                description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
1518
 
                if description:
1519
 
                    tooltip.set_text(description)
1520
 
                else:
1521
 
                    return False
1522
 
            elif role == TreeViewHelper.ROLE_PODCASTS:
1523
 
                channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1524
 
                if channel is None:
1525
 
                    return False
1526
 
                channel.request_save_dir_size()
1527
 
                diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1528
 
                error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1529
 
                if error_str:
1530
 
                    error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1531
 
                    error_str = '<span foreground="#ff0000">%s</span>' % error_str
1532
 
                table = gtk.Table(rows=3, columns=3)
1533
 
                table.set_row_spacings(5)
1534
 
                table.set_col_spacings(5)
1535
 
                table.set_border_width(5)
1536
 
 
1537
 
                heading = gtk.Label()
1538
 
                heading.set_alignment(0, 1)
1539
 
                heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1540
 
                table.attach(heading, 0, 1, 0, 1)
1541
 
                size_info = gtk.Label()
1542
 
                size_info.set_alignment(1, 1)
1543
 
                size_info.set_justify(gtk.JUSTIFY_RIGHT)
1544
 
                size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1545
 
                table.attach(size_info, 2, 3, 0, 1)
1546
 
 
1547
 
                table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1548
 
 
1549
 
                if len(channel.description) < 500:
1550
 
                    description = channel.description
1551
 
                else:
1552
 
                    pos = channel.description.find('\n\n')
1553
 
                    if pos == -1 or pos > 500:
1554
 
                        description = channel.description[:498]+'[...]'
1555
 
                    else:
1556
 
                        description = channel.description[:pos]
1557
 
 
1558
 
                description = gtk.Label(description)
1559
 
                if error_str:
1560
 
                    description.set_markup(error_str)
1561
 
                description.set_alignment(0, 0)
1562
 
                description.set_line_wrap(True)
1563
 
                table.attach(description, 0, 3, 2, 3)
1564
 
 
1565
 
                table.show_all()
1566
 
                tooltip.set_custom(table)
1567
 
 
1568
 
            return True
1569
 
 
1570
 
        setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1571
 
        return False
1572
 
 
1573
 
    def treeview_allow_tooltips(self, treeview, allow):
1574
 
        setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1575
 
 
1576
 
    def update_m3u_playlist_clicked(self, widget):
1577
 
        if self.active_channel is not None:
1578
 
            self.active_channel.update_m3u_playlist()
1579
 
            self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1580
 
 
1581
 
    def treeview_handle_context_menu_click(self, treeview, event):
1582
 
        x, y = int(event.x), int(event.y)
1583
 
        path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1584
 
 
1585
 
        selection = treeview.get_selection()
1586
 
        model, paths = selection.get_selected_rows()
1587
 
 
1588
 
        if path is None or (path not in paths and \
1589
 
                event.button == self.context_menu_mouse_button):
1590
 
            # We have right-clicked, but not into the selection,
1591
 
            # assume we don't want to operate on the selection
1592
 
            paths = []
1593
 
 
1594
 
        if path is not None and not paths and \
1595
 
                event.button == self.context_menu_mouse_button:
1596
 
            # No selection or clicked outside selection;
1597
 
            # select the single item where we clicked
1598
 
            treeview.grab_focus()
1599
 
            treeview.set_cursor(path, column, 0)
1600
 
            paths = [path]
1601
 
 
1602
 
        if not paths:
1603
 
            # Unselect any remaining items (clicked elsewhere)
1604
 
            if hasattr(treeview, 'is_rubber_banding_active'):
1605
 
                if not treeview.is_rubber_banding_active():
1606
 
                    selection.unselect_all()
1607
 
            else:
1608
 
                selection.unselect_all()
1609
 
 
1610
 
        return model, paths
1611
 
 
1612
 
    def downloads_list_get_selection(self, model=None, paths=None):
1613
 
        if model is None and paths is None:
1614
 
            selection = self.treeDownloads.get_selection()
1615
 
            model, paths = selection.get_selected_rows()
1616
 
 
1617
 
        can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
1618
 
        selected_tasks = [(gtk.TreeRowReference(model, path), \
1619
 
                           model.get_value(model.get_iter(path), \
1620
 
                           DownloadStatusModel.C_TASK)) for path in paths]
1621
 
 
1622
 
        for row_reference, task in selected_tasks:
1623
 
            if task.status != download.DownloadTask.QUEUED:
1624
 
                can_force = False
1625
 
            if task.status not in (download.DownloadTask.PAUSED, \
1626
 
                    download.DownloadTask.FAILED, \
1627
 
                    download.DownloadTask.CANCELLED):
1628
 
                can_queue = False
1629
 
            if task.status not in (download.DownloadTask.PAUSED, \
1630
 
                    download.DownloadTask.QUEUED, \
1631
 
                    download.DownloadTask.DOWNLOADING, \
1632
 
                    download.DownloadTask.FAILED):
1633
 
                can_cancel = False
1634
 
            if task.status not in (download.DownloadTask.QUEUED, \
1635
 
                    download.DownloadTask.DOWNLOADING):
1636
 
                can_pause = False
1637
 
            if task.status not in (download.DownloadTask.CANCELLED, \
1638
 
                    download.DownloadTask.FAILED, \
1639
 
                    download.DownloadTask.DONE):
1640
 
                can_remove = False
1641
 
 
1642
 
        return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
1643
 
 
1644
 
    def downloads_finished(self, download_tasks_seen):
1645
 
        finished_downloads = [str(task) for task in download_tasks_seen if task.notify_as_finished()]
1646
 
        failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.notify_as_failed()]
1647
 
 
1648
 
        if finished_downloads and failed_downloads:
1649
 
            message = self.format_episode_list(finished_downloads, 5)
1650
 
            message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
1651
 
            message += self.format_episode_list(failed_downloads, 5)
1652
 
            self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
1653
 
        elif finished_downloads:
1654
 
            message = self.format_episode_list(finished_downloads)
1655
 
            self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
1656
 
        elif failed_downloads:
1657
 
            message = self.format_episode_list(failed_downloads)
1658
 
            self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
1659
 
 
1660
 
        # Open torrent files right after download (bug 1029)
1661
 
        if self.config.open_torrent_after_download:
1662
 
            for task in download_tasks_seen:
1663
 
                if task.status != task.DONE:
1664
 
                    continue
1665
 
 
1666
 
                episode = task.episode
1667
 
                if episode.mimetype != 'application/x-bittorrent':
1668
 
                    continue
1669
 
 
1670
 
                self.playback_episodes([episode])
1671
 
 
1672
 
 
1673
 
    def format_episode_list(self, episode_list, max_episodes=10):
1674
 
        """
1675
 
        Format a list of episode names for notifications
1676
 
 
1677
 
        Will truncate long episode names and limit the amount of
1678
 
        episodes displayed (max_episodes=10).
1679
 
 
1680
 
        The episode_list parameter should be a list of strings.
1681
 
        """
1682
 
        MAX_TITLE_LENGTH = 100
1683
 
 
1684
 
        result = []
1685
 
        for title in episode_list[:min(len(episode_list), max_episodes)]:
1686
 
            if len(title) > MAX_TITLE_LENGTH:
1687
 
                middle = (MAX_TITLE_LENGTH/2)-2
1688
 
                title = '%s...%s' % (title[0:middle], title[-middle:])
1689
 
            result.append(saxutils.escape(title))
1690
 
            result.append('\n')
1691
 
 
1692
 
        more_episodes = len(episode_list) - max_episodes
1693
 
        if more_episodes > 0:
1694
 
            result.append('(...')
1695
 
            result.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes) % {'count':more_episodes})
1696
 
            result.append('...)')
1697
 
 
1698
 
        return (''.join(result)).strip()
1699
 
 
1700
 
    def _for_each_task_set_status(self, tasks, status, force_start=False):
1701
 
        episode_urls = set()
1702
 
        model = self.treeDownloads.get_model()
1703
 
        for row_reference, task in tasks:
1704
 
            if status == download.DownloadTask.QUEUED:
1705
 
                # Only queue task when its paused/failed/cancelled (or forced)
1706
 
                if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
1707
 
                    self.download_queue_manager.add_task(task, force_start)
1708
 
                    self.enable_download_list_update()
1709
 
            elif status == download.DownloadTask.CANCELLED:
1710
 
                # Cancelling a download allowed when downloading/queued
1711
 
                if task.status in (task.QUEUED, task.DOWNLOADING):
1712
 
                    task.status = status
1713
 
                # Cancelling paused/failed downloads requires a call to .run()
1714
 
                elif task.status in (task.PAUSED, task.FAILED):
1715
 
                    task.status = status
1716
 
                    # Call run, so the partial file gets deleted
1717
 
                    task.run()
1718
 
            elif status == download.DownloadTask.PAUSED:
1719
 
                # Pausing a download only when queued/downloading
1720
 
                if task.status in (task.DOWNLOADING, task.QUEUED):
1721
 
                    task.status = status
1722
 
            elif status is None:
1723
 
                # Remove the selected task - cancel downloading/queued tasks
1724
 
                if task.status in (task.QUEUED, task.DOWNLOADING):
1725
 
                    task.status = task.CANCELLED
1726
 
                model.remove(model.get_iter(row_reference.get_path()))
1727
 
                # Remember the URL, so we can tell the UI to update
1728
 
                try:
1729
 
                    # We don't "see" this task anymore - remove it;
1730
 
                    # this is needed, so update_episode_list_icons()
1731
 
                    # below gets the correct list of "seen" tasks
1732
 
                    self.download_tasks_seen.remove(task)
1733
 
                except KeyError, key_error:
1734
 
                    log('Cannot remove task from "seen" list: %s', task, sender=self)
1735
 
                episode_urls.add(task.url)
1736
 
                # Tell the task that it has been removed (so it can clean up)
1737
 
                task.removed_from_list()
1738
 
            else:
1739
 
                # We can (hopefully) simply set the task status here
1740
 
                task.status = status
1741
 
        # Tell the podcasts tab to update icons for our removed podcasts
1742
 
        self.update_episode_list_icons(episode_urls)
1743
 
        # Update the tab title and downloads list
1744
 
        self.update_downloads_list()
1745
 
 
1746
 
    def treeview_downloads_show_context_menu(self, treeview, event):
1747
 
        model, paths = self.treeview_handle_context_menu_click(treeview, event)
1748
 
        if not paths:
1749
 
            if not hasattr(treeview, 'is_rubber_banding_active'):
1750
 
                return True
1751
 
            else:
1752
 
                return not treeview.is_rubber_banding_active()
1753
 
 
1754
 
        if event.button == self.context_menu_mouse_button:
1755
 
            selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
1756
 
                    self.downloads_list_get_selection(model, paths)
1757
 
 
1758
 
            def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
1759
 
                # This creates a menu item for selection-wide actions
1760
 
                item = gtk.ImageMenuItem(label)
1761
 
                item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1762
 
                item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
1763
 
                item.set_sensitive(sensitive)
1764
 
                return self.set_finger_friendly(item)
1765
 
 
1766
 
            menu = gtk.Menu()
1767
 
 
1768
 
            item = gtk.ImageMenuItem(_('Episode details'))
1769
 
            item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1770
 
            if len(selected_tasks) == 1:
1771
 
                row_reference, task = selected_tasks[0]
1772
 
                episode = task.episode
1773
 
                item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1774
 
            else:
1775
 
                item.set_sensitive(False)
1776
 
            menu.append(self.set_finger_friendly(item))
1777
 
            menu.append(gtk.SeparatorMenuItem())
1778
 
            if can_force:
1779
 
                menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
1780
 
            else:
1781
 
                menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
1782
 
            menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1783
 
            menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1784
 
            menu.append(gtk.SeparatorMenuItem())
1785
 
            menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1786
 
 
1787
 
            if gpodder.ui.maemo or self.config.enable_fingerscroll:
1788
 
                # Because we open the popup on left-click for Maemo,
1789
 
                # we also include a non-action to close the menu
1790
 
                menu.append(gtk.SeparatorMenuItem())
1791
 
                item = gtk.ImageMenuItem(_('Close this menu'))
1792
 
                item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1793
 
 
1794
 
                menu.append(self.set_finger_friendly(item))
1795
 
 
1796
 
            menu.show_all()
1797
 
            menu.popup(None, None, None, event.button, event.time)
1798
 
            return True
1799
 
 
1800
 
    def treeview_channels_show_context_menu(self, treeview, event):
1801
 
        model, paths = self.treeview_handle_context_menu_click(treeview, event)
1802
 
        if not paths:
1803
 
            return True
1804
 
 
1805
 
        # Check for valid channel id, if there's no id then
1806
 
        # assume that it is a proxy channel or equivalent
1807
 
        # and cannot be operated with right click
1808
 
        if self.active_channel.id is None:
1809
 
            return True
1810
 
 
1811
 
        if event.button == 3:
1812
 
            menu = gtk.Menu()
1813
 
 
1814
 
            ICON = lambda x: x
1815
 
 
1816
 
            item = gtk.ImageMenuItem( _('Update podcast'))
1817
 
            item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1818
 
            item.connect('activate', self.on_itemUpdateChannel_activate)
1819
 
            item.set_sensitive(not self.updating_feed_cache)
1820
 
            menu.append(item)
1821
 
 
1822
 
            menu.append(gtk.SeparatorMenuItem())
1823
 
 
1824
 
            item = gtk.CheckMenuItem(_('Keep episodes'))
1825
 
            item.set_active(self.active_channel.channel_is_locked)
1826
 
            item.connect('activate', self.on_channel_toggle_lock_activate)
1827
 
            menu.append(self.set_finger_friendly(item))
1828
 
 
1829
 
            item = gtk.ImageMenuItem(_('Remove podcast'))
1830
 
            item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1831
 
            item.connect( 'activate', self.on_itemRemoveChannel_activate)
1832
 
            menu.append( item)
1833
 
 
1834
 
            if self.config.device_type != 'none':
1835
 
                item = gtk.MenuItem(_('Synchronize to device'))
1836
 
                item.connect('activate', lambda item: self.on_sync_to_ipod_activate(item, self.active_channel.get_downloaded_episodes(), force_played=False))
1837
 
                menu.append(item)
1838
 
 
1839
 
            menu.append( gtk.SeparatorMenuItem())
1840
 
 
1841
 
            item = gtk.ImageMenuItem(_('Podcast details'))
1842
 
            item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1843
 
            item.connect('activate', self.on_itemEditChannel_activate)
1844
 
            menu.append(item)
1845
 
 
1846
 
            menu.show_all()
1847
 
            # Disable tooltips while we are showing the menu, so 
1848
 
            # the tooltip will not appear over the menu
1849
 
            self.treeview_allow_tooltips(self.treeChannels, False)
1850
 
            menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1851
 
            menu.popup( None, None, None, event.button, event.time)
1852
 
 
1853
 
            return True
1854
 
 
1855
 
    def on_itemClose_activate(self, widget):
1856
 
        if self.tray_icon is not None:
1857
 
            self.iconify_main_window()
1858
 
        else:
1859
 
            self.on_gPodder_delete_event(widget)
1860
 
 
1861
 
    def cover_file_removed(self, channel_url):
1862
 
        """
1863
 
        The Cover Downloader calls this when a previously-
1864
 
        available cover has been removed from the disk. We
1865
 
        have to update our model to reflect this change.
1866
 
        """
1867
 
        self.podcast_list_model.delete_cover_by_url(channel_url)
1868
 
    
1869
 
    def cover_download_finished(self, channel, pixbuf):
1870
 
        """
1871
 
        The Cover Downloader calls this when it has finished
1872
 
        downloading (or registering, if already downloaded)
1873
 
        a new channel cover, which is ready for displaying.
1874
 
        """
1875
 
        self.podcast_list_model.add_cover_by_channel(channel, pixbuf)
1876
 
 
1877
 
    def save_episodes_as_file(self, episodes):
1878
 
        for episode in episodes:
1879
 
            self.save_episode_as_file(episode)
1880
 
 
1881
 
    def save_episode_as_file(self, episode):
1882
 
        PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1883
 
        if episode.was_downloaded(and_exists=True):
1884
 
            folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1885
 
            copy_from = episode.local_filename(create=False)
1886
 
            assert copy_from is not None
1887
 
            copy_to = util.sanitize_filename(episode.sync_filename(\
1888
 
                    self.config.custom_sync_name_enabled, \
1889
 
                    self.config.custom_sync_name))
1890
 
            (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1891
 
            setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1892
 
 
1893
 
    def copy_episodes_bluetooth(self, episodes):
1894
 
        episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1895
 
 
1896
 
        if gpodder.ui.maemo:
1897
 
            util.bluetooth_send_files_maemo([e.local_filename(create=False) \
1898
 
                    for e in episodes_to_copy])
1899
 
            return True
1900
 
 
1901
 
        def convert_and_send_thread(episode):
1902
 
            for episode in episodes:
1903
 
                filename = episode.local_filename(create=False)
1904
 
                assert filename is not None
1905
 
                destfile = os.path.join(tempfile.gettempdir(), \
1906
 
                        util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1907
 
                (base, ext) = os.path.splitext(filename)
1908
 
                if not destfile.endswith(ext):
1909
 
                    destfile += ext
1910
 
 
1911
 
                try:
1912
 
                    shutil.copyfile(filename, destfile)
1913
 
                    util.bluetooth_send_file(destfile)
1914
 
                except:
1915
 
                    log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1916
 
                    self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1917
 
 
1918
 
                util.delete_file(destfile)
1919
 
 
1920
 
        threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1921
 
 
1922
 
    def get_device_name(self):
1923
 
        if self.config.device_type == 'ipod':
1924
 
            return _('iPod')
1925
 
        elif self.config.device_type in ('filesystem', 'mtp'):
1926
 
            return _('MP3 player')
1927
 
        else:
1928
 
            return '(unknown device)'
1929
 
 
1930
 
    def _treeview_button_released(self, treeview, event):
1931
 
        xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1932
 
        dy = int(abs(event.y-ypos))
1933
 
        dx = int(event.x-xpos)
1934
 
 
1935
 
        selection = treeview.get_selection()
1936
 
        path = treeview.get_path_at_pos(int(event.x), int(event.y))
1937
 
        if path is None or dy > 30:
1938
 
            return (False, dx, dy)
1939
 
 
1940
 
        path, column, x, y = path
1941
 
        selection.select_path(path)
1942
 
        treeview.set_cursor(path)
1943
 
        treeview.grab_focus()
1944
 
 
1945
 
        return (True, dx, dy)
1946
 
 
1947
 
    def treeview_channels_handle_gestures(self, treeview, event):
1948
 
        if self.currently_updating:
1949
 
            return False
1950
 
 
1951
 
        selected, dx, dy = self._treeview_button_released(treeview, event)
1952
 
 
1953
 
        if selected:
1954
 
            if self.config.maemo_enable_gestures:
1955
 
                if dx > 70:
1956
 
                    self.on_itemUpdateChannel_activate()
1957
 
                elif dx < -70:
1958
 
                    self.on_itemEditChannel_activate(treeview)
1959
 
 
1960
 
        return False
1961
 
 
1962
 
    def treeview_available_handle_gestures(self, treeview, event):
1963
 
        selected, dx, dy = self._treeview_button_released(treeview, event)
1964
 
 
1965
 
        if selected:
1966
 
            if self.config.maemo_enable_gestures:
1967
 
                if dx > 70:
1968
 
                    self.on_playback_selected_episodes(None)
1969
 
                    return True
1970
 
                elif dx < -70:
1971
 
                    self.on_shownotes_selected_episodes(None)
1972
 
                    return True
1973
 
 
1974
 
            # Pass the event to the context menu handler for treeAvailable
1975
 
            self.treeview_available_show_context_menu(treeview, event)
1976
 
 
1977
 
        return True
1978
 
 
1979
 
    def treeview_available_show_context_menu(self, treeview, event):
1980
 
        model, paths = self.treeview_handle_context_menu_click(treeview, event)
1981
 
        if not paths:
1982
 
            if not hasattr(treeview, 'is_rubber_banding_active'):
1983
 
                return True
1984
 
            else:
1985
 
                return not treeview.is_rubber_banding_active()
1986
 
 
1987
 
        if event.button == self.context_menu_mouse_button:
1988
 
            episodes = self.get_selected_episodes()
1989
 
            any_locked = any(e.is_locked for e in episodes)
1990
 
            any_played = any(e.is_played for e in episodes)
1991
 
            one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1992
 
            downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
1993
 
            downloading = any(self.episode_is_downloading(e) for e in episodes)
1994
 
 
1995
 
            menu = gtk.Menu()
1996
 
 
1997
 
            (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1998
 
 
1999
 
            if open_instead_of_play:
2000
 
                item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
2001
 
            elif downloaded:
2002
 
                item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
2003
 
            else:
2004
 
                item = gtk.ImageMenuItem(_('Stream'))
2005
 
                item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
2006
 
 
2007
 
            item.set_sensitive(can_play and not downloading)
2008
 
            item.connect('activate', self.on_playback_selected_episodes)
2009
 
            menu.append(self.set_finger_friendly(item))
2010
 
 
2011
 
            if not can_cancel:
2012
 
                item = gtk.ImageMenuItem(_('Download'))
2013
 
                item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
2014
 
                item.set_sensitive(can_download)
2015
 
                item.connect('activate', self.on_download_selected_episodes)
2016
 
                menu.append(self.set_finger_friendly(item))
2017
 
            else:
2018
 
                item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
2019
 
                item.connect('activate', self.on_item_cancel_download_activate)
2020
 
                menu.append(self.set_finger_friendly(item))
2021
 
 
2022
 
            item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
2023
 
            item.set_sensitive(can_delete)
2024
 
            item.connect('activate', self.on_btnDownloadedDelete_clicked)
2025
 
            menu.append(self.set_finger_friendly(item))
2026
 
 
2027
 
            ICON = lambda x: x
2028
 
 
2029
 
            # Ok, this probably makes sense to only display for downloaded files
2030
 
            if downloaded:
2031
 
                menu.append(gtk.SeparatorMenuItem())
2032
 
                share_item = gtk.MenuItem(_('Send to'))
2033
 
                menu.append(self.set_finger_friendly(share_item))
2034
 
                share_menu = gtk.Menu()
2035
 
 
2036
 
                item = gtk.ImageMenuItem(_('Local folder'))
2037
 
                item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
2038
 
                item.connect('button-press-event', lambda w, ee: self.save_episodes_as_file(episodes))
2039
 
                share_menu.append(self.set_finger_friendly(item))
2040
 
                if self.bluetooth_available:
2041
 
                    item = gtk.ImageMenuItem(_('Bluetooth device'))
2042
 
                    if gpodder.ui.maemo:
2043
 
                        icon_name = ICON('qgn_list_filesys_bluetooth')
2044
 
                    else:
2045
 
                        icon_name = ICON('bluetooth')
2046
 
                    item.set_image(gtk.image_new_from_icon_name(icon_name, gtk.ICON_SIZE_MENU))
2047
 
                    item.connect('button-press-event', lambda w, ee: self.copy_episodes_bluetooth(episodes))
2048
 
                    share_menu.append(self.set_finger_friendly(item))
2049
 
                if can_transfer:
2050
 
                    item = gtk.ImageMenuItem(self.get_device_name())
2051
 
                    item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
2052
 
                    item.connect('button-press-event', lambda w, ee: self.on_sync_to_ipod_activate(w, episodes))
2053
 
                    share_menu.append(self.set_finger_friendly(item))
2054
 
 
2055
 
                share_item.set_submenu(share_menu)
2056
 
 
2057
 
            if (downloaded or one_is_new or can_download) and not downloading:
2058
 
                menu.append(gtk.SeparatorMenuItem())
2059
 
                if one_is_new:
2060
 
                    item = gtk.CheckMenuItem(_('New'))
2061
 
                    item.set_active(True)
2062
 
                    item.connect('activate', lambda w: self.mark_selected_episodes_old())
2063
 
                    menu.append(self.set_finger_friendly(item))
2064
 
                elif can_download:
2065
 
                    item = gtk.CheckMenuItem(_('New'))
2066
 
                    item.set_active(False)
2067
 
                    item.connect('activate', lambda w: self.mark_selected_episodes_new())
2068
 
                    menu.append(self.set_finger_friendly(item))
2069
 
 
2070
 
                if downloaded:
2071
 
                    item = gtk.CheckMenuItem(_('Played'))
2072
 
                    item.set_active(any_played)
2073
 
                    item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, not any_played))
2074
 
                    menu.append(self.set_finger_friendly(item))
2075
 
 
2076
 
                    item = gtk.CheckMenuItem(_('Keep episode'))
2077
 
                    item.set_active(any_locked)
2078
 
                    item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
2079
 
                    menu.append(self.set_finger_friendly(item))
2080
 
 
2081
 
            menu.append(gtk.SeparatorMenuItem())
2082
 
            # Single item, add episode information menu item
2083
 
            item = gtk.ImageMenuItem(_('Episode details'))
2084
 
            item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
2085
 
            item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
2086
 
            menu.append(self.set_finger_friendly(item))
2087
 
 
2088
 
            if gpodder.ui.maemo or self.config.enable_fingerscroll:
2089
 
                # Because we open the popup on left-click for Maemo,
2090
 
                # we also include a non-action to close the menu
2091
 
                menu.append(gtk.SeparatorMenuItem())
2092
 
                item = gtk.ImageMenuItem(_('Close this menu'))
2093
 
                item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
2094
 
                menu.append(self.set_finger_friendly(item))
2095
 
 
2096
 
            menu.show_all()
2097
 
            # Disable tooltips while we are showing the menu, so 
2098
 
            # the tooltip will not appear over the menu
2099
 
            self.treeview_allow_tooltips(self.treeAvailable, False)
2100
 
            menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
2101
 
            menu.popup( None, None, None, event.button, event.time)
2102
 
 
2103
 
            return True
2104
 
 
2105
 
    def set_title(self, new_title):
2106
 
        if not gpodder.ui.fremantle:
2107
 
            self.default_title = new_title
2108
 
            self.gPodder.set_title(new_title)
2109
 
 
2110
 
    def update_episode_list_icons(self, urls=None, selected=False, all=False):
2111
 
        """
2112
 
        Updates the status icons in the episode list.
2113
 
 
2114
 
        If urls is given, it should be a list of URLs
2115
 
        of episodes that should be updated.
2116
 
 
2117
 
        If urls is None, set ONE OF selected, all to
2118
 
        True (the former updates just the selected
2119
 
        episodes and the latter updates all episodes).
2120
 
        """
2121
 
        additional_args = (self.episode_is_downloading, \
2122
 
                self.config.episode_list_descriptions and gpodder.ui.desktop, \
2123
 
                self.config.episode_list_thumbnails and gpodder.ui.desktop)
2124
 
 
2125
 
        if urls is not None:
2126
 
            # We have a list of URLs to walk through
2127
 
            self.episode_list_model.update_by_urls(urls, *additional_args)
2128
 
        elif selected and not all:
2129
 
            # We should update all selected episodes
2130
 
            selection = self.treeAvailable.get_selection()
2131
 
            model, paths = selection.get_selected_rows()
2132
 
            for path in reversed(paths):
2133
 
                iter = model.get_iter(path)
2134
 
                self.episode_list_model.update_by_filter_iter(iter, \
2135
 
                        *additional_args)
2136
 
        elif all and not selected:
2137
 
            # We update all (even the filter-hidden) episodes
2138
 
            self.episode_list_model.update_all(*additional_args)
2139
 
        else:
2140
 
            # Wrong/invalid call - have to specify at least one parameter
2141
 
            raise ValueError('Invalid call to update_episode_list_icons')
2142
 
 
2143
 
    def episode_list_status_changed(self, episodes):
2144
 
        self.update_episode_list_icons(set(e.url for e in episodes))
2145
 
        self.update_podcast_list_model(set(e.channel.url for e in episodes))
2146
 
        self.db.commit()
2147
 
 
2148
 
    def clean_up_downloads(self, delete_partial=False):
2149
 
        # Clean up temporary files left behind by old gPodder versions
2150
 
        temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
2151
 
 
2152
 
        if delete_partial:
2153
 
            temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
2154
 
 
2155
 
        for tempfile in temporary_files:
2156
 
            util.delete_file(tempfile)
2157
 
 
2158
 
        # Clean up empty download folders and abandoned download folders
2159
 
        download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
2160
 
        for ddir in download_dirs:
2161
 
            if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2162
 
                globr = glob.glob(os.path.join(ddir, '*'))
2163
 
                if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
2164
 
                    log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
2165
 
                    shutil.rmtree(ddir, ignore_errors=True)
2166
 
 
2167
 
    def streaming_possible(self):
2168
 
        if gpodder.ui.desktop:
2169
 
            # User has to have a media player set on the Desktop, or else we
2170
 
            # would probably open the browser when giving a URL to xdg-open..
2171
 
            return (self.config.player and self.config.player != 'default')
2172
 
        elif gpodder.ui.maemo:
2173
 
            # On Maemo, the default is to use the Nokia Media Player, which is
2174
 
            # already able to deal with HTTP URLs the right way, so we
2175
 
            # unconditionally enable streaming always on Maemo
2176
 
            return True
2177
 
 
2178
 
        return False
2179
 
 
2180
 
    def playback_episodes_for_real(self, episodes):
2181
 
        groups = collections.defaultdict(list)
2182
 
        for episode in episodes:
2183
 
            file_type = episode.file_type()
2184
 
            if file_type == 'video' and self.config.videoplayer and \
2185
 
                    self.config.videoplayer != 'default':
2186
 
                player = self.config.videoplayer
2187
 
                if gpodder.ui.diablo:
2188
 
                    # Use the wrapper script if it's installed to crop 3GP YouTube
2189
 
                    # videos to fit the screen (looks much nicer than w/ black border)
2190
 
                    if player == 'mplayer' and util.find_command('gpodder-mplayer'):
2191
 
                        player = 'gpodder-mplayer'
2192
 
                elif gpodder.ui.fremantle and player == 'mplayer':
2193
 
                    player = 'mplayer -fs %F'
2194
 
            elif file_type == 'audio' and self.config.player and \
2195
 
                    self.config.player != 'default':
2196
 
                player = self.config.player
2197
 
            else:
2198
 
                player = 'default'
2199
 
 
2200
 
            # Mark episode as played in the database
2201
 
            episode.mark(is_played=True)
2202
 
            self.mygpo_client.on_playback([episode])
2203
 
 
2204
 
            filename = episode.local_filename(create=False)
2205
 
            if filename is None or not os.path.exists(filename):
2206
 
                filename = episode.url
2207
 
                if youtube.is_video_link(filename):
2208
 
                    fmt_id = self.config.youtube_preferred_fmt_id
2209
 
                    if gpodder.ui.fremantle:
2210
 
                        fmt_id = 5
2211
 
                    filename = youtube.get_real_download_url(filename, fmt_id)
2212
 
 
2213
 
            # Determine the playback resume position - if the file
2214
 
            # was played 100%, we simply start from the beginning
2215
 
            resume_position = episode.current_position
2216
 
            if resume_position == episode.total_time:
2217
 
                resume_position = 0
2218
 
 
2219
 
            # Only on Maemo 5, and only if the episode isn't finished yet
2220
 
            if gpodder.ui.fremantle and not episode.is_finished():
2221
 
                self.mafw_monitor.set_resume_point(filename, resume_position)
2222
 
 
2223
 
            # If Panucci is configured, use D-Bus on Maemo to call it
2224
 
            if player == 'panucci':
2225
 
                try:
2226
 
                    PANUCCI_NAME = 'org.panucci.panucciInterface'
2227
 
                    PANUCCI_PATH = '/panucciInterface'
2228
 
                    PANUCCI_INTF = 'org.panucci.panucciInterface'
2229
 
                    o = gpodder.dbus_session_bus.get_object(PANUCCI_NAME, PANUCCI_PATH)
2230
 
                    i = dbus.Interface(o, PANUCCI_INTF)
2231
 
 
2232
 
                    def on_reply(*args):
2233
 
                        pass
2234
 
 
2235
 
                    def error_handler(filename, err):
2236
 
                        log('Exception in D-Bus call: %s', str(err), \
2237
 
                                sender=self)
2238
 
 
2239
 
                        # Fallback: use the command line client
2240
 
                        for command in util.format_desktop_command('panucci', \
2241
 
                                [filename]):
2242
 
                            log('Executing: %s', repr(command), sender=self)
2243
 
                            subprocess.Popen(command)
2244
 
 
2245
 
                    on_error = lambda err: error_handler(filename, err)
2246
 
 
2247
 
                    # This method only exists in Panucci > 0.9 ('new Panucci')
2248
 
                    i.playback_from(filename, resume_position, \
2249
 
                            reply_handler=on_reply, error_handler=on_error)
2250
 
 
2251
 
                    continue # This file was handled by the D-Bus call
2252
 
                except Exception, e:
2253
 
                    log('Error calling Panucci using D-Bus', sender=self, traceback=True)
2254
 
            elif player == 'MediaBox' and gpodder.ui.maemo:
2255
 
                try:
2256
 
                    MEDIABOX_NAME = 'de.pycage.mediabox'
2257
 
                    MEDIABOX_PATH = '/de/pycage/mediabox/control'
2258
 
                    MEDIABOX_INTF = 'de.pycage.mediabox.control'
2259
 
                    o = gpodder.dbus_session_bus.get_object(MEDIABOX_NAME, MEDIABOX_PATH)
2260
 
                    i = dbus.Interface(o, MEDIABOX_INTF)
2261
 
 
2262
 
                    def on_reply(*args):
2263
 
                        pass
2264
 
 
2265
 
                    def on_error(err):
2266
 
                        log('Exception in D-Bus call: %s', str(err), \
2267
 
                                sender=self)
2268
 
 
2269
 
                    i.load(filename, '%s/x-unknown' % file_type, \
2270
 
                            reply_handler=on_reply, error_handler=on_error)
2271
 
 
2272
 
                    continue # This file was handled by the D-Bus call
2273
 
                except Exception, e:
2274
 
                    log('Error calling MediaBox using D-Bus', sender=self, traceback=True)
2275
 
 
2276
 
            groups[player].append(filename)
2277
 
 
2278
 
        # Open episodes with system default player
2279
 
        if 'default' in groups:
2280
 
            # Special-casing for a single episode when the object is a PDF
2281
 
            # file - this is needed on Maemo 5, so we only use gui_open()
2282
 
            # for single PDF files, but still use the built-in media player
2283
 
            # with an M3U file for single audio/video files. (The Maemo 5
2284
 
            # media player behaves differently when opening a single-file
2285
 
            # M3U playlist compared to opening the single file directly.)
2286
 
            if len(groups['default']) == 1:
2287
 
                fn = groups['default'][0]
2288
 
                # The list of extensions is taken from gui_open in util.py
2289
 
                # where all special-cases of Maemo apps are listed
2290
 
                for extension in ('.pdf', '.jpg', '.jpeg', '.png'):
2291
 
                    if fn.lower().endswith(extension):
2292
 
                        util.gui_open(fn)
2293
 
                        groups['default'] = []
2294
 
                        break
2295
 
 
2296
 
            if gpodder.ui.maemo and groups['default']:
2297
 
                # The Nokia Media Player app does not support receiving multiple
2298
 
                # file names via D-Bus, so we simply place all file names into a
2299
 
                # temporary M3U playlist and open that with the Media Player.
2300
 
                m3u_filename = os.path.join(gpodder.home, 'gpodder_open_with.m3u')
2301
 
 
2302
 
                def to_url(x):
2303
 
                    # Diablo's Player hates file:// URLs (Maemo bug 11647)
2304
 
                    if gpodder.ui.diablo:
2305
 
                        return x
2306
 
 
2307
 
                    if '://' not in x:
2308
 
                        return 'file://' + urllib.quote(os.path.abspath(x))
2309
 
                    return x
2310
 
 
2311
 
                util.write_m3u_playlist(m3u_filename, \
2312
 
                        map(to_url, groups['default']), \
2313
 
                        extm3u=False)
2314
 
                util.gui_open(m3u_filename)
2315
 
            else:
2316
 
                for filename in groups['default']:
2317
 
                    log('Opening with system default: %s', filename, sender=self)
2318
 
                    util.gui_open(filename)
2319
 
            del groups['default']
2320
 
        elif gpodder.ui.maemo and groups:
2321
 
            # When on Maemo and not opening with default, show a notification
2322
 
            # (no startup notification for Panucci / MPlayer yet...)
2323
 
            if len(episodes) == 1:
2324
 
                text = _('Opening %s') % episodes[0].title
2325
 
            else:
2326
 
                count = len(episodes)
2327
 
                text = N_('Opening %(count)d episode', 'Opening %(count)d episodes', count) % {'count':count}
2328
 
 
2329
 
            banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
2330
 
 
2331
 
            def destroy_banner_later(banner):
2332
 
                banner.destroy()
2333
 
                return False
2334
 
            gobject.timeout_add(5000, destroy_banner_later, banner)
2335
 
 
2336
 
        # For each type now, go and create play commands
2337
 
        for group in groups:
2338
 
            for command in util.format_desktop_command(group, groups[group]):
2339
 
                log('Executing: %s', repr(command), sender=self)
2340
 
                subprocess.Popen(command)
2341
 
 
2342
 
        # Persist episode status changes to the database
2343
 
        self.db.commit()
2344
 
 
2345
 
        # Flush updated episode status
2346
 
        self.mygpo_client.flush()
2347
 
 
2348
 
    def playback_episodes(self, episodes):
2349
 
        # We need to create a list, because we run through it more than once
2350
 
        episodes = list(PodcastEpisode.sort_by_pubdate(e for e in episodes if \
2351
 
               e.was_downloaded(and_exists=True) or self.streaming_possible()))
2352
 
 
2353
 
        try:
2354
 
            self.playback_episodes_for_real(episodes)
2355
 
        except Exception, e:
2356
 
            log('Error in playback!', sender=self, traceback=True)
2357
 
            if gpodder.ui.desktop:
2358
 
                self.show_message(_('Please check your media player settings in the preferences dialog.'), \
2359
 
                        _('Error opening player'), widget=self.toolPreferences)
2360
 
            else:
2361
 
                self.show_message(_('Please check your media player settings in the preferences dialog.'))
2362
 
 
2363
 
        channel_urls = set()
2364
 
        episode_urls = set()
2365
 
        for episode in episodes:
2366
 
            channel_urls.add(episode.channel.url)
2367
 
            episode_urls.add(episode.url)
2368
 
        self.update_episode_list_icons(episode_urls)
2369
 
        self.update_podcast_list_model(channel_urls)
2370
 
 
2371
 
    def play_or_download(self):
2372
 
        if not gpodder.ui.fremantle:
2373
 
            if self.wNotebook.get_current_page() > 0:
2374
 
                if gpodder.ui.desktop:
2375
 
                    self.toolCancel.set_sensitive(True)
2376
 
                return
2377
 
 
2378
 
        if self.currently_updating:
2379
 
            return (False, False, False, False, False, False)
2380
 
 
2381
 
        ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
2382
 
        ( is_played, is_locked ) = (False,)*2
2383
 
 
2384
 
        open_instead_of_play = False
2385
 
 
2386
 
        selection = self.treeAvailable.get_selection()
2387
 
        if selection.count_selected_rows() > 0:
2388
 
            (model, paths) = selection.get_selected_rows()
2389
 
         
2390
 
            for path in paths:
2391
 
                try:
2392
 
                    episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
2393
 
                except TypeError, te:
2394
 
                    log('Invalid episode at path %s', str(path), sender=self)
2395
 
                    continue
2396
 
 
2397
 
                if episode.file_type() not in ('audio', 'video'):
2398
 
                    open_instead_of_play = True
2399
 
 
2400
 
                if episode.was_downloaded():
2401
 
                    can_play = episode.was_downloaded(and_exists=True)
2402
 
                    is_played = episode.is_played
2403
 
                    is_locked = episode.is_locked
2404
 
                    if not can_play:
2405
 
                        can_download = True
2406
 
                else:
2407
 
                    if self.episode_is_downloading(episode):
2408
 
                        can_cancel = True
2409
 
                    else:
2410
 
                        can_download = True
2411
 
 
2412
 
            can_download = can_download and not can_cancel
2413
 
            can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
2414
 
            can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
2415
 
            can_delete = not can_cancel
2416
 
 
2417
 
        if gpodder.ui.desktop:
2418
 
            if open_instead_of_play:
2419
 
                self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
2420
 
            else:
2421
 
                self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
2422
 
            self.toolPlay.set_sensitive( can_play)
2423
 
            self.toolDownload.set_sensitive( can_download)
2424
 
            self.toolTransfer.set_sensitive( can_transfer)
2425
 
            self.toolCancel.set_sensitive( can_cancel)
2426
 
 
2427
 
        if not gpodder.ui.fremantle:
2428
 
            self.item_cancel_download.set_sensitive(can_cancel)
2429
 
            self.itemDownloadSelected.set_sensitive(can_download)
2430
 
            self.itemOpenSelected.set_sensitive(can_play)
2431
 
            self.itemPlaySelected.set_sensitive(can_play)
2432
 
            self.itemDeleteSelected.set_sensitive(can_delete)
2433
 
            self.item_toggle_played.set_sensitive(can_play)
2434
 
            self.item_toggle_lock.set_sensitive(can_play)
2435
 
            self.itemOpenSelected.set_visible(open_instead_of_play)
2436
 
            self.itemPlaySelected.set_visible(not open_instead_of_play)
2437
 
 
2438
 
        return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
2439
 
 
2440
 
    def on_cbMaxDownloads_toggled(self, widget, *args):
2441
 
        self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
2442
 
 
2443
 
    def on_cbLimitDownloads_toggled(self, widget, *args):
2444
 
        self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
2445
 
 
2446
 
    def episode_new_status_changed(self, urls):
2447
 
        self.update_podcast_list_model()
2448
 
        self.update_episode_list_icons(urls)
2449
 
 
2450
 
    def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
2451
 
        """Update the podcast list treeview model
2452
 
 
2453
 
        If urls is given, it should list the URLs of each
2454
 
        podcast that has to be updated in the list.
2455
 
 
2456
 
        If selected is True, only update the model contents
2457
 
        for the currently-selected podcast - nothing more.
2458
 
 
2459
 
        The caller can optionally specify "select_url",
2460
 
        which is the URL of the podcast that is to be
2461
 
        selected in the list after the update is complete.
2462
 
        This only works if the podcast list has to be
2463
 
        reloaded; i.e. something has been added or removed
2464
 
        since the last update of the podcast list).
2465
 
        """
2466
 
        selection = self.treeChannels.get_selection()
2467
 
        model, iter = selection.get_selected()
2468
 
 
2469
 
        if self.config.podcast_list_view_all and not self.channel_list_changed:
2470
 
            # Update "all episodes" view in any case (if enabled)
2471
 
            self.podcast_list_model.update_first_row()
2472
 
 
2473
 
        if selected:
2474
 
            # very cheap! only update selected channel
2475
 
            if iter is not None:
2476
 
                # If we have selected the "all episodes" view, we have
2477
 
                # to update all channels for selected episodes:
2478
 
                if self.config.podcast_list_view_all and \
2479
 
                        self.podcast_list_model.iter_is_first_row(iter):
2480
 
                    urls = self.get_podcast_urls_from_selected_episodes()
2481
 
                    self.podcast_list_model.update_by_urls(urls)
2482
 
                else:
2483
 
                    # Otherwise just update the selected row (a podcast)
2484
 
                    self.podcast_list_model.update_by_filter_iter(iter)
2485
 
        elif not self.channel_list_changed:
2486
 
            # we can keep the model, but have to update some
2487
 
            if urls is None:
2488
 
                # still cheaper than reloading the whole list
2489
 
                self.podcast_list_model.update_all()
2490
 
            else:
2491
 
                # ok, we got a bunch of urls to update
2492
 
                self.podcast_list_model.update_by_urls(urls)
2493
 
        else:
2494
 
            if model and iter and select_url is None:
2495
 
                # Get the URL of the currently-selected podcast
2496
 
                select_url = model.get_value(iter, PodcastListModel.C_URL)
2497
 
 
2498
 
            # Update the podcast list model with new channels
2499
 
            self.podcast_list_model.set_channels(self.db, self.config, self.channels)
2500
 
 
2501
 
            try:
2502
 
                selected_iter = model.get_iter_first()
2503
 
                # Find the previously-selected URL in the new
2504
 
                # model if we have an URL (else select first)
2505
 
                if select_url is not None:
2506
 
                    pos = model.get_iter_first()
2507
 
                    while pos is not None:
2508
 
                        url = model.get_value(pos, PodcastListModel.C_URL)
2509
 
                        if url == select_url:
2510
 
                            selected_iter = pos
2511
 
                            break
2512
 
                        pos = model.iter_next(pos)
2513
 
 
2514
 
                if not gpodder.ui.maemo:
2515
 
                    if selected_iter is not None:
2516
 
                        selection.select_iter(selected_iter)
2517
 
                    self.on_treeChannels_cursor_changed(self.treeChannels)
2518
 
            except:
2519
 
                log('Cannot select podcast in list', traceback=True, sender=self)
2520
 
        self.channel_list_changed = False
2521
 
 
2522
 
    def episode_is_downloading(self, episode):
2523
 
        """Returns True if the given episode is being downloaded at the moment"""
2524
 
        if episode is None:
2525
 
            return False
2526
 
 
2527
 
        return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
2528
 
 
2529
 
    def on_episode_list_filter_changed(self, has_episodes):
2530
 
        if gpodder.ui.fremantle:
2531
 
            if has_episodes:
2532
 
                self.episodes_window.empty_label.hide()
2533
 
                self.episodes_window.pannablearea.show()
2534
 
            else:
2535
 
                if self.config.episode_list_view_mode != \
2536
 
                        EpisodeListModel.VIEW_ALL:
2537
 
                    text = _('No episodes in current view')
2538
 
                else:
2539
 
                    text = _('No episodes available')
2540
 
                self.episodes_window.empty_label.set_text(text)
2541
 
                self.episodes_window.pannablearea.hide()
2542
 
                self.episodes_window.empty_label.show()
2543
 
 
2544
 
    def update_episode_list_model(self):
2545
 
        if self.channels and self.active_channel is not None:
2546
 
            if gpodder.ui.fremantle:
2547
 
                hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2548
 
 
2549
 
            self.currently_updating = True
2550
 
            self.episode_list_model.clear()
2551
 
            if gpodder.ui.fremantle:
2552
 
                self.episodes_window.pannablearea.hide()
2553
 
                self.episodes_window.empty_label.set_text(_('Loading episodes'))
2554
 
                self.episodes_window.empty_label.show()
2555
 
 
2556
 
            def update():
2557
 
                additional_args = (self.episode_is_downloading, \
2558
 
                        self.config.episode_list_descriptions and gpodder.ui.desktop, \
2559
 
                        self.config.episode_list_thumbnails and gpodder.ui.desktop)
2560
 
                self.episode_list_model.replace_from_channel(self.active_channel, *additional_args)
2561
 
 
2562
 
                self.treeAvailable.get_selection().unselect_all()
2563
 
                self.treeAvailable.scroll_to_point(0, 0)
2564
 
 
2565
 
                self.currently_updating = False
2566
 
                self.play_or_download()
2567
 
 
2568
 
                if gpodder.ui.fremantle:
2569
 
                    hildon.hildon_gtk_window_set_progress_indicator(\
2570
 
                            self.episodes_window.main_window, False)
2571
 
 
2572
 
            util.idle_add(update)
2573
 
        else:
2574
 
            self.episode_list_model.clear()
2575
 
 
2576
 
    @dbus.service.method(gpodder.dbus_interface)
2577
 
    def offer_new_episodes(self, channels=None):
2578
 
        if gpodder.ui.fremantle:
2579
 
            # Assume that when this function is called that the
2580
 
            # notification is not shown anymore (Maemo bug 11345)
2581
 
            self._fremantle_notification_visible = False
2582
 
 
2583
 
        new_episodes = self.get_new_episodes(channels)
2584
 
        if new_episodes:
2585
 
            self.new_episodes_show(new_episodes)
2586
 
            return True
2587
 
        return False
2588
 
 
2589
 
    def add_podcast_list(self, urls, auth_tokens=None):
2590
 
        """Subscribe to a list of podcast given their URLs
2591
 
 
2592
 
        If auth_tokens is given, it should be a dictionary
2593
 
        mapping URLs to (username, password) tuples."""
2594
 
 
2595
 
        if auth_tokens is None:
2596
 
            auth_tokens = {}
2597
 
 
2598
 
        # Sort and split the URL list into five buckets
2599
 
        queued, failed, existing, worked, authreq = [], [], [], [], []
2600
 
        for input_url in urls:
2601
 
            url = util.normalize_feed_url(input_url)
2602
 
            if url is None:
2603
 
                # Fail this one because the URL is not valid
2604
 
                failed.append(input_url)
2605
 
            elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
2606
 
                # A podcast already exists in the list for this URL
2607
 
                existing.append(url)
2608
 
            else:
2609
 
                # This URL has survived the first round - queue for add
2610
 
                queued.append(url)
2611
 
                if url != input_url and input_url in auth_tokens:
2612
 
                    auth_tokens[url] = auth_tokens[input_url]
2613
 
 
2614
 
        error_messages = {}
2615
 
        redirections = {}
2616
 
 
2617
 
        progress = ProgressIndicator(_('Adding podcasts'), \
2618
 
                _('Please wait while episode information is downloaded.'), \
2619
 
                parent=self.get_dialog_parent())
2620
 
 
2621
 
        def on_after_update():
2622
 
            progress.on_finished()
2623
 
            # Report already-existing subscriptions to the user
2624
 
            if existing:
2625
 
                title = _('Existing subscriptions skipped')
2626
 
                message = _('You are already subscribed to these podcasts:') \
2627
 
                     + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
2628
 
                self.show_message(message, title, widget=self.treeChannels)
2629
 
 
2630
 
            # Report subscriptions that require authentication
2631
 
            if authreq:
2632
 
                retry_podcasts = {}
2633
 
                for url in authreq:
2634
 
                    title = _('Podcast requires authentication')
2635
 
                    message = _('Please login to %s:') % (saxutils.escape(url),)
2636
 
                    success, auth_tokens = self.show_login_dialog(title, message)
2637
 
                    if success:
2638
 
                        retry_podcasts[url] = auth_tokens
2639
 
                    else:
2640
 
                        # Stop asking the user for more login data
2641
 
                        retry_podcasts = {}
2642
 
                        for url in authreq:
2643
 
                            error_messages[url] = _('Authentication failed')
2644
 
                            failed.append(url)
2645
 
                        break
2646
 
 
2647
 
                # If we have authentication data to retry, do so here
2648
 
                if retry_podcasts:
2649
 
                    self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
2650
 
 
2651
 
            # Report website redirections
2652
 
            for url in redirections:
2653
 
                title = _('Website redirection detected')
2654
 
                message = _('The URL %(url)s redirects to %(target)s.') \
2655
 
                        + '\n\n' + _('Do you want to visit the website now?')
2656
 
                message = message % {'url': url, 'target': redirections[url]}
2657
 
                if self.show_confirmation(message, title):
2658
 
                    util.open_website(url)
2659
 
                else:
2660
 
                    break
2661
 
 
2662
 
            # Report failed subscriptions to the user
2663
 
            if failed:
2664
 
                title = _('Could not add some podcasts')
2665
 
                message = _('Some podcasts could not be added to your list:') \
2666
 
                     + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
2667
 
                        error_messages.get(url, _('Unknown')))) for url in failed)
2668
 
                self.show_message(message, title, important=True)
2669
 
 
2670
 
            # Upload subscription changes to gpodder.net
2671
 
            self.mygpo_client.on_subscribe(worked)
2672
 
 
2673
 
            # If at least one podcast has been added, save and update all
2674
 
            if self.channel_list_changed:
2675
 
                # Fix URLs if mygpo has rewritten them
2676
 
                self.rewrite_urls_mygpo()
2677
 
 
2678
 
                self.save_channels_opml()
2679
 
 
2680
 
                # If only one podcast was added, select it after the update
2681
 
                if len(worked) == 1:
2682
 
                    url = worked[0]
2683
 
                else:
2684
 
                    url = None
2685
 
 
2686
 
                # Update the list of subscribed podcasts
2687
 
                self.update_feed_cache(force_update=False, select_url_afterwards=url)
2688
 
                self.update_podcasts_tab()
2689
 
 
2690
 
                # Offer to download new episodes
2691
 
                episodes = []
2692
 
                for podcast in self.channels:
2693
 
                    if podcast.url in worked:
2694
 
                        episodes.extend(podcast.get_all_episodes())
2695
 
 
2696
 
                if episodes:
2697
 
                    episodes = list(PodcastEpisode.sort_by_pubdate(episodes, \
2698
 
                            reverse=True))
2699
 
                    self.new_episodes_show(episodes, \
2700
 
                            selected=[e.check_is_new() for e in episodes])
2701
 
 
2702
 
 
2703
 
        def thread_proc():
2704
 
            # After the initial sorting and splitting, try all queued podcasts
2705
 
            length = len(queued)
2706
 
            for index, url in enumerate(queued):
2707
 
                progress.on_progress(float(index)/float(length))
2708
 
                progress.on_message(url)
2709
 
                log('QUEUE RUNNER: %s', url, sender=self)
2710
 
                try:
2711
 
                    # The URL is valid and does not exist already - subscribe!
2712
 
                    channel = PodcastChannel.load(self.db, url=url, create=True, \
2713
 
                            authentication_tokens=auth_tokens.get(url, None), \
2714
 
                            max_episodes=self.config.max_episodes_per_feed, \
2715
 
                            download_dir=self.config.download_dir, \
2716
 
                            allow_empty_feeds=self.config.allow_empty_feeds, \
2717
 
                            mimetype_prefs=self.config.mimetype_prefs)
2718
 
 
2719
 
                    try:
2720
 
                        username, password = util.username_password_from_url(url)
2721
 
                    except ValueError, ve:
2722
 
                        username, password = (None, None)
2723
 
 
2724
 
                    if username is not None and channel.username is None and \
2725
 
                            password is not None and channel.password is None:
2726
 
                        channel.username = username
2727
 
                        channel.password = password
2728
 
                        channel.save()
2729
 
 
2730
 
                    self._update_cover(channel)
2731
 
                except feedcore.AuthenticationRequired:
2732
 
                    if url in auth_tokens:
2733
 
                        # Fail for wrong authentication data
2734
 
                        error_messages[url] = _('Authentication failed')
2735
 
                        failed.append(url)
2736
 
                    else:
2737
 
                        # Queue for login dialog later
2738
 
                        authreq.append(url)
2739
 
                    continue
2740
 
                except feedcore.WifiLogin, error:
2741
 
                    redirections[url] = error.data
2742
 
                    failed.append(url)
2743
 
                    error_messages[url] = _('Redirection detected')
2744
 
                    continue
2745
 
                except Exception, e:
2746
 
                    log('Subscription error: %s', e, traceback=True, sender=self)
2747
 
                    error_messages[url] = str(e)
2748
 
                    failed.append(url)
2749
 
                    continue
2750
 
 
2751
 
                assert channel is not None
2752
 
                worked.append(channel.url)
2753
 
                self.channels.append(channel)
2754
 
                self.channel_list_changed = True
2755
 
            util.idle_add(on_after_update)
2756
 
        threading.Thread(target=thread_proc).start()
2757
 
 
2758
 
    def save_channels_opml(self):
2759
 
        exporter = opml.Exporter(gpodder.subscription_file)
2760
 
        return exporter.write(self.channels)
2761
 
 
2762
 
    def find_episode(self, podcast_url, episode_url):
2763
 
        """Find an episode given its podcast and episode URL
2764
 
 
2765
 
        The function will return a PodcastEpisode object if
2766
 
        the episode is found, or None if it's not found.
2767
 
        """
2768
 
        for podcast in self.channels:
2769
 
            if podcast_url == podcast.url:
2770
 
                for episode in podcast.get_all_episodes():
2771
 
                    if episode_url == episode.url:
2772
 
                        return episode
2773
 
 
2774
 
        return None
2775
 
 
2776
 
    def process_received_episode_actions(self, updated_urls):
2777
 
        """Process/merge episode actions from gpodder.net
2778
 
 
2779
 
        This function will merge all changes received from
2780
 
        the server to the local database and update the
2781
 
        status of the affected episodes as necessary.
2782
 
        """
2783
 
        indicator = ProgressIndicator(_('Merging episode actions'), \
2784
 
                _('Episode actions from gpodder.net are merged.'), \
2785
 
                False, self.get_dialog_parent())
2786
 
 
2787
 
        for idx, action in enumerate(self.mygpo_client.get_episode_actions(updated_urls)):
2788
 
            if action.action == 'play':
2789
 
                episode = self.find_episode(action.podcast_url, \
2790
 
                                            action.episode_url)
2791
 
 
2792
 
                if episode is not None:
2793
 
                    log('Play action for %s', episode.url, sender=self)
2794
 
                    episode.mark(is_played=True)
2795
 
 
2796
 
                    if action.timestamp > episode.current_position_updated and \
2797
 
                            action.position is not None:
2798
 
                        log('Updating position for %s', episode.url, sender=self)
2799
 
                        episode.current_position = action.position
2800
 
                        episode.current_position_updated = action.timestamp
2801
 
 
2802
 
                    if action.total:
2803
 
                        log('Updating total time for %s', episode.url, sender=self)
2804
 
                        episode.total_time = action.total
2805
 
 
2806
 
                    episode.save()
2807
 
            elif action.action == 'delete':
2808
 
                episode = self.find_episode(action.podcast_url, \
2809
 
                                            action.episode_url)
2810
 
 
2811
 
                if episode is not None:
2812
 
                    if not episode.was_downloaded(and_exists=True):
2813
 
                        # Set the episode to a "deleted" state
2814
 
                        log('Marking as deleted: %s', episode.url, sender=self)
2815
 
                        episode.delete_from_disk()
2816
 
                        episode.save()
2817
 
 
2818
 
            indicator.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx) % {'count':idx})
2819
 
            gtk.main_iteration(False)
2820
 
 
2821
 
        indicator.on_finished()
2822
 
        self.db.commit()
2823
 
 
2824
 
 
2825
 
    def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2826
 
        self.db.commit()
2827
 
        self.updating_feed_cache = False
2828
 
 
2829
 
        self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2830
 
 
2831
 
        # Process received episode actions for all updated URLs
2832
 
        self.process_received_episode_actions(updated_urls)
2833
 
 
2834
 
        self.channel_list_changed = True
2835
 
        self.update_podcast_list_model(select_url=select_url_afterwards)
2836
 
 
2837
 
        # Only search for new episodes in podcasts that have been
2838
 
        # updated, not in other podcasts (for single-feed updates)
2839
 
        episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2840
 
 
2841
 
        if gpodder.ui.fremantle:
2842
 
            self.fancy_progress_bar.hide()
2843
 
            self.button_subscribe.set_sensitive(True)
2844
 
            self.button_refresh.set_sensitive(True)
2845
 
            hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2846
 
            hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2847
 
            self.update_podcasts_tab()
2848
 
            self.update_episode_list_model()
2849
 
            if self.feed_cache_update_cancelled:
2850
 
                return
2851
 
 
2852
 
            def application_in_foreground():
2853
 
                try:
2854
 
                    return any(w.get_property('is-topmost') for w in hildon.WindowStack.get_default().get_windows())
2855
 
                except Exception, e:
2856
 
                    log('Could not determine is-topmost', traceback=True)
2857
 
                    # When in doubt, assume not in foreground
2858
 
                    return False
2859
 
 
2860
 
            if episodes:
2861
 
                if self.config.auto_download == 'quiet' and not self.config.auto_update_feeds:
2862
 
                    # New episodes found, but we should do nothing
2863
 
                    self.show_message(_('New episodes are available.'))
2864
 
                elif self.config.auto_download == 'always' or \
2865
 
                        (self.config.auto_download == 'wifi' and \
2866
 
                         self.network_manager.connection_is_wlan()):
2867
 
                    count = len(episodes)
2868
 
                    title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2869
 
                    self.show_message(title)
2870
 
                    self.download_episode_list(episodes)
2871
 
                elif self.config.auto_download == 'queue':
2872
 
                    self.show_message(_('New episodes have been added to the download list.'))
2873
 
                    self.download_episode_list_paused(episodes)
2874
 
                elif application_in_foreground():
2875
 
                    if not self._fremantle_notification_visible:
2876
 
                        self.new_episodes_show(episodes)
2877
 
                elif not self._fremantle_notification_visible:
2878
 
                    try:
2879
 
                        import pynotify
2880
 
                        pynotify.init('gPodder')
2881
 
                        n = pynotify.Notification('gPodder', _('New episodes available'), 'gpodder')
2882
 
                        n.set_urgency(pynotify.URGENCY_CRITICAL)
2883
 
                        n.set_hint('dbus-callback-default', ' '.join([
2884
 
                            gpodder.dbus_bus_name,
2885
 
                            gpodder.dbus_gui_object_path,
2886
 
                            gpodder.dbus_interface,
2887
 
                            'offer_new_episodes',
2888
 
                        ]))
2889
 
                        n.set_category('gpodder-new-episodes')
2890
 
                        n.show()
2891
 
                        self._fremantle_notification_visible = True
2892
 
                    except Exception, e:
2893
 
                        log('Error: %s', str(e), sender=self, traceback=True)
2894
 
                        self.new_episodes_show(episodes)
2895
 
                        self._fremantle_notification_visible = False
2896
 
            elif not self.config.auto_update_feeds:
2897
 
                self.show_message(_('No new episodes. Please check for new episodes later.'))
2898
 
            return
2899
 
 
2900
 
        if self.tray_icon:
2901
 
            self.tray_icon.set_status()
2902
 
 
2903
 
        if self.feed_cache_update_cancelled:
2904
 
            # The user decided to abort the feed update
2905
 
            self.show_update_feeds_buttons()
2906
 
        elif not episodes:
2907
 
            # Nothing new here - but inform the user
2908
 
            self.pbFeedUpdate.set_fraction(1.0)
2909
 
            self.pbFeedUpdate.set_text(_('No new episodes'))
2910
 
            self.feed_cache_update_cancelled = True
2911
 
            self.btnCancelFeedUpdate.show()
2912
 
            self.btnCancelFeedUpdate.set_sensitive(True)
2913
 
            self.itemUpdate.set_sensitive(True)
2914
 
            if gpodder.ui.maemo:
2915
 
                # btnCancelFeedUpdate is a ToolButton on Maemo
2916
 
                self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2917
 
            else:
2918
 
                # btnCancelFeedUpdate is a normal gtk.Button
2919
 
                self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2920
 
        else:
2921
 
            count = len(episodes)
2922
 
            # New episodes are available
2923
 
            self.pbFeedUpdate.set_fraction(1.0)
2924
 
            # Are we minimized and should we auto download?
2925
 
            if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2926
 
                self.download_episode_list(episodes)
2927
 
                title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2928
 
                self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2929
 
                self.show_update_feeds_buttons()
2930
 
            elif self.config.auto_download == 'queue':
2931
 
                self.download_episode_list_paused(episodes)
2932
 
                title = N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count) % {'count':count}
2933
 
                self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2934
 
                self.show_update_feeds_buttons()
2935
 
            else:
2936
 
                self.show_update_feeds_buttons()
2937
 
                # New episodes are available and we are not minimized
2938
 
                if not self.config.do_not_show_new_episodes_dialog:
2939
 
                    self.new_episodes_show(episodes, notification=True)
2940
 
                else:
2941
 
                    message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
2942
 
                    self.pbFeedUpdate.set_text(message)
2943
 
 
2944
 
    def _update_cover(self, channel):
2945
 
        if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2946
 
            self.cover_downloader.request_cover(channel)
2947
 
 
2948
 
    def update_feed_cache_proc(self, channels, select_url_afterwards):
2949
 
        total = len(channels)
2950
 
 
2951
 
        for updated, channel in enumerate(channels):
2952
 
            if not self.feed_cache_update_cancelled:
2953
 
                try:
2954
 
                    channel.update(max_episodes=self.config.max_episodes_per_feed, \
2955
 
                            mimetype_prefs=self.config.mimetype_prefs)
2956
 
                    self._update_cover(channel)
2957
 
                except Exception, e:
2958
 
                    d = {'url': saxutils.escape(channel.url), 'message': saxutils.escape(str(e))}
2959
 
                    if d['message']:
2960
 
                        message = _('Error while updating %(url)s: %(message)s')
2961
 
                    else:
2962
 
                        message = _('The feed at %(url)s could not be updated.')
2963
 
                    self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
2964
 
                    log('Error: %s', str(e), sender=self, traceback=True)
2965
 
 
2966
 
            if self.feed_cache_update_cancelled:
2967
 
                break
2968
 
 
2969
 
            # By the time we get here the update may have already been cancelled
2970
 
            if not self.feed_cache_update_cancelled:
2971
 
                def update_progress():
2972
 
                    d = {'podcast': channel.title, 'position': updated+1, 'total': total}
2973
 
                    progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2974
 
                    self.pbFeedUpdate.set_text(progression)
2975
 
                    if self.tray_icon:
2976
 
                        self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2977
 
                    self.pbFeedUpdate.set_fraction(float(updated+1)/float(total))
2978
 
                util.idle_add(update_progress)
2979
 
 
2980
 
        updated_urls = [c.url for c in channels]
2981
 
        util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2982
 
 
2983
 
    def show_update_feeds_buttons(self):
2984
 
        # Make sure that the buttons for updating feeds
2985
 
        # appear - this should happen after a feed update
2986
 
        if gpodder.ui.maemo:
2987
 
            self.btnUpdateSelectedFeed.show()
2988
 
            self.toolFeedUpdateProgress.hide()
2989
 
            self.btnCancelFeedUpdate.hide()
2990
 
            self.btnCancelFeedUpdate.set_is_important(False)
2991
 
            self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2992
 
            self.toolbarSpacer.set_expand(True)
2993
 
            self.toolbarSpacer.set_draw(False)
2994
 
        else:
2995
 
            self.hboxUpdateFeeds.hide()
2996
 
        self.btnUpdateFeeds.show()
2997
 
        self.itemUpdate.set_sensitive(True)
2998
 
        self.itemUpdateChannel.set_sensitive(True)
2999
 
 
3000
 
    def on_btnCancelFeedUpdate_clicked(self, widget):
3001
 
        if not self.feed_cache_update_cancelled:
3002
 
            self.pbFeedUpdate.set_text(_('Cancelling...'))
3003
 
            self.feed_cache_update_cancelled = True
3004
 
            if not gpodder.ui.fremantle:
3005
 
                self.btnCancelFeedUpdate.set_sensitive(False)
3006
 
        elif not gpodder.ui.fremantle:
3007
 
            self.show_update_feeds_buttons()
3008
 
 
3009
 
    def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
3010
 
        if self.updating_feed_cache:
3011
 
            if gpodder.ui.fremantle:
3012
 
                self.feed_cache_update_cancelled = True
3013
 
            return
3014
 
 
3015
 
        if not force_update:
3016
 
            self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
3017
 
            self.channel_list_changed = True
3018
 
            self.update_podcast_list_model(select_url=select_url_afterwards)
3019
 
            return
3020
 
 
3021
 
        # Fix URLs if mygpo has rewritten them
3022
 
        self.rewrite_urls_mygpo()
3023
 
 
3024
 
        self.updating_feed_cache = True
3025
 
 
3026
 
        if channels is None:
3027
 
            # Only update podcasts for which updates are enabled
3028
 
            channels = [c for c in self.channels if c.feed_update_enabled]
3029
 
 
3030
 
        if gpodder.ui.fremantle:
3031
 
            hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
3032
 
            hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
3033
 
            self.fancy_progress_bar.show()
3034
 
            self.button_subscribe.set_sensitive(False)
3035
 
            self.button_refresh.set_sensitive(False)
3036
 
            self.feed_cache_update_cancelled = False
3037
 
        else:
3038
 
            self.itemUpdate.set_sensitive(False)
3039
 
            self.itemUpdateChannel.set_sensitive(False)
3040
 
 
3041
 
            if self.tray_icon:
3042
 
                self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
3043
 
 
3044
 
            self.feed_cache_update_cancelled = False
3045
 
            self.btnCancelFeedUpdate.show()
3046
 
            self.btnCancelFeedUpdate.set_sensitive(True)
3047
 
            if gpodder.ui.maemo:
3048
 
                self.toolbarSpacer.set_expand(False)
3049
 
                self.toolbarSpacer.set_draw(True)
3050
 
                self.btnUpdateSelectedFeed.hide()
3051
 
                self.toolFeedUpdateProgress.show_all()
3052
 
            else:
3053
 
                self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
3054
 
                self.hboxUpdateFeeds.show_all()
3055
 
            self.btnUpdateFeeds.hide()
3056
 
 
3057
 
        if len(channels) == 1:
3058
 
            text = _('Updating "%s"...') % channels[0].title
3059
 
        else:
3060
 
            count = len(channels)
3061
 
            text = N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count) % {'count':count}
3062
 
        self.pbFeedUpdate.set_text(text)
3063
 
        self.pbFeedUpdate.set_fraction(0)
3064
 
 
3065
 
        args = (channels, select_url_afterwards)
3066
 
        threading.Thread(target=self.update_feed_cache_proc, args=args).start()
3067
 
 
3068
 
    def on_gPodder_delete_event(self, widget, *args):
3069
 
        """Called when the GUI wants to close the window
3070
 
        Displays a confirmation dialog (and closes/hides gPodder)
3071
 
        """
3072
 
 
3073
 
        downloading = self.download_status_model.are_downloads_in_progress()
3074
 
 
3075
 
        if downloading:
3076
 
            if gpodder.ui.fremantle:
3077
 
                self.close_gpodder()
3078
 
            elif gpodder.ui.diablo:
3079
 
                result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
3080
 
                if result:
3081
 
                    self.close_gpodder()
3082
 
                else:
3083
 
                    return True
3084
 
            dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
3085
 
            dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3086
 
            quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
3087
 
 
3088
 
            title = _('Quit gPodder')
3089
 
            message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3090
 
 
3091
 
            dialog.set_title(title)
3092
 
            dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
3093
 
 
3094
 
            quit_button.grab_focus()
3095
 
            result = dialog.run()
3096
 
            dialog.destroy()
3097
 
 
3098
 
            if result == gtk.RESPONSE_CLOSE:
3099
 
                self.close_gpodder()
3100
 
        else:
3101
 
            self.close_gpodder()
3102
 
 
3103
 
        return True
3104
 
 
3105
 
    def close_gpodder(self):
3106
 
        """ clean everything and exit properly
3107
 
        """
3108
 
        if self.channels:
3109
 
            if self.save_channels_opml():
3110
 
                pass # FIXME: Add mygpo synchronization here
3111
 
            else:
3112
 
                self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
3113
 
 
3114
 
        self.gPodder.hide()
3115
 
 
3116
 
        if self.tray_icon is not None:
3117
 
            self.tray_icon.set_visible(False)
3118
 
 
3119
 
        # Notify all tasks to to carry out any clean-up actions
3120
 
        self.download_status_model.tell_all_tasks_to_quit()
3121
 
 
3122
 
        while gtk.events_pending():
3123
 
            gtk.main_iteration(False)
3124
 
 
3125
 
        self.db.close()
3126
 
 
3127
 
        self.quit()
3128
 
        sys.exit(0)
3129
 
 
3130
 
    def get_expired_episodes(self):
3131
 
        for channel in self.channels:
3132
 
            for episode in channel.get_downloaded_episodes():
3133
 
                # Never consider locked episodes as old
3134
 
                if episode.is_locked:
3135
 
                    continue
3136
 
 
3137
 
                # Never consider fresh episodes as old
3138
 
                if episode.age_in_days() < self.config.episode_old_age:
3139
 
                    continue
3140
 
 
3141
 
                # Do not delete played episodes (except if configured)
3142
 
                if episode.is_played:
3143
 
                    if not self.config.auto_remove_played_episodes:
3144
 
                        continue
3145
 
 
3146
 
                # Do not delete unplayed episodes (except if configured)
3147
 
                if not episode.is_played:
3148
 
                    if not self.config.auto_remove_unplayed_episodes:
3149
 
                        continue
3150
 
 
3151
 
                yield episode
3152
 
 
3153
 
    def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
3154
 
        if not episodes:
3155
 
            return False
3156
 
 
3157
 
        if skip_locked:
3158
 
            episodes = [e for e in episodes if not e.is_locked]
3159
 
 
3160
 
            if not episodes:
3161
 
                title = _('Episodes are locked')
3162
 
                message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3163
 
                self.notification(message, title, widget=self.treeAvailable)
3164
 
                return False
3165
 
 
3166
 
        count = len(episodes)
3167
 
        title = N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count) % {'count':count}
3168
 
        message = _('Deleting episodes removes downloaded files.')
3169
 
 
3170
 
        if gpodder.ui.fremantle:
3171
 
            message = '\n'.join([title, message])
3172
 
 
3173
 
        if confirm and not self.show_confirmation(message, title):
3174
 
            return False
3175
 
 
3176
 
        progress = ProgressIndicator(_('Deleting episodes'), \
3177
 
                _('Please wait while episodes are deleted'), \
3178
 
                parent=self.get_dialog_parent())
3179
 
 
3180
 
        def finish_deletion(episode_urls, channel_urls):
3181
 
            progress.on_finished()
3182
 
 
3183
 
            # Episodes have been deleted - persist the database
3184
 
            self.db.commit()
3185
 
 
3186
 
            self.update_episode_list_icons(episode_urls)
3187
 
            self.update_podcast_list_model(channel_urls)
3188
 
            self.play_or_download()
3189
 
 
3190
 
        def thread_proc():
3191
 
            episode_urls = set()
3192
 
            channel_urls = set()
3193
 
 
3194
 
            episodes_status_update = []
3195
 
            for idx, episode in enumerate(episodes):
3196
 
                progress.on_progress(float(idx)/float(len(episodes)))
3197
 
                if episode.is_locked and skip_locked:
3198
 
                    log('Not deleting episode (is locked): %s', episode.title)
3199
 
                else:
3200
 
                    log('Deleting episode: %s', episode.title)
3201
 
                    progress.on_message(episode.title)
3202
 
                    episode.delete_from_disk()
3203
 
                    episode_urls.add(episode.url)
3204
 
                    channel_urls.add(episode.channel.url)
3205
 
                    episodes_status_update.append(episode)
3206
 
 
3207
 
                    # Tell the shownotes window that we have removed the episode
3208
 
                    if self.episode_shownotes_window is not None and \
3209
 
                            self.episode_shownotes_window.episode is not None and \
3210
 
                            self.episode_shownotes_window.episode.url == episode.url:
3211
 
                        util.idle_add(self.episode_shownotes_window._download_status_changed, None)
3212
 
 
3213
 
            # Notify the web service about the status update + upload
3214
 
            self.mygpo_client.on_delete(episodes_status_update)
3215
 
            self.mygpo_client.flush()
3216
 
 
3217
 
            util.idle_add(finish_deletion, episode_urls, channel_urls)
3218
 
 
3219
 
        threading.Thread(target=thread_proc).start()
3220
 
 
3221
 
        return True
3222
 
 
3223
 
    def on_itemRemoveOldEpisodes_activate(self, widget):
3224
 
        self.show_delete_episodes_window()
3225
 
 
3226
 
    def show_delete_episodes_window(self, channel=None):
3227
 
        """Offer deletion of episodes
3228
 
 
3229
 
        If channel is None, offer deletion of all episodes.
3230
 
        Otherwise only offer deletion of episodes in the channel.
3231
 
        """
3232
 
        if gpodder.ui.maemo:
3233
 
            columns = (
3234
 
                ('maemo_remove_markup', None, None, _('Episode')),
3235
 
            )
3236
 
        else:
3237
 
            columns = (
3238
 
                ('title_markup', None, None, _('Episode')),
3239
 
                ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3240
 
                ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3241
 
                ('played_prop', None, None, _('Status')),
3242
 
                ('age_prop', 'age_int_prop', gobject.TYPE_INT, _('Downloaded')),
3243
 
            )
3244
 
 
3245
 
        msg_older_than = N_('Select older than %(count)d day', 'Select older than %(count)d days', self.config.episode_old_age)
3246
 
        selection_buttons = {
3247
 
                _('Select played'): lambda episode: episode.is_played,
3248
 
                _('Select finished'): lambda episode: episode.is_finished(),
3249
 
                msg_older_than % {'count':self.config.episode_old_age}: lambda episode: episode.age_in_days() > self.config.episode_old_age,
3250
 
        }
3251
 
 
3252
 
        instructions = _('Select the episodes you want to delete:')
3253
 
 
3254
 
        if channel is None:
3255
 
            channels = self.channels
3256
 
        else:
3257
 
            channels = [channel]
3258
 
 
3259
 
        episodes = []
3260
 
        for channel in channels:
3261
 
            for episode in channel.get_downloaded_episodes():
3262
 
                # Disallow deletion of locked episodes that still exist
3263
 
                if not episode.is_locked or not episode.file_exists():
3264
 
                    episodes.append(episode)
3265
 
 
3266
 
        selected = [e.is_played or not e.file_exists() for e in episodes]
3267
 
 
3268
 
        gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
3269
 
                                episodes = episodes, selected = selected, columns = columns, \
3270
 
                                stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
3271
 
                                selection_buttons = selection_buttons, _config=self.config, \
3272
 
                                show_episode_shownotes=self.show_episode_shownotes)
3273
 
 
3274
 
    def on_selected_episodes_status_changed(self):
3275
 
        # The order of the updates here is important! When "All episodes" is
3276
 
        # selected, the update of the podcast list model depends on the episode
3277
 
        # list selection to determine which podcasts are affected. Updating
3278
 
        # the episode list could remove the selection if a filter is active.
3279
 
        self.update_podcast_list_model(selected=True)
3280
 
        self.update_episode_list_icons(selected=True)
3281
 
        self.db.commit()
3282
 
 
3283
 
    def mark_selected_episodes_new(self):
3284
 
        for episode in self.get_selected_episodes():
3285
 
            episode.mark_new()
3286
 
        self.on_selected_episodes_status_changed()
3287
 
 
3288
 
    def mark_selected_episodes_old(self):
3289
 
        for episode in self.get_selected_episodes():
3290
 
            episode.mark_old()
3291
 
        self.on_selected_episodes_status_changed()
3292
 
 
3293
 
    def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
3294
 
        for episode in self.get_selected_episodes():
3295
 
            if toggle:
3296
 
                episode.mark(is_played=not episode.is_played)
3297
 
            else:
3298
 
                episode.mark(is_played=new_value)
3299
 
        self.on_selected_episodes_status_changed()
3300
 
 
3301
 
    def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3302
 
        for episode in self.get_selected_episodes():
3303
 
            if toggle:
3304
 
                episode.mark(is_locked=not episode.is_locked)
3305
 
            else:
3306
 
                episode.mark(is_locked=new_value)
3307
 
        self.on_selected_episodes_status_changed()
3308
 
 
3309
 
    def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3310
 
        if self.active_channel is None:
3311
 
            return
3312
 
 
3313
 
        self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
3314
 
        self.active_channel.update_channel_lock()
3315
 
 
3316
 
        for episode in self.active_channel.get_all_episodes():
3317
 
            episode.mark(is_locked=self.active_channel.channel_is_locked)
3318
 
 
3319
 
        self.update_podcast_list_model(selected=True)
3320
 
        self.update_episode_list_icons(all=True)
3321
 
 
3322
 
    def on_itemUpdateChannel_activate(self, widget=None):
3323
 
        if self.active_channel is None:
3324
 
            title = _('No podcast selected')
3325
 
            message = _('Please select a podcast in the podcasts list to update.')
3326
 
            self.show_message( message, title, widget=self.treeChannels)
3327
 
            return
3328
 
 
3329
 
        # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3330
 
        if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3331
 
            self.update_feed_cache()
3332
 
        else:
3333
 
            self.update_feed_cache(channels=[self.active_channel])
3334
 
 
3335
 
    def on_itemUpdate_activate(self, widget=None):
3336
 
        # Check if we have outstanding subscribe/unsubscribe actions
3337
 
        if self.on_add_remove_podcasts_mygpo():
3338
 
            log('Update cancelled (received server changes)', sender=self)
3339
 
            return
3340
 
 
3341
 
        if self.channels:
3342
 
            self.update_feed_cache()
3343
 
        else:
3344
 
            gPodderWelcome(self.gPodder,
3345
 
                    center_on_widget=self.gPodder,
3346
 
                    show_example_podcasts_callback=self.on_itemImportChannels_activate,
3347
 
                    setup_my_gpodder_callback=self.on_download_subscriptions_from_mygpo)
3348
 
 
3349
 
    def download_episode_list_paused(self, episodes):
3350
 
        self.download_episode_list(episodes, True)
3351
 
 
3352
 
    def download_episode_list(self, episodes, add_paused=False, force_start=False):
3353
 
        enable_update = False
3354
 
 
3355
 
        for episode in episodes:
3356
 
            log('Downloading episode: %s', episode.title, sender = self)
3357
 
            if not episode.was_downloaded(and_exists=True):
3358
 
                task_exists = False
3359
 
                for task in self.download_tasks_seen:
3360
 
                    if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
3361
 
                        self.download_queue_manager.add_task(task, force_start)
3362
 
                        enable_update = True
3363
 
                        task_exists = True
3364
 
                        continue
3365
 
 
3366
 
                if task_exists:
3367
 
                    continue
3368
 
 
3369
 
                try:
3370
 
                    task = download.DownloadTask(episode, self.config)
3371
 
                except Exception, e:
3372
 
                    d = {'episode': episode.title, 'message': str(e)}
3373
 
                    message = _('Download error while downloading %(episode)s: %(message)s')
3374
 
                    self.show_message(message % d, _('Download error'), important=True)
3375
 
                    log('Download error while downloading %s', episode.title, sender=self, traceback=True)
3376
 
                    continue
3377
 
 
3378
 
                if add_paused:
3379
 
                    task.status = task.PAUSED
3380
 
                else:
3381
 
                    self.mygpo_client.on_download([task.episode])
3382
 
                    self.download_queue_manager.add_task(task, force_start)
3383
 
 
3384
 
                self.download_status_model.register_task(task)
3385
 
                enable_update = True
3386
 
 
3387
 
        if enable_update:
3388
 
            self.enable_download_list_update()
3389
 
 
3390
 
        # Flush updated episode status
3391
 
        self.mygpo_client.flush()
3392
 
 
3393
 
    def cancel_task_list(self, tasks):
3394
 
        if not tasks:
3395
 
            return
3396
 
 
3397
 
        for task in tasks:
3398
 
            if task.status in (task.QUEUED, task.DOWNLOADING):
3399
 
                task.status = task.CANCELLED
3400
 
            elif task.status == task.PAUSED:
3401
 
                task.status = task.CANCELLED
3402
 
                # Call run, so the partial file gets deleted
3403
 
                task.run()
3404
 
 
3405
 
        self.update_episode_list_icons([task.url for task in tasks])
3406
 
        self.play_or_download()
3407
 
 
3408
 
        # Update the tab title and downloads list
3409
 
        self.update_downloads_list()
3410
 
 
3411
 
    def new_episodes_show(self, episodes, notification=False, selected=None):
3412
 
        if gpodder.ui.maemo:
3413
 
            columns = (
3414
 
                ('maemo_markup', None, None, _('Episode')),
3415
 
            )
3416
 
            show_notification = notification
3417
 
        else:
3418
 
            columns = (
3419
 
                ('title_markup', None, None, _('Episode')),
3420
 
                ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3421
 
                ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3422
 
            )
3423
 
            show_notification = False
3424
 
 
3425
 
        instructions = _('Select the episodes you want to download:')
3426
 
 
3427
 
        if self.new_episodes_window is not None:
3428
 
            self.new_episodes_window.main_window.destroy()
3429
 
            self.new_episodes_window = None
3430
 
 
3431
 
        def download_episodes_callback(episodes):
3432
 
            self.new_episodes_window = None
3433
 
            self.download_episode_list(episodes)
3434
 
 
3435
 
        if selected is None:
3436
 
            # Select all by default
3437
 
            selected = [True]*len(episodes)
3438
 
 
3439
 
        self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
3440
 
                title=_('New episodes available'), \
3441
 
                instructions=instructions, \
3442
 
                episodes=episodes, \
3443
 
                columns=columns, \
3444
 
                selected=selected, \
3445
 
                stock_ok_button = 'gpodder-download', \
3446
 
                callback=download_episodes_callback, \
3447
 
                remove_callback=lambda e: e.mark_old(), \
3448
 
                remove_action=_('Mark as old'), \
3449
 
                remove_finished=self.episode_new_status_changed, \
3450
 
                _config=self.config, \
3451
 
                show_notification=show_notification, \
3452
 
                show_episode_shownotes=self.show_episode_shownotes)
3453
 
 
3454
 
    def on_itemDownloadAllNew_activate(self, widget, *args):
3455
 
        if not self.offer_new_episodes():
3456
 
            self.show_message(_('Please check for new episodes later.'), \
3457
 
                    _('No new episodes available'), widget=self.btnUpdateFeeds)
3458
 
 
3459
 
    def get_new_episodes(self, channels=None):
3460
 
        if channels is None:
3461
 
            channels = self.channels
3462
 
        episodes = []
3463
 
        for channel in channels:
3464
 
            for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
3465
 
                episodes.append(episode)
3466
 
 
3467
 
        return episodes
3468
 
 
3469
 
    @dbus.service.method(gpodder.dbus_interface)
3470
 
    def start_device_synchronization(self):
3471
 
        """Public D-Bus API for starting Device sync (Desktop only)
3472
 
 
3473
 
        This method can be called to initiate a synchronization with
3474
 
        a configured protable media player. This only works for the
3475
 
        Desktop version of gPodder and does nothing on Maemo.
3476
 
        """
3477
 
        if gpodder.ui.desktop:
3478
 
            self.on_sync_to_ipod_activate(None)
3479
 
            return True
3480
 
 
3481
 
        return False
3482
 
 
3483
 
    def on_sync_to_ipod_activate(self, widget, episodes=None, force_played=True):
3484
 
        self.sync_ui.on_synchronize_episodes(self.channels, episodes, force_played)
3485
 
 
3486
 
    def commit_changes_to_database(self):
3487
 
        """This will be called after the sync process is finished"""
3488
 
        self.db.commit()
3489
 
 
3490
 
    def on_cleanup_ipod_activate(self, widget, *args):
3491
 
        self.sync_ui.on_cleanup_device()
3492
 
 
3493
 
    def on_manage_device_playlist(self, widget):
3494
 
        self.sync_ui.on_manage_device_playlist()
3495
 
 
3496
 
    def show_hide_tray_icon(self):
3497
 
        if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
3498
 
            self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
3499
 
        elif not self.config.display_tray_icon and self.tray_icon:
3500
 
            self.tray_icon.set_visible(False)
3501
 
            del self.tray_icon
3502
 
            self.tray_icon = None
3503
 
 
3504
 
        if self.tray_icon:
3505
 
            self.tray_icon.set_visible(True)
3506
 
 
3507
 
    def on_itemShowAllEpisodes_activate(self, widget):
3508
 
        self.config.podcast_list_view_all = widget.get_active()
3509
 
 
3510
 
    def on_itemShowToolbar_activate(self, widget):
3511
 
        self.config.show_toolbar = self.itemShowToolbar.get_active()
3512
 
 
3513
 
    def on_itemShowDescription_activate(self, widget):
3514
 
        self.config.episode_list_descriptions = self.itemShowDescription.get_active()
3515
 
 
3516
 
    def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
3517
 
        self.config.podcast_list_hide_boring = toggleaction.get_active()
3518
 
        if self.config.podcast_list_hide_boring:
3519
 
            self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3520
 
        else:
3521
 
            self.podcast_list_model.set_view_mode(-1)
3522
 
 
3523
 
    def on_item_view_podcasts_changed(self, radioaction, current):
3524
 
        # Only on Fremantle
3525
 
        if current == self.item_view_podcasts_all:
3526
 
            self.podcast_list_model.set_view_mode(-1)
3527
 
        elif current == self.item_view_podcasts_downloaded:
3528
 
            self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3529
 
        elif current == self.item_view_podcasts_unplayed:
3530
 
            self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3531
 
 
3532
 
        self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
3533
 
 
3534
 
    def on_item_view_episodes_changed(self, radioaction, current):
3535
 
        if current == self.item_view_episodes_all:
3536
 
            self.config.episode_list_view_mode = EpisodeListModel.VIEW_ALL
3537
 
        elif current == self.item_view_episodes_undeleted:
3538
 
            self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNDELETED
3539
 
        elif current == self.item_view_episodes_downloaded:
3540
 
            self.config.episode_list_view_mode = EpisodeListModel.VIEW_DOWNLOADED
3541
 
        elif current == self.item_view_episodes_unplayed:
3542
 
            self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNPLAYED
3543
 
 
3544
 
        self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
3545
 
 
3546
 
        if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
3547
 
            self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3548
 
 
3549
 
    def update_item_device( self):
3550
 
        if not gpodder.ui.fremantle:
3551
 
            if self.config.device_type != 'none':
3552
 
                self.itemDevice.set_visible(True)
3553
 
                self.itemDevice.label = self.get_device_name()
3554
 
            else:
3555
 
                self.itemDevice.set_visible(False)
3556
 
 
3557
 
    def properties_closed( self):
3558
 
        self.preferences_dialog = None
3559
 
        self.show_hide_tray_icon()
3560
 
        self.update_item_device()
3561
 
        if gpodder.ui.maemo:
3562
 
            selection = self.treeAvailable.get_selection()
3563
 
            if self.config.maemo_enable_gestures or \
3564
 
                    self.config.enable_fingerscroll:
3565
 
                selection.set_mode(gtk.SELECTION_SINGLE)
3566
 
            else:
3567
 
                selection.set_mode(gtk.SELECTION_MULTIPLE)
3568
 
 
3569
 
    def on_itemPreferences_activate(self, widget, *args):
3570
 
        self.preferences_dialog = gPodderPreferences(self.main_window, \
3571
 
                _config=self.config, \
3572
 
                callback_finished=self.properties_closed, \
3573
 
                user_apps_reader=self.user_apps_reader, \
3574
 
                parent_window=self.main_window, \
3575
 
                mygpo_client=self.mygpo_client, \
3576
 
                on_send_full_subscriptions=self.on_send_full_subscriptions, \
3577
 
                on_itemExportChannels_activate=self.on_itemExportChannels_activate)
3578
 
 
3579
 
        # Initial message to relayout window (in case it's opened in portrait mode
3580
 
        self.preferences_dialog.on_window_orientation_changed(self._last_orientation)
3581
 
 
3582
 
    def on_itemDependencies_activate(self, widget):
3583
 
        gPodderDependencyManager(self.gPodder)
3584
 
 
3585
 
    def on_goto_mygpo(self, widget):
3586
 
        self.mygpo_client.open_website()
3587
 
 
3588
 
    def on_download_subscriptions_from_mygpo(self, action=None):
3589
 
        title = _('Login to gpodder.net')
3590
 
        message = _('Please login to download your subscriptions.')
3591
 
        success, (username, password) = self.show_login_dialog(title, message, \
3592
 
                self.config.mygpo_username, self.config.mygpo_password)
3593
 
        if not success:
3594
 
            return
3595
 
 
3596
 
        self.config.mygpo_username = username
3597
 
        self.config.mygpo_password = password
3598
 
 
3599
 
        dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3600
 
                custom_title=_('Subscriptions on gpodder.net'), \
3601
 
                add_urls_callback=self.add_podcast_list, \
3602
 
                hide_url_entry=True)
3603
 
 
3604
 
        # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3605
 
        #       we do not have to hardcode the URL here
3606
 
        OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
3607
 
        url = util.url_add_authentication(OPML_URL, \
3608
 
                self.config.mygpo_username, \
3609
 
                self.config.mygpo_password)
3610
 
        dir.download_opml_file(url)
3611
 
 
3612
 
    def on_mygpo_settings_activate(self, action=None):
3613
 
        # This dialog is only used for Maemo 4
3614
 
        if not gpodder.ui.diablo:
3615
 
            return
3616
 
 
3617
 
        settings = MygPodderSettings(self.main_window, \
3618
 
                config=self.config, \
3619
 
                mygpo_client=self.mygpo_client, \
3620
 
                on_send_full_subscriptions=self.on_send_full_subscriptions)
3621
 
 
3622
 
    def on_itemAddChannel_activate(self, widget=None):
3623
 
        gPodderAddPodcast(self.gPodder, \
3624
 
                add_urls_callback=self.add_podcast_list)
3625
 
 
3626
 
    def on_itemEditChannel_activate(self, widget, *args):
3627
 
        if self.active_channel is None:
3628
 
            title = _('No podcast selected')
3629
 
            message = _('Please select a podcast in the podcasts list to edit.')
3630
 
            self.show_message( message, title, widget=self.treeChannels)
3631
 
            return
3632
 
 
3633
 
        callback_closed = lambda: self.update_podcast_list_model(selected=True)
3634
 
        gPodderChannel(self.main_window, \
3635
 
                channel=self.active_channel, \
3636
 
                callback_closed=callback_closed, \
3637
 
                cover_downloader=self.cover_downloader)
3638
 
 
3639
 
    def on_itemMassUnsubscribe_activate(self, item=None):
3640
 
        columns = (
3641
 
            ('title', None, None, _('Podcast')),
3642
 
        )
3643
 
 
3644
 
        # We're abusing the Episode Selector for selecting Podcasts here,
3645
 
        # but it works and looks good, so why not? -- thp
3646
 
        gPodderEpisodeSelector(self.main_window, \
3647
 
                title=_('Remove podcasts'), \
3648
 
                instructions=_('Select the podcast you want to remove.'), \
3649
 
                episodes=self.channels, \
3650
 
                columns=columns, \
3651
 
                size_attribute=None, \
3652
 
                stock_ok_button=_('Remove'), \
3653
 
                callback=self.remove_podcast_list, \
3654
 
                _config=self.config)
3655
 
 
3656
 
    def remove_podcast_list(self, channels, confirm=True):
3657
 
        if not channels:
3658
 
            log('No podcasts selected for deletion', sender=self)
3659
 
            return
3660
 
 
3661
 
        if len(channels) == 1:
3662
 
            title = _('Removing podcast')
3663
 
            info = _('Please wait while the podcast is removed')
3664
 
            message = _('Do you really want to remove this podcast and its episodes?')
3665
 
        else:
3666
 
            title = _('Removing podcasts')
3667
 
            info = _('Please wait while the podcasts are removed')
3668
 
            message = _('Do you really want to remove the selected podcasts and their episodes?')
3669
 
 
3670
 
        if confirm and not self.show_confirmation(message, title):
3671
 
            return
3672
 
 
3673
 
        progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
3674
 
 
3675
 
        def finish_deletion(select_url):
3676
 
            # Upload subscription list changes to the web service
3677
 
            self.mygpo_client.on_unsubscribe([c.url for c in channels])
3678
 
 
3679
 
            # Re-load the channels and select the desired new channel
3680
 
            self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
3681
 
            progress.on_finished()
3682
 
            self.update_podcasts_tab()
3683
 
 
3684
 
        def thread_proc():
3685
 
            select_url = None
3686
 
 
3687
 
            for idx, channel in enumerate(channels):
3688
 
                # Update the UI for correct status messages
3689
 
                progress.on_progress(float(idx)/float(len(channels)))
3690
 
                progress.on_message(channel.title)
3691
 
 
3692
 
                # Delete downloaded episodes
3693
 
                channel.remove_downloaded()
3694
 
 
3695
 
                # cancel any active downloads from this channel
3696
 
                for episode in channel.get_all_episodes():
3697
 
                    util.idle_add(self.download_status_model.cancel_by_url,
3698
 
                            episode.url)
3699
 
 
3700
 
                if len(channels) == 1:
3701
 
                    # get the URL of the podcast we want to select next
3702
 
                    if channel in self.channels:
3703
 
                        position = self.channels.index(channel)
3704
 
                    else:
3705
 
                        position = -1
3706
 
 
3707
 
                    if position == len(self.channels)-1:
3708
 
                        # this is the last podcast, so select the URL
3709
 
                        # of the item before this one (i.e. the "new last")
3710
 
                        select_url = self.channels[position-1].url
3711
 
                    else:
3712
 
                        # there is a podcast after the deleted one, so
3713
 
                        # we simply select the one that comes after it
3714
 
                        select_url = self.channels[position+1].url
3715
 
 
3716
 
                # Remove the channel and clean the database entries
3717
 
                channel.delete()
3718
 
                self.channels.remove(channel)
3719
 
 
3720
 
            # Clean up downloads and download directories
3721
 
            self.clean_up_downloads()
3722
 
 
3723
 
            self.channel_list_changed = True
3724
 
            self.save_channels_opml()
3725
 
 
3726
 
            # The remaining stuff is to be done in the GTK main thread
3727
 
            util.idle_add(finish_deletion, select_url)
3728
 
 
3729
 
        threading.Thread(target=thread_proc).start()
3730
 
 
3731
 
    def on_itemRemoveChannel_activate(self, widget, *args):
3732
 
        if self.active_channel is None:
3733
 
            title = _('No podcast selected')
3734
 
            message = _('Please select a podcast in the podcasts list to remove.')
3735
 
            self.show_message( message, title, widget=self.treeChannels)
3736
 
            return
3737
 
 
3738
 
        self.remove_podcast_list([self.active_channel])
3739
 
 
3740
 
    def get_opml_filter(self):
3741
 
        filter = gtk.FileFilter()
3742
 
        filter.add_pattern('*.opml')
3743
 
        filter.add_pattern('*.xml')
3744
 
        filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3745
 
        return filter
3746
 
 
3747
 
    def on_item_import_from_file_activate(self, widget, filename=None):
3748
 
        if filename is None:
3749
 
            if gpodder.ui.desktop or gpodder.ui.fremantle:
3750
 
                dlg = gtk.FileChooserDialog(title=_('Import from OPML'), \
3751
 
                        parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3752
 
                dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3753
 
                dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3754
 
            elif gpodder.ui.diablo:
3755
 
                dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
3756
 
            dlg.set_filter(self.get_opml_filter())
3757
 
            response = dlg.run()
3758
 
            filename = None
3759
 
            if response == gtk.RESPONSE_OK:
3760
 
                filename = dlg.get_filename()
3761
 
            dlg.destroy()
3762
 
 
3763
 
        if filename is not None:
3764
 
            dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3765
 
                    custom_title=_('Import podcasts from OPML file'), \
3766
 
                    add_urls_callback=self.add_podcast_list, \
3767
 
                    hide_url_entry=True)
3768
 
            dir.download_opml_file(filename)
3769
 
 
3770
 
    def on_itemExportChannels_activate(self, widget, *args):
3771
 
        if not self.channels:
3772
 
            title = _('Nothing to export')
3773
 
            message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3774
 
            self.show_message(message, title, widget=self.treeChannels)
3775
 
            return
3776
 
 
3777
 
        if gpodder.ui.desktop:
3778
 
            dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
3779
 
            dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3780
 
            dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
3781
 
        elif gpodder.ui.fremantle:
3782
 
            dlg = gobject.new(hildon.FileChooserDialog, \
3783
 
                    action=gtk.FILE_CHOOSER_ACTION_SAVE)
3784
 
            dlg.set_title(_('Export to OPML'))
3785
 
        elif gpodder.ui.diablo:
3786
 
            dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
3787
 
        dlg.set_filter(self.get_opml_filter())
3788
 
        response = dlg.run()
3789
 
        if response == gtk.RESPONSE_OK:
3790
 
            filename = dlg.get_filename()
3791
 
            dlg.destroy()
3792
 
            exporter = opml.Exporter( filename)
3793
 
            if filename is not None and exporter.write(self.channels):
3794
 
                count = len(self.channels)
3795
 
                title = N_('%(count)d subscription exported', '%(count)d subscriptions exported', count) % {'count':count}
3796
 
                self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
3797
 
            else:
3798
 
                self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
3799
 
        else:
3800
 
            dlg.destroy()
3801
 
 
3802
 
    def on_itemImportChannels_activate(self, widget, *args):
3803
 
        if gpodder.ui.fremantle:
3804
 
            gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
3805
 
                    self.config.toplist_url, \
3806
 
                    self.config.opml_url, \
3807
 
                    self.add_podcast_list, \
3808
 
                    self.on_itemAddChannel_activate, \
3809
 
                    self.on_download_subscriptions_from_mygpo, \
3810
 
                    self.show_text_edit_dialog)
3811
 
        else:
3812
 
            dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
3813
 
                    add_urls_callback=self.add_podcast_list)
3814
 
            util.idle_add(dir.download_opml_file, self.config.opml_url)
3815
 
 
3816
 
    def on_homepage_activate(self, widget, *args):
3817
 
        util.open_website(gpodder.__url__)
3818
 
 
3819
 
    def on_wiki_activate(self, widget, *args):
3820
 
        util.open_website('http://gpodder.org/wiki/User_Manual')
3821
 
 
3822
 
    def on_bug_tracker_activate(self, widget, *args):
3823
 
        if gpodder.ui.maemo:
3824
 
            util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3825
 
        else:
3826
 
            util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3827
 
 
3828
 
    def on_item_support_activate(self, widget):
3829
 
        util.open_website('http://gpodder.org/donate')
3830
 
 
3831
 
    def on_itemAbout_activate(self, widget, *args):
3832
 
        if gpodder.ui.fremantle:
3833
 
            from gpodder.gtkui.frmntl.about import HeAboutDialog
3834
 
            HeAboutDialog.present(self.main_window,
3835
 
                                 'gPodder',
3836
 
                                 'gpodder',
3837
 
                                 gpodder.__version__,
3838
 
                                 _('A podcast client with focus on usability'),
3839
 
                                 gpodder.__copyright__,
3840
 
                                 gpodder.__url__,
3841
 
                                 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3842
 
                                 'http://gpodder.org/donate')
3843
 
            return
3844
 
 
3845
 
        dlg = gtk.AboutDialog()
3846
 
        dlg.set_transient_for(self.main_window)
3847
 
        dlg.set_name('gPodder')
3848
 
        dlg.set_version(gpodder.__version__)
3849
 
        dlg.set_copyright(gpodder.__copyright__)
3850
 
        dlg.set_comments(_('A podcast client with focus on usability'))
3851
 
        dlg.set_website(gpodder.__url__)
3852
 
        dlg.set_translator_credits( _('translator-credits'))
3853
 
        dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3854
 
 
3855
 
        if gpodder.ui.desktop:
3856
 
            # For the "GUI" version, we add some more
3857
 
            # items to the about dialog (credits and logo)
3858
 
            app_authors = [
3859
 
                    _('Maintainer:'),
3860
 
                    'Thomas Perl <thp.io>',
3861
 
            ]
3862
 
 
3863
 
            if os.path.exists(gpodder.credits_file):
3864
 
                credits = open(gpodder.credits_file).read().strip().split('\n')
3865
 
                app_authors += ['', _('Patches, bug reports and donations by:')]
3866
 
                app_authors += credits
3867
 
 
3868
 
            dlg.set_authors(app_authors)
3869
 
            try:
3870
 
                dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3871
 
            except:
3872
 
                dlg.set_logo_icon_name('gpodder')
3873
 
 
3874
 
        dlg.run()
3875
 
 
3876
 
    def on_wNotebook_switch_page(self, widget, *args):
3877
 
        page_num = args[1]
3878
 
        if gpodder.ui.maemo:
3879
 
            self.tool_downloads.set_active(page_num == 1)
3880
 
            page = self.wNotebook.get_nth_page(page_num)
3881
 
            tab_label = self.wNotebook.get_tab_label(page).get_text()
3882
 
            if page_num == 0 and self.active_channel is not None:
3883
 
                self.set_title(self.active_channel.title)
3884
 
            else:
3885
 
                self.set_title(tab_label)
3886
 
        if page_num == 0:
3887
 
            self.play_or_download()
3888
 
            self.menuChannels.set_sensitive(True)
3889
 
            self.menuSubscriptions.set_sensitive(True)
3890
 
            # The message area in the downloads tab should be hidden
3891
 
            # when the user switches away from the downloads tab
3892
 
            if self.message_area is not None:
3893
 
                self.message_area.hide()
3894
 
                self.message_area = None
3895
 
        else:
3896
 
            self.menuChannels.set_sensitive(False)
3897
 
            self.menuSubscriptions.set_sensitive(False)
3898
 
            if gpodder.ui.desktop:
3899
 
                self.toolDownload.set_sensitive(False)
3900
 
                self.toolPlay.set_sensitive(False)
3901
 
                self.toolTransfer.set_sensitive(False)
3902
 
                self.toolCancel.set_sensitive(False)
3903
 
 
3904
 
    def on_treeChannels_row_activated(self, widget, path, *args):
3905
 
        # double-click action of the podcast list or enter
3906
 
        self.treeChannels.set_cursor(path)
3907
 
 
3908
 
    def on_treeChannels_cursor_changed(self, widget, *args):
3909
 
        ( model, iter ) = self.treeChannels.get_selection().get_selected()
3910
 
 
3911
 
        if model is not None and iter is not None:
3912
 
            old_active_channel = self.active_channel
3913
 
            self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3914
 
 
3915
 
            if self.active_channel == old_active_channel:
3916
 
                return
3917
 
 
3918
 
            if gpodder.ui.maemo:
3919
 
                self.set_title(self.active_channel.title)
3920
 
 
3921
 
            # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3922
 
            if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3923
 
                self.itemEditChannel.set_visible(False)
3924
 
                self.itemRemoveChannel.set_visible(False)
3925
 
            else:
3926
 
                self.itemEditChannel.set_visible(True)
3927
 
                self.itemRemoveChannel.set_visible(True)
3928
 
        else:
3929
 
            self.active_channel = None
3930
 
            self.itemEditChannel.set_visible(False)
3931
 
            self.itemRemoveChannel.set_visible(False)
3932
 
 
3933
 
        self.update_episode_list_model()
3934
 
 
3935
 
    def on_btnEditChannel_clicked(self, widget, *args):
3936
 
        self.on_itemEditChannel_activate( widget, args)
3937
 
 
3938
 
    def get_podcast_urls_from_selected_episodes(self):
3939
 
        """Get a set of podcast URLs based on the selected episodes"""
3940
 
        return set(episode.channel.url for episode in \
3941
 
                self.get_selected_episodes())
3942
 
 
3943
 
    def get_selected_episodes(self):
3944
 
        """Get a list of selected episodes from treeAvailable"""
3945
 
        selection = self.treeAvailable.get_selection()
3946
 
        model, paths = selection.get_selected_rows()
3947
 
 
3948
 
        episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3949
 
        return episodes
3950
 
 
3951
 
    def on_transfer_selected_episodes(self, widget):
3952
 
        self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3953
 
 
3954
 
    def on_playback_selected_episodes(self, widget):
3955
 
        self.playback_episodes(self.get_selected_episodes())
3956
 
 
3957
 
    def on_shownotes_selected_episodes(self, widget):
3958
 
        episodes = self.get_selected_episodes()
3959
 
        if episodes:
3960
 
            episode = episodes.pop(0)
3961
 
            self.show_episode_shownotes(episode)
3962
 
        else:
3963
 
            self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3964
 
 
3965
 
    def on_download_selected_episodes(self, widget):
3966
 
        episodes = self.get_selected_episodes()
3967
 
        self.download_episode_list(episodes)
3968
 
        self.update_episode_list_icons([episode.url for episode in episodes])
3969
 
        self.play_or_download()
3970
 
 
3971
 
    def on_treeAvailable_row_activated(self, widget, path, view_column):
3972
 
        """Double-click/enter action handler for treeAvailable"""
3973
 
        # We should only have one one selected as it was double clicked!
3974
 
        e = self.get_selected_episodes()[0]
3975
 
        
3976
 
        if (self.config.double_click_episode_action == 'download'):
3977
 
            # If the episode has already been downloaded and exists then play it
3978
 
            if e.was_downloaded(and_exists=True):
3979
 
                self.playback_episodes(self.get_selected_episodes())
3980
 
            # else download it if it is not already downloading
3981
 
            elif not self.episode_is_downloading(e): 
3982
 
                self.download_episode_list([e])
3983
 
                self.update_episode_list_icons([e.url])
3984
 
                self.play_or_download()
3985
 
        elif (self.config.double_click_episode_action == 'stream'):
3986
 
            # If we happen to have downloaded this episode simple play it
3987
 
            if e.was_downloaded(and_exists=True):
3988
 
                self.playback_episodes(self.get_selected_episodes())
3989
 
            # else if streaming is possible stream it    
3990
 
            elif self.streaming_possible():
3991
 
                self.playback_episodes(self.get_selected_episodes())
3992
 
            else:
3993
 
                log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3994
 
                self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3995
 
        else:
3996
 
            # default action is to display show notes
3997
 
            self.on_shownotes_selected_episodes(widget)
3998
 
 
3999
 
    def show_episode_shownotes(self, episode):
4000
 
        if self.episode_shownotes_window is None:
4001
 
            log('First-time use of episode window --- creating', sender=self)
4002
 
            self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
4003
 
                    _download_episode_list=self.download_episode_list, \
4004
 
                    _playback_episodes=self.playback_episodes, \
4005
 
                    _delete_episode_list=self.delete_episode_list, \
4006
 
                    _episode_list_status_changed=self.episode_list_status_changed, \
4007
 
                    _cancel_task_list=self.cancel_task_list, \
4008
 
                    _episode_is_downloading=self.episode_is_downloading, \
4009
 
                    _streaming_possible=self.streaming_possible())
4010
 
        self.episode_shownotes_window.show(episode)
4011
 
        if self.episode_is_downloading(episode):
4012
 
            self.update_downloads_list()
4013
 
 
4014
 
    def restart_auto_update_timer(self):
4015
 
        if self._auto_update_timer_source_id is not None:
4016
 
            log('Removing existing auto update timer.', sender=self)
4017
 
            gobject.source_remove(self._auto_update_timer_source_id)
4018
 
            self._auto_update_timer_source_id = None
4019
 
 
4020
 
        if self.config.auto_update_feeds and \
4021
 
                self.config.auto_update_frequency:
4022
 
            interval = 60*1000*self.config.auto_update_frequency
4023
 
            log('Setting up auto update timer with interval %d.', \
4024
 
                    self.config.auto_update_frequency, sender=self)
4025
 
            self._auto_update_timer_source_id = gobject.timeout_add(\
4026
 
                    interval, self._on_auto_update_timer)
4027
 
 
4028
 
    def _on_auto_update_timer(self):
4029
 
        log('Auto update timer fired.', sender=self)
4030
 
        self.update_feed_cache(force_update=True)
4031
 
 
4032
 
        # Ask web service for sub changes (if enabled)
4033
 
        self.mygpo_client.flush()
4034
 
 
4035
 
        return True
4036
 
 
4037
 
    def on_treeDownloads_row_activated(self, widget, *args):
4038
 
        # Use the standard way of working on the treeview
4039
 
        selection = self.treeDownloads.get_selection()
4040
 
        (model, paths) = selection.get_selected_rows()
4041
 
        selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
4042
 
 
4043
 
        for tree_row_reference, task in selected_tasks:
4044
 
            if task.status in (task.DOWNLOADING, task.QUEUED):
4045
 
                task.status = task.PAUSED
4046
 
            elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
4047
 
                self.download_queue_manager.add_task(task)
4048
 
                self.enable_download_list_update()
4049
 
            elif task.status == task.DONE:
4050
 
                model.remove(model.get_iter(tree_row_reference.get_path()))
4051
 
                
4052
 
        self.play_or_download()
4053
 
 
4054
 
        # Update the tab title and downloads list
4055
 
        self.update_downloads_list()
4056
 
 
4057
 
    def on_item_cancel_download_activate(self, widget):
4058
 
        if self.wNotebook.get_current_page() == 0:
4059
 
            selection = self.treeAvailable.get_selection()
4060
 
            (model, paths) = selection.get_selected_rows()
4061
 
            urls = [model.get_value(model.get_iter(path), \
4062
 
                    self.episode_list_model.C_URL) for path in paths]
4063
 
            selected_tasks = [task for task in self.download_tasks_seen \
4064
 
                    if task.url in urls]
4065
 
        else:
4066
 
            selection = self.treeDownloads.get_selection()
4067
 
            (model, paths) = selection.get_selected_rows()
4068
 
            selected_tasks = [model.get_value(model.get_iter(path), \
4069
 
                    self.download_status_model.C_TASK) for path in paths]
4070
 
        self.cancel_task_list(selected_tasks)
4071
 
 
4072
 
    def on_btnCancelAll_clicked(self, widget, *args):
4073
 
        self.cancel_task_list(self.download_tasks_seen)
4074
 
 
4075
 
    def on_btnDownloadedDelete_clicked(self, widget, *args):
4076
 
        episodes = self.get_selected_episodes()
4077
 
        if len(episodes) == 1:
4078
 
            self.delete_episode_list(episodes, skip_locked=False)
4079
 
        else:
4080
 
            self.delete_episode_list(episodes)
4081
 
 
4082
 
    def on_key_press(self, widget, event):
4083
 
        # Allow tab switching with Ctrl + PgUp/PgDown
4084
 
        if event.state & gtk.gdk.CONTROL_MASK:
4085
 
            if event.keyval == gtk.keysyms.Page_Up:
4086
 
                self.wNotebook.prev_page()
4087
 
                return True
4088
 
            elif event.keyval == gtk.keysyms.Page_Down:
4089
 
                self.wNotebook.next_page()
4090
 
                return True
4091
 
 
4092
 
        # After this code we only handle Maemo hardware keys,
4093
 
        # so if we are not a Maemo app, we don't do anything
4094
 
        if not gpodder.ui.maemo:
4095
 
            return False
4096
 
        
4097
 
        diff = 0
4098
 
        if event.keyval == gtk.keysyms.F7: #plus
4099
 
            diff = 1
4100
 
        elif event.keyval == gtk.keysyms.F8: #minus
4101
 
            diff = -1
4102
 
 
4103
 
        if diff != 0 and not self.currently_updating:
4104
 
            selection = self.treeChannels.get_selection()
4105
 
            (model, iter) = selection.get_selected()
4106
 
            new_path = ((model.get_path(iter)[0]+diff)%len(model),)
4107
 
            selection.select_path(new_path)
4108
 
            self.treeChannels.set_cursor(new_path)
4109
 
            return True
4110
 
 
4111
 
        return False
4112
 
 
4113
 
    def on_iconify(self):
4114
 
        if self.tray_icon:
4115
 
            self.gPodder.set_skip_taskbar_hint(False)
4116
 
        else:
4117
 
            self.gPodder.set_skip_taskbar_hint(False)
4118
 
 
4119
 
    def on_uniconify(self):
4120
 
        if self.tray_icon:
4121
 
            self.gPodder.set_skip_taskbar_hint(False)
4122
 
        else:
4123
 
            self.gPodder.set_skip_taskbar_hint(False)
4124
 
 
4125
 
    def uniconify_main_window(self):
4126
 
        # We need to hide and then show the window in WMs like Metacity
4127
 
        # or KWin4 to move the window to the active workspace
4128
 
        # (see http://gpodder.org/bug/1125)
4129
 
        self.gPodder.hide()
4130
 
        self.gPodder.show()
4131
 
        self.gPodder.present()
4132
 
 
4133
 
    def iconify_main_window(self):
4134
 
        if not self.is_iconified():
4135
 
            self.gPodder.hide()
4136
 
 
4137
 
    def update_podcasts_tab(self):
4138
 
        if gpodder.ui.fremantle:
4139
 
            return
4140
 
 
4141
 
        self.label2.set_text(_('Podcasts'))
4142
 
        count = len(self.channels)
4143
 
        if count:
4144
 
            self.label2.set_text(self.label2.get_text() + ' (%d)' % count)
4145
 
 
4146
 
    @dbus.service.method(gpodder.dbus_interface)
4147
 
    def show_gui_window(self):
4148
 
        parent = self.get_dialog_parent()
4149
 
        parent.present()
4150
 
 
4151
 
    @dbus.service.method(gpodder.dbus_interface)
4152
 
    def subscribe_to_url(self, url):
4153
 
        gPodderAddPodcast(self.gPodder,
4154
 
                add_urls_callback=self.add_podcast_list,
4155
 
                preset_url=url)
4156
 
 
4157
 
    @dbus.service.method(gpodder.dbus_interface)
4158
 
    def mark_episode_played(self, filename):
4159
 
        if filename is None:
4160
 
            return False
4161
 
 
4162
 
        for channel in self.channels:
4163
 
            for episode in channel.get_all_episodes():
4164
 
                fn = episode.local_filename(create=False, check_only=True)
4165
 
                if fn == filename:
4166
 
                    episode.mark(is_played=True)
4167
 
                    self.db.commit()
4168
 
                    self.update_episode_list_icons([episode.url])
4169
 
                    self.update_podcast_list_model([episode.channel.url])
4170
 
                    return True
4171
 
 
4172
 
        return False
4173
 
 
4174
 
 
4175
 
def main(options=None):
4176
 
    gobject.threads_init()
4177
 
    gobject.set_application_name('gPodder')
4178
 
 
4179
 
    if gpodder.ui.maemo:
4180
 
        # Try to enable the custom icon theme for gPodder on Maemo
4181
 
        settings = gtk.settings_get_default()
4182
 
        settings.set_string_property('gtk-icon-theme-name', \
4183
 
                                     'gpodder', __file__)
4184
 
        # Extend the search path for the optified icon theme (Maemo 5)
4185
 
        icon_theme = gtk.icon_theme_get_default()
4186
 
        icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
4187
 
 
4188
 
        # Add custom icons for the new Maemo 5 look :)
4189
 
        for id in ('audio', 'video', 'download', 'audio-locked', 'video-locked'):
4190
 
            filename = os.path.join(gpodder.images_folder, '%s.png' % id)
4191
 
            pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
4192
 
            gtk.icon_theme_add_builtin_icon('gpodder-%s' % id, 40, pixbuf)
4193
 
 
4194
 
    gtk.window_set_default_icon_name('gpodder')
4195
 
    gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
4196
 
 
4197
 
    try:
4198
 
        dbus_main_loop = dbus.glib.DBusGMainLoop(set_as_default=True)
4199
 
        gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
4200
 
 
4201
 
        bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
4202
 
    except dbus.exceptions.DBusException, dbe:
4203
 
        log('Warning: Cannot get "on the bus".', traceback=True)
4204
 
        dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
4205
 
                gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
4206
 
        dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
4207
 
        dlg.set_title('gPodder')
4208
 
        dlg.run()
4209
 
        dlg.destroy()
4210
 
        sys.exit(0)
4211
 
 
4212
 
    util.make_directory(gpodder.home)
4213
 
    gpodder.load_plugins()
4214
 
 
4215
 
    config = UIConfig(gpodder.config_file)
4216
 
 
4217
 
    # Load hook modules and install the hook manager globally
4218
 
    # if modules have been found an instantiated by the manager
4219
 
    user_hooks = hooks.HookManager()
4220
 
    if user_hooks.has_modules():
4221
 
        gpodder.user_hooks = user_hooks
4222
 
 
4223
 
    if gpodder.ui.diablo:
4224
 
        # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4225
 
        # folder exists there (allow moving "gpodder" between SD cards or USB)
4226
 
        # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4227
 
        if not os.path.exists(config.download_dir):
4228
 
            log('Downloads might have been moved. Trying to locate them...')
4229
 
            for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
4230
 
                dir = os.path.join(basedir, 'gpodder')
4231
 
                if os.path.exists(dir):
4232
 
                    log('Downloads found in: %s', dir)
4233
 
                    config.download_dir = dir
4234
 
                    break
4235
 
                else:
4236
 
                    log('Downloads NOT FOUND in %s', dir)
4237
 
 
4238
 
    if config.enable_fingerscroll:
4239
 
        BuilderWidget.use_fingerscroll = True
4240
 
 
4241
 
    config.mygpo_device_type = util.detect_device_type()
4242
 
 
4243
 
    gp = gPodder(bus_name, config)
4244
 
 
4245
 
    # Handle options
4246
 
    if options.subscribe:
4247
 
        util.idle_add(gp.subscribe_to_url, options.subscribe)
4248
 
 
4249
 
    # mac OS X stuff :
4250
 
    # handle "subscribe to podcast" events from firefox
4251
 
    if platform.system() == 'Darwin':
4252
 
        from gpodder import gpodderosx
4253
 
        gpodderosx.register_handlers(gp)
4254
 
    # end mac OS X stuff
4255
 
 
4256
 
    gp.run()
4257
 
 
4258
 
 
4259