gpodder/src/gpodder/gui.py

2532 lines
108 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (C) 2005-2007 Thomas Perl <thp at perli.net>
#
# gPodder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import os
import gtk
import gtk.gdk
import gobject
import pango
import sys
import shutil
import subprocess
import glob
from xml.sax import saxutils
from threading import Event
from threading import Thread
from string import strip
from gpodder import util
from gpodder import opml
from gpodder import services
from gpodder import sync
from gpodder import download
from gpodder import SimpleGladeApp
from gpodder import trayicon
from libpodcasts import podcastChannel
from libpodcasts import channels_to_model
from libpodcasts import load_channels
from libpodcasts import save_channels
from gpodder.liblogger import log
from gpodder.libgpodder import gl
from libplayers import UserAppsReader
from libtagupdate import tagging_supported
app_name = "gpodder"
app_version = "unknown" # will be set in main() call
app_authors = [ 'Thomas Perl <thp@perli.net>' ]
app_copyright = 'Copyright (c) 2005-2007 Thomas Perl'
app_website = 'http://www.gpodder.org/'
# these will be filled with pathnames in bin/gpodder
glade_dir = [ 'share', 'gpodder' ]
icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
class GladeWidget(SimpleGladeApp.SimpleGladeApp):
gpodder_main_window = None
def __init__( self, **kwargs):
path = os.path.join( glade_dir, '%s.glade' % app_name)
root = self.__class__.__name__
domain = app_name
SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
if root == 'gPodder':
GladeWidget.gpodder_main_window = self.gPodder
else:
# If we have a child window, set it transient for our main window
getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
if hasattr( self, 'center_on_widget'):
( x, y ) = self.gpodder_main_window.get_position()
a = self.center_on_widget.allocation
( x, y ) = ( x + a.x, y + a.y )
( w, h ) = ( a.width, a.height )
( pw, ph ) = getattr( self, root).get_size()
getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
else:
getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
def notification(self, message, title=None):
util.idle_add(self.show_message, message, title)
def show_message( self, message, title = None):
if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
if title is None:
title = 'gPodder'
self.tray_icon.send_notification(message, title)
return
dlg = gtk.MessageDialog( GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
if title:
dlg.set_title(str(title))
dlg.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % ( title, message ))
else:
dlg.set_markup( '<span weight="bold" size="larger">%s</span>' % ( message ))
dlg.run()
dlg.destroy()
def show_confirmation( self, message, title = None):
dlg = gtk.MessageDialog( GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
if title:
dlg.set_title( title)
dlg.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % ( title, message ))
else:
dlg.set_markup('<span weight="bold" size="larger">%s</span>' % message)
response = dlg.run()
dlg.destroy()
return response == gtk.RESPONSE_YES
def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
if dst_filename is None:
dst_filename = src_filename
if dst_directory is None:
dst_directory = os.path.expanduser( '~')
( base, extension ) = os.path.splitext( src_filename)
if not dst_filename.endswith( extension):
dst_filename += extension
dlg = gtk.FileChooserDialog( title = title, parent = GladeWidget.gpodder_main_window, action = gtk.FILE_CHOOSER_ACTION_SAVE)
dlg.set_do_overwrite_confirmation( True)
dlg.set_current_name( os.path.basename( dst_filename))
dlg.set_current_folder( dst_directory)
dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
dlg.add_button( gtk.STOCK_SAVE, gtk.RESPONSE_OK)
if dlg.run() == gtk.RESPONSE_OK:
dst_filename = dlg.get_filename()
if not dst_filename.endswith( extension):
dst_filename += extension
log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
try:
shutil.copyfile( src_filename, dst_filename)
except:
log( 'Error copying file.', sender = self, traceback = True)
dlg.destroy()
class gPodder(GladeWidget):
def new(self):
self.uar = None
self.minimized = False
self.gPodder.connect('window-state-event', self.window_state_event)
self.tray_icon = None
self.show_hide_tray_icon()
self.already_notified_new_episodes = []
self.episode_description_shown = gl.config.episode_list_descriptions
self.itemShowToolbar.set_active(gl.config.show_toolbar)
self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
self.update_view_settings()
if self.tray_icon:
if gl.config.start_iconified:
self.iconify_main_window()
elif gl.config.minimize_to_tray:
self.tray_icon.set_visible(False)
gl.config.connect_gtk_window( self.gPodder)
gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
while gtk.events_pending():
gtk.main_iteration( False)
if app_version.rfind( "svn") != -1:
self.gPodder.set_title( 'gPodder %s' % app_version)
self.default_title = self.gPodder.get_title()
# cell renderers for channel tree
namecolumn = gtk.TreeViewColumn( _('Channel'))
iconcell = gtk.CellRendererPixbuf()
namecolumn.pack_start( iconcell, False)
namecolumn.add_attribute( iconcell, 'pixbuf', 5)
namecell = gtk.CellRendererText()
namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
namecolumn.pack_start( namecell, True)
namecolumn.add_attribute( namecell, 'markup', 2)
namecolumn.add_attribute( namecell, 'weight', 4)
iconcell = gtk.CellRendererPixbuf()
namecolumn.pack_start( iconcell, False)
namecolumn.add_attribute( iconcell, 'pixbuf', 3)
self.treeChannels.append_column( namecolumn)
# enable alternating colors hint
self.treeAvailable.set_rules_hint( True)
self.treeChannels.set_rules_hint( True)
# connect to tooltip signals
try:
self.treeChannels.set_property('has-tooltip', True)
self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
except:
log('No tooltips for channel navigator (need at least PyGTK 2.12)', sender = self)
self.last_tooltip_channel = None
# Add our context menu to treeAvailable
self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
iconcell = gtk.CellRendererPixbuf()
iconcolumn = gtk.TreeViewColumn( _("Status"), iconcell, pixbuf = 4)
namecell = gtk.CellRendererText()
namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
namecolumn.set_expand(True)
sizecell = gtk.CellRendererText()
sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
releasecell = gtk.CellRendererText()
releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
itemcolumn.set_reorderable(True)
self.treeAvailable.append_column(itemcolumn)
# enable search in treeavailable
self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
# enable multiple selection support
self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
# columns and renderers for "download progress" tab
episodecell = gtk.CellRendererText()
episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
speedcell = gtk.CellRendererText()
speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
progresscell = gtk.CellRendererProgress()
progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
self.treeDownloads.append_column( itemcolumn)
services.download_status_manager.register( 'list-changed', self.download_status_updated)
services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
self.treeDownloads.set_model( services.download_status_manager.tree_model)
#Add Drag and Drop Support
flags = gtk.DEST_DEFAULT_ALL
targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
self.treeChannels.drag_dest_set( flags, targets, actions)
self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
# Subscribed channels
self.active_channel = None
self.channels = load_channels( load_items = False, offline = True)
# load list of user applications for audio playback
self.user_apps_reader = UserAppsReader('audio')
Thread(target = self.user_apps_reader.read).start()
# load list of user applications for video playback
self.user_video_apps_reader = UserAppsReader('video')
Thread(target = self.user_video_apps_reader.read).start()
# Clean up old, orphaned download files
gl.clean_up_downloads( delete_partial = True)
# Set the "Device" menu item for the first time
self.update_item_device()
# Now, update the feed cache, when everything's in place
self.update_feed_cache(force_update=gl.config.update_on_startup)
# Start the auto-update procedure
self.auto_update_procedure(first_run=True)
# Delete old episodes if the user wishes to
if gl.config.auto_remove_old_episodes:
old_episodes = self.get_old_episodes()
if len(old_episodes) > 0:
self.delete_episode_list(old_episodes, confirm=False)
self.updateComboBox()
def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
# FIXME: Do not hardcode treeview header height
HEADER_HEIGHT = 25
(path, column, rx, ry) = treeview.get_path_at_pos( x, y-HEADER_HEIGHT) or (None,)*4
if path is not None:
model = treeview.get_model()
iter = model.get_iter(path)
url = model.get_value(iter, 0)
for channel in self.channels:
if channel.url == url:
if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
self.last_tooltip_channel = None
return False
self.last_tooltip_channel = channel
tooltip.set_icon(channel.get_cover_pixbuf())
diskspace_str = _('Used disk space: %s') % util.format_filesize(channel.save_dir_size)
tooltip.set_markup( '<b>%s</b>\n<small><i>%s</i></small>\n%s\n\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url), saxutils.escape(channel.description), diskspace_str))
return True
self.last_tooltip_channel = None
return False
def treeview_channels_button_pressed( self, treeview, event):
if event.button == 3:
( x, y ) = ( int(event.x), int(event.y) )
( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
paths = []
# Did the user right-click into a selection?
selection = treeview.get_selection()
if selection.count_selected_rows() and path:
( model, paths ) = selection.get_selected_rows()
if path not in paths:
# We have right-clicked, but not into the
# selection, assume we don't want to operate
# on the selection
paths = []
# No selection or right click not in selection:
# Select the single item where we clicked
if not len( paths) and path:
treeview.grab_focus()
treeview.set_cursor( path, column, 0)
( model, paths ) = ( treeview.get_model(), [ path ] )
# We did not find a selection, and the user didn't
# click on an item to select -- don't show the menu
if not len( paths):
return True
menu = gtk.Menu()
channel_title = model.get_value( model.get_iter( paths[0]), 1)
item = gtk.ImageMenuItem( _('Open download folder'))
item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
menu.append( item)
menu.append( gtk.SeparatorMenuItem())
item = gtk.ImageMenuItem('')
( label, image ) = item.get_children()
label.set_text( _('Edit %s') % channel_title)
item.set_image( gtk.image_new_from_stock( gtk.STOCK_EDIT, gtk.ICON_SIZE_MENU))
item.connect( 'activate', self.on_itemEditChannel_activate)
menu.append( item)
item = gtk.ImageMenuItem( _('Remove %s') % ( channel_title, ))
item.set_image( gtk.image_new_from_stock( gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
item.connect( 'activate', self.on_itemRemoveChannel_activate)
menu.append( item)
menu.show_all()
menu.popup( None, None, None, event.button, event.time)
return True
def save_episode_as_file( self, url, *args):
episode = self.active_channel.find_episode( url)
self.show_copy_dialog( src_filename = episode.local_filename(), dst_filename = episode.sync_filename())
def copy_episode_bluetooth(self, url, *args):
episode = self.active_channel.find_episode(url)
filename = episode.local_filename()
if gl.config.bluetooth_ask_always:
device = None
else:
device = gl.config.bluetooth_device_address
destfile = os.path.join(gl.tempdir, episode.sync_filename())
(base, ext) = os.path.splitext(filename)
if not destfile.endswith(ext):
destfile += ext
if gl.config.bluetooth_use_converter:
title = _('Converting file')
message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
dlg.set_title(title)
dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
dlg.show_all()
else:
dlg = None
def convert_and_send_thread(filename, destfile, device, dialog, notify):
if gl.config.bluetooth_use_converter:
p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
result = p.wait()
if dialog is not None:
dialog.destroy()
else:
try:
shutil.copyfile(filename, destfile)
result = 0
except:
log('Cannot copy "%s" to "%s".', sender=self)
result = 1
if result == 0 or not os.path.exists(destfile):
util.bluetooth_send_file(destfile, device)
else:
notify(_('Error converting file.'), _('Bluetooth file transfer'))
util.delete_file(destfile)
Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
def treeview_button_pressed( self, treeview, event):
if event.button == 3:
( x, y ) = ( int(event.x), int(event.y) )
( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
paths = []
# Did the user right-click into a selection?
selection = self.treeAvailable.get_selection()
if selection.count_selected_rows() and path:
( model, paths ) = selection.get_selected_rows()
if path not in paths:
# We have right-clicked, but not into the
# selection, assume we don't want to operate
# on the selection
paths = []
# No selection or right click not in selection:
# Select the single item where we clicked
if not len( paths) and path:
treeview.grab_focus()
treeview.set_cursor( path, column, 0)
( model, paths ) = ( treeview.get_model(), [ path ] )
# We did not find a selection, and the user didn't
# click on an item to select -- don't show the menu
if not len( paths):
return True
first_url = model.get_value( model.get_iter( paths[0]), 0)
menu = gtk.Menu()
( can_play, can_download, can_transfer, can_cancel ) = self.play_or_download()
if len(paths) == 1:
# Single item, add episode information menu item
episode_title = model.get_value( model.get_iter( paths[0]), 1)
episode_url = model.get_value( model.get_iter( paths[0]), 0)
if len(episode_title) > 30:
episode_title = episode_title[:27] + '...'
item = gtk.ImageMenuItem('')
( label, image ) = item.get_children()
label.set_text( _('Episode information: %s') % episode_title)
item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
menu.append( item)
if can_play:
item = gtk.ImageMenuItem( _('Save %s to folder...') % episode_title)
item.set_image( gtk.image_new_from_stock( gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.save_episode_as_file( episode_url))
menu.append( item)
if gl.config.bluetooth_enabled:
if gl.config.bluetooth_ask_always:
bt_device_name = _('bluetooth device')
else:
bt_device_name = _('%s via bluetooth')%gl.config.bluetooth_device_name
item = gtk.ImageMenuItem(_('Send to %s') % bt_device_name)
item.set_image(gtk.image_new_from_icon_name('stock_bluetooth', gtk.ICON_SIZE_MENU))
item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
menu.append( item)
menu.append( gtk.SeparatorMenuItem())
else:
episode_title = _('%d selected episodes') % len(paths)
if can_play:
item = gtk.ImageMenuItem( _('Play %s') % episode_title)
item.set_image( gtk.image_new_from_stock( gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
menu.append( item)
is_locked = gl.history_is_locked(first_url)
if not is_locked:
item = gtk.ImageMenuItem(_('Remove %s') % episode_title)
item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
item.connect('activate', self.on_btnDownloadedDelete_clicked)
menu.append(item)
if can_download:
item = gtk.ImageMenuItem( _('Download %s') % episode_title)
item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
menu.append( item)
menu.append( gtk.SeparatorMenuItem())
is_downloaded = gl.history_is_downloaded(first_url)
if is_downloaded:
item = gtk.ImageMenuItem( _('Mark %s as not downloaded') % episode_title)
item.set_image( gtk.image_new_from_stock( gtk.STOCK_UNDELETE, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.on_item_toggle_downloaded_activate( w, False, False))
menu.append( item)
else:
item = gtk.ImageMenuItem( _('Mark %s as deleted') % episode_title)
item.set_image( gtk.image_new_from_stock( gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.on_item_toggle_downloaded_activate( w, False, True))
menu.append( item)
if can_transfer:
item = gtk.ImageMenuItem(_('Transfer %s to %s') % (episode_title, gl.get_device_name()))
item.set_image( gtk.image_new_from_stock( gtk.STOCK_NETWORK, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolTransfer))
menu.append( item)
if can_play:
menu.append( gtk.SeparatorMenuItem())
is_played = gl.history_is_played(first_url)
if is_played:
item = gtk.ImageMenuItem( _('Mark %s as unplayed') % episode_title)
item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
menu.append( item)
else:
item = gtk.ImageMenuItem( _('Mark %s as played') % episode_title)
item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
menu.append( item)
menu.append(gtk.SeparatorMenuItem())
is_locked = gl.history_is_locked(first_url)
if is_locked:
item = gtk.ImageMenuItem(_('Unlock %s') % episode_title)
item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
item.connect('activate', self.on_item_toggle_lock_activate)
menu.append(item)
else:
item = gtk.ImageMenuItem(_('Lock %s') % episode_title)
item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
item.connect('activate', self.on_item_toggle_lock_activate)
menu.append(item)
if can_cancel:
item = gtk.ImageMenuItem( _('_Cancel download'))
item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
menu.append( item)
menu.show_all()
menu.popup( None, None, None, event.button, event.time)
return True
def download_progress_updated( self, count, percentage):
title = [ self.default_title ]
if count == 1:
title.append( _('downloading one file'))
elif count > 1:
title.append( _('downloading %d files') % count)
if len(title) == 2:
title[1] = ''.join( [ title[1], ' (%d%%)' % ( percentage, ) ])
self.gPodder.set_title( ' - '.join( title))
def playback_episode( self, current_channel, current_podcast):
(success, application) = gl.playback_episode(current_channel, current_podcast)
if not success:
self.show_message( _('The selected player application cannot be found. Please check your media player settings in the preferences dialog.'), _('Error opening player: %s') % ( saxutils.escape( application), ))
self.download_status_updated()
def treeAvailable_search_equal( self, model, column, key, iter, data = None):
if model is None:
return True
key = key.lower()
# columns, as defined in libpodcasts' get model method
# 1 = episode title, 7 = description
columns = (1, 7)
for column in columns:
value = model.get_value( iter, column).lower()
if value.find( key) != -1:
return False
return True
def play_or_download( self):
if self.wNotebook.get_current_page() > 0:
return
( can_play, can_download, can_transfer, can_cancel ) = (False,)*4
selection = self.treeAvailable.get_selection()
if selection.count_selected_rows() > 0:
(model, paths) = selection.get_selected_rows()
for path in paths:
url = model.get_value( model.get_iter( path), 0)
local_filename = model.get_value( model.get_iter( path), 8)
if os.path.exists( local_filename):
can_play = True
else:
if services.download_status_manager.is_download_in_progress( url):
can_cancel = True
else:
can_download = True
if util.file_type_by_extension( util.file_extension_from_url( url)) == 'torrent':
can_download = can_download or gl.config.use_gnome_bittorrent
can_download = can_download and not can_cancel
can_play = can_play and not can_cancel and not can_download
can_transfer = can_play and gl.config.device_type != 'none'
self.toolPlay.set_sensitive( can_play)
self.toolDownload.set_sensitive( can_download)
self.toolTransfer.set_sensitive( can_transfer)
self.toolCancel.set_sensitive( can_cancel)
return ( can_play, can_download, can_transfer, can_cancel )
def download_status_updated( self):
count = services.download_status_manager.count()
if count:
self.labelDownloads.set_text( _('Downloads (%d)') % count)
else:
self.labelDownloads.set_text( _('Downloads'))
for channel in self.channels:
channel.update_model()
self.updateComboBox()
def updateComboBox( self):
( model, iter ) = self.treeChannels.get_selection().get_selected()
if model and iter:
selected = model.get_path( iter)
else:
selected = (0,)
rect = self.treeChannels.get_visible_rect()
self.treeChannels.set_model(channels_to_model(self.channels))
self.treeChannels.scroll_to_point( rect.x, rect.y)
while gtk.events_pending():
gtk.main_iteration( False)
self.treeChannels.scroll_to_point( rect.x, rect.y)
try:
self.treeChannels.get_selection().select_path( selected)
except:
log( 'Cannot set selection on treeChannels', sender = self)
self.on_treeChannels_cursor_changed( self.treeChannels)
def updateTreeView( self):
if self.channels and self.active_channel is not None:
self.treeAvailable.set_model(self.active_channel.tree_model)
self.treeAvailable.columns_autosize()
self.play_or_download()
else:
if self.treeAvailable.get_model():
self.treeAvailable.get_model().clear()
def drag_data_received(self, widget, context, x, y, sel, ttype, time):
result = sel.data
self.add_new_channel( result)
def add_new_channel( self, result = None, ask_download_new = True):
result = util.normalize_feed_url( result)
if result:
for old_channel in self.channels:
if old_channel.url == result:
self.show_message( _('You have already subscribed to this channel: %s') % ( saxutils.escape( old_channel.title), ), _('Already added'))
log( 'Channel already exists: %s', result)
# Select the existing channel in combo box
for i in range( len( self.channels)):
if self.channels[i] == old_channel:
self.treeChannels.get_selection().select_path( (i,))
return
log( 'Adding new channel: %s', result)
try:
channel = podcastChannel.get_by_url( url = result, force_update = True)
except:
log('Error in podcastChannel.get_by_url(%s)', result, sender=self)
channel = None
if channel:
self.channels.append( channel)
save_channels( self.channels)
# download changed channels
self.update_feed_cache(force_update=False)
(username, password) = util.username_password_from_url( result)
if username and self.show_confirmation( _('You have supplied <b>%s</b> as username and a password for this feed. Would you like to use the same authentication data for downloading episodes?') % ( saxutils.escape( username), ), _('Password authentication')):
channel.username = username
channel.password = password
log('Saving authentication data for episode downloads..', sender = self)
channel.save_settings()
# ask user to download some new episodes
self.treeChannels.get_selection().select_path( (len( self.channels)-1,))
self.active_channel = channel
self.updateTreeView()
if ask_download_new:
self.on_btnDownloadNewer_clicked( None)
else:
title = _('Error adding channel')
message = _('The channel could not be added. Please check the spelling of the URL or try again later.')
self.show_message( message, title)
else:
if result:
title = _('URL scheme not supported')
message = _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
self.show_message( message, title)
def update_feed_cache_callback(self, progressbar, position, count):
title = self.channels[position].title
progression = _('Updating %s (%d/%d)')%(title, position+1, count)
progressbar.set_text(progression)
if self.tray_icon:
self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
if count > 0:
progressbar.set_fraction(float(position)/float(count))
def update_feed_cache_finish_callback(self, force_update=False, please_wait_dialog=None, notify_no_new_episodes=False):
if please_wait_dialog is not None:
please_wait_dialog.destroy()
self.updateComboBox()
if self.tray_icon:
self.tray_icon.set_status(None)
if self.minimized and force_update:
new_episodes = []
# look for new episodes to notify
for channel in self.channels:
for episode in channel.get_new_episodes():
if not episode.url in self.already_notified_new_episodes:
new_episodes.append(episode)
self.already_notified_new_episodes.append(episode.url)
# notify new episodes
if len(new_episodes) == 0:
if notify_no_new_episodes and self.tray_icon is not None:
msg = _('No new episodes available for download')
self.tray_icon.send_notification(msg)
return
elif len(new_episodes) == 1:
title = _('gPodder has found %s') % (_('one new episode:'),)
else:
title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
message = self.tray_icon.format_episode_list(new_episodes)
#auto download new episodes
if gl.config.auto_download_when_minimized:
message += '\n<i>(%s...)</i>' % _('downloading')
self.download_episode_list(new_episodes)
self.tray_icon.send_notification(message, title)
return
# open the episodes selection dialog
if force_update:
self.on_itemDownloadAllNew_activate( self.gPodder)
def update_feed_cache_proc( self, force_update, callback_proc = None, callback_error = None, finish_proc = None):
self.channels = load_channels( force_update = force_update, callback_proc = callback_proc, callback_error = callback_error, offline = not force_update)
if finish_proc:
finish_proc()
def update_feed_cache(self, force_update=True, notify_no_new_episodes=False):
if self.tray_icon:
self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
# skip dialog if the window is not active and tray icon is displayed
if self.minimized and self.tray_icon:
show_update_dialog = False
else:
show_update_dialog = True
please_wait = None
if show_update_dialog:
if force_update:
title = _('Downloading podcast feeds')
heading = _('Downloading feeds')
else:
title = _('Loading podcast feeds')
heading = _('Loading feeds')
body = _('Podcast feeds contain channel metadata and information about current episodes.')
please_wait = gtk.Dialog(title, self.gPodder, gtk.DIALOG_MODAL, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
please_wait.set_transient_for(self.gPodder)
please_wait.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
please_wait.vbox.set_spacing(5)
please_wait.set_border_width(5)
please_wait.set_resizable(False)
label_heading = gtk.Label()
label_heading.set_alignment(0.0, 0.5)
label_heading.set_markup('<span weight="bold" size="larger">%s</span>'%heading)
label_body = gtk.Label(body)
label_body.set_alignment(0.0, 0.5)
label_body.set_line_wrap(True)
progressbar = gtk.ProgressBar()
progressbar.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
# put it all together
for widget in (label_heading, label_body, progressbar):
please_wait.vbox.pack_start(widget)
please_wait.show_all()
# center the dialog on the gPodder main window
(x, y) = self.gPodder.get_position()
(w, h) = self.gPodder.get_size()
(pw, ph) = please_wait.get_size()
please_wait.move(int(x+w/2-pw/2), int(y+h/2-ph/2))
# hide separator line
please_wait.set_has_separator(False)
# let's get down to business..
if show_update_dialog:
callback_proc = lambda pos, count: util.idle_add(self.update_feed_cache_callback, progressbar, pos, count)
else:
callback_proc = None
finish_proc = lambda: util.idle_add(self.update_feed_cache_finish_callback, force_update, please_wait, notify_no_new_episodes)
args = (force_update, callback_proc, self.notification, finish_proc)
thread = Thread( target = self.update_feed_cache_proc, args = args)
thread.start()
if please_wait is not None:
please_wait.run()
please_wait.destroy()
def download_podcast_by_url( self, url, want_message_dialog = True, widget = None):
if self.active_channel is None:
return
current_channel = self.active_channel
current_podcast = current_channel.find_episode( url)
filename = current_podcast.local_filename()
if widget:
if (widget.get_name() == 'itemPlaySelected' or widget.get_name() == 'toolPlay') and os.path.exists( filename):
# addDownloadedItem just to make sure the episode is marked correctly in localdb
current_channel.addDownloadedItem( current_podcast)
# open the file now
if current_podcast.file_type() != 'torrent':
self.playback_episode( current_channel, current_podcast)
return
if widget.get_name() == 'treeAvailable':
play_callback = lambda: self.playback_episode( current_channel, current_podcast)
download_callback = lambda: self.download_podcast_by_url( url, want_message_dialog, None)
gpe = gPodderEpisode( episode = current_podcast, channel = current_channel, download_callback = download_callback, play_callback = play_callback, center_on_widget = self.treeAvailable)
return
if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( current_podcast.url):
download.DownloadThread( current_channel, current_podcast, self.notification).start()
else:
if want_message_dialog and os.path.exists( filename) and not current_podcast.file_type() == 'torrent':
title = _('Episode already downloaded')
message = _('You have already downloaded this episode. Click on the episode to play it.')
self.show_message( message, title)
elif want_message_dialog and not current_podcast.file_type() == 'torrent':
title = _('Download in progress')
message = _('You are currently downloading this episode. Please check the download status tab to check when the download is finished.')
self.show_message( message, title)
if os.path.exists( filename):
log( 'Episode has already been downloaded.')
current_channel.addDownloadedItem( current_podcast)
self.updateComboBox()
def on_gPodder_delete_event(self, widget, *args):
"""Called when the GUI wants to close the window
Displays a confirmation dialog (and closes/hides gPodder)
"""
downloading = services.download_status_manager.has_items()
if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon:
self.iconify_main_window()
elif gl.config.on_quit_ask or downloading:
dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
if self.tray_icon:
dialog.add_button(_('Hide gPodder'), gtk.RESPONSE_YES)
dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
title = _('Quit gPodder')
if downloading:
message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
elif self.tray_icon:
message = _('If you hide gPodder, it will continue to run in the system tray notification area.')
else:
message = _('Do you really want to quit gPodder now?')
dialog.set_title(title)
dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
if not downloading:
cb_ask = gtk.CheckButton(_("Don't ask me again"))
dialog.vbox.pack_start(cb_ask)
cb_ask.show_all()
result = dialog.run()
dialog.destroy()
if result == gtk.RESPONSE_CLOSE:
if not downloading and cb_ask.get_active() == True:
gl.config.on_quit_ask = False
gl.config.on_quit_systray = False
self.close_gpodder()
elif result == gtk.RESPONSE_YES:
if not downloading and cb_ask.get_active() == True:
gl.config.on_quit_ask = False
gl.config.on_quit_systray = True
self.iconify_main_window()
else:
self.close_gpodder()
return True
def close_gpodder(self):
""" clean everything and exit properly
"""
if self.channels:
if not save_channels(self.channels):
self.show_message(_('Please check your permissions and free disk space.'), _('Error saving channel list'))
services.download_status_manager.cancel_all()
self.gtk_main_quit()
sys.exit( 0)
def get_old_episodes(self):
episodes = []
for channel in self.channels:
for episode in channel.get_all_episodes():
if episode.is_downloaded() and episode.is_old() and not episode.is_locked() and episode.is_played():
episodes.append(episode)
return episodes
def for_each_selected_episode_url( self, callback):
( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
for path in paths:
url = model.get_value( model.get_iter( path), 0)
try:
callback( url)
except:
log( 'Warning: Error in for_each_selected_episode_url for URL %s', url, sender = self)
self.active_channel.update_model()
self.updateComboBox()
def delete_episode_list( self, episodes, confirm = True):
if len(episodes) == 0:
return
if len(episodes) == 1:
message = _('Do you really want to delete this episode?')
else:
message = _('Do you really want to delete %d episodes?') % len(episodes)
if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
return
for episode in episodes:
log('Deleting episode: %s', episode.title, sender = self)
episode.delete_from_disk()
self.download_status_updated()
def on_itemRemoveOldEpisodes_activate( self, widget):
columns = (
('title', _('Episode')),
('channel_prop', _('Channel')),
('filesize_prop', _('Size')),
('pubdate_prop', _('Released')),
('played_prop', _('Status')),
('age_prop', _('Downloaded')),
)
selection_buttons = {
_('Select played'): lambda episode: episode.is_played(),
_('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
}
instructions = _('Select the episodes you want to delete from your hard disk.')
episodes = []
selected = []
for channel in self.channels:
for episode in channel:
if episode.is_downloaded() and not episode.is_locked():
episodes.append( episode)
selected.append( episode.is_played())
gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
episodes = episodes, selected = selected, columns = columns, \
stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
selection_buttons = selection_buttons)
def on_item_toggle_downloaded_activate( self, widget, toggle = True, new_value = False):
if toggle:
callback = lambda url: gl.history_mark_downloaded(url, not gl.history_is_downloaded(url))
else:
callback = lambda url: gl.history_mark_downloaded(url, new_value)
self.for_each_selected_episode_url( callback)
def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
if toggle:
callback = lambda url: gl.history_mark_played(url, not gl.history_is_played(url))
else:
callback = lambda url: gl.history_mark_played(url, new_value)
self.for_each_selected_episode_url( callback)
def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
if toggle:
callback = lambda url: gl.history_mark_locked(url, not gl.history_is_locked(url))
else:
callback = lambda url: gl.history_mark_locked(url, new_value)
self.for_each_selected_episode_url(callback)
def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
if self.channels:
self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
else:
title = _('No channels available')
message = _('You need to subscribe to some podcast feeds before you can start downloading podcasts. Use your favorite search engine to look for interesting podcasts.')
self.show_message( message, title)
def download_episode_list( self, episodes):
for episode in episodes:
log('Downloading episode: %s', episode.title, sender = self)
filename = episode.local_filename()
if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( episode.url):
download.DownloadThread( episode.channel, episode, self.notification).start()
def new_episodes_show(self, episodes):
columns = (
('title', _('Episode')),
('channel_prop', _('Channel')),
('filesize_prop', _('Size')),
('pubdate_prop', _('Released')),
)
if len(episodes) > 0:
instructions = _('Select the episodes you want to download now.')
gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
episodes=episodes, columns=columns, selected_default=True, \
callback=self.download_episode_list)
else:
title = _('No new episodes')
message = _('There are no new episodes to download from your podcast subscriptions. Please check for new episodes later.')
self.show_message(message, title)
def on_itemDownloadAllNew_activate(self, widget, *args):
episodes = []
for channel in self.channels:
for episode in channel.get_new_episodes():
episodes.append(episode)
self.new_episodes_show(episodes)
def on_sync_to_ipod_activate(self, widget, episodes=None):
Thread(target=self.sync_to_ipod_thread, args=(widget, episodes)).start()
def sync_to_ipod_thread(self, widget, episodes=None):
device = sync.open_device()
if device is None:
title = _('No device configured')
message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
self.notification(message, title)
return
if not device.open():
title = _('Cannot open device')
message = _('There has been an error opening your device.')
self.notification(message, title)
return
gPodderSync(device=device, gPodder=self)
if self.tray_icon:
self.tray_icon.set_synchronisation_device(device)
if episodes is None:
episodes_to_sync = []
for channel in self.channels:
if not channel.sync_to_devices:
log('Skipping channel: %s', channel.title, sender=self)
continue
for episode in channel.get_all_episodes():
if episode.is_downloaded():
episodes_to_sync.append(episode)
device.add_tracks(episodes_to_sync)
else:
device.add_tracks(episodes, force_played=True)
if not device.close():
title = _('Error closing device')
message = _('There has been an error closing your device.')
self.notification(message, title)
return
if self.tray_icon:
self.tray_icon.release_synchronisation_device()
# update model for played state updates after sync
for channel in self.channels:
util.idle_add(channel.update_model)
util.idle_add(self.updateComboBox)
def ipod_cleanup_callback(self, device, tracks):
title = _('Delete podcasts from device?')
message = _('Do you really want to completely remove the selected episodes?')
if len(tracks) > 0 and self.show_confirmation(message, title):
device.remove_tracks(tracks)
if not device.close():
title = _('Error closing device')
message = _('There has been an error closing your device.')
self.show_message(message, title)
return
def on_cleanup_ipod_activate(self, widget, *args):
columns = (
('title', _('Episode')),
('filesize', _('Size')),
('modified', _('Copied')),
)
device = sync.open_device()
if device is None:
title = _('No device configured')
message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
self.show_message(message, title)
return
if not device.open():
title = _('Cannot open device')
message = _('There has been an error opening your device.')
self.show_message(message, title)
return
gPodderSync(device=device, gPodder=self)
tracks = device.get_all_tracks()
if len(tracks) > 0:
remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
title = _('Remove podcasts from device')
instructions = _('Select the podcast episodes you want to remove from your device.')
gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=columns, \
stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback)
else:
title = _('No files on device')
message = _('The devices contains no files to be removed.')
self.show_message(message, title)
def show_hide_tray_icon(self):
if gl.config.display_tray_icon and self.tray_icon is None:
self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
elif not gl.config.display_tray_icon and self.tray_icon is not None:
self.tray_icon.set_visible(False)
del self.tray_icon
self.tray_icon = None
if gl.config.minimize_to_tray and self.tray_icon:
self.tray_icon.set_visible(self.minimized)
elif self.tray_icon:
self.tray_icon.set_visible(True)
def update_view_settings(self):
if gl.config.show_toolbar:
self.toolbar.show_all()
else:
self.toolbar.hide_all()
if self.episode_description_shown != gl.config.episode_list_descriptions:
for channel in self.channels:
channel.force_update_tree_model()
self.updateTreeView()
self.episode_description_shown = gl.config.episode_list_descriptions
def on_itemShowToolbar_activate(self, widget):
gl.config.show_toolbar = self.itemShowToolbar.get_active()
self.update_view_settings()
def on_itemShowDescription_activate(self, widget):
gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
self.update_view_settings()
def update_item_device( self):
if gl.config.device_type != 'none':
self.itemDevice.show_all()
( label, image ) = self.itemDevice.get_children()
label.set_text( gl.get_device_name())
else:
self.itemDevice.hide_all()
def properties_closed( self):
self.show_hide_tray_icon()
self.update_item_device()
self.updateComboBox()
def on_itemPreferences_activate(self, widget, *args):
prop = gPodderProperties( callback_finished = self.properties_closed)
prop.set_uar( self.user_apps_reader)
prop.set_video_uar(self.user_video_apps_reader)
def on_itemAddChannel_activate(self, widget, *args):
if self.channelPaned.get_position() < 200:
self.channelPaned.set_position( 200)
self.entryAddChannel.set_text( _('Enter podcast URL'))
self.entryAddChannel.grab_focus()
def on_itemEditChannel_activate(self, widget, *args):
if self.active_channel is None:
title = _('No channel selected')
message = _('Please select a channel in the channels list to edit.')
self.show_message( message, title)
return
gPodderChannel(channel=self.active_channel, callback_closed=self.updateComboBox, callback_change_url=self.change_channel_url)
def change_channel_url(self, old_url, new_url):
channel = None
try:
channel = podcastChannel.get_by_url(url=new_url, force_update=True)
except:
channel = None
if channel is None:
self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
return
for channel in self.channels:
if channel.url == old_url:
log('=> change channel url from %s to %s', old_url, new_url)
old_save_dir = channel.save_dir
channel.url = new_url
new_save_dir = channel.save_dir
log('old save dir=%s', old_save_dir, sender=self)
log('new save dir=%s', new_save_dir, sender=self)
files = glob.glob(os.path.join(old_save_dir, '*'))
log('moving %d files to %s', len(files), new_save_dir, sender=self)
for file in files:
log('moving %s', file, sender=self)
shutil.move(file, new_save_dir)
try:
os.rmdir(old_save_dir)
except:
log('Warning: cannot delete %s', old_save_dir, sender=self)
save_channels(self.channels)
self.update_feed_cache(force_update=False)
def on_itemRemoveChannel_activate(self, widget, *args):
try:
dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
title = _('Remove channel and episodes?')
message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
dialog.set_title(title)
dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
dialog.vbox.pack_start(cb_ask)
cb_ask.show_all()
result = dialog.run()
dialog.destroy()
if result == gtk.RESPONSE_YES:
# delete downloaded episodes only if checkbox is unchecked
if cb_ask.get_active() == False:
self.active_channel.remove_downloaded()
else:
log('Not removing downloaded episodes', sender=self)
# only delete partial files if we do not have any downloads in progress
delete_partial = not services.download_status_manager.has_items()
gl.clean_up_downloads(delete_partial)
self.channels.remove(self.active_channel)
save_channels(self.channels)
if len(self.channels) > 0:
self.treeChannels.get_selection().select_path((len(self.channels)-1,))
self.active_channel = self.channels[len(self.channels)-1]
self.update_feed_cache(force_update=False)
except:
log('There has been an error removing the channel.', traceback=True, sender=self)
def on_itemExportChannels_activate(self, widget, *args):
if not self.channels:
title = _('Nothing to export')
message = _('Your list of channel subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
self.show_message( message, title)
return
dlg = gtk.FileChooserDialog( title=_("Export to OPML"), parent = None, action = gtk.FILE_CHOOSER_ACTION_SAVE)
dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
dlg.add_button( gtk.STOCK_SAVE, gtk.RESPONSE_OK)
response = dlg.run()
if response == gtk.RESPONSE_OK:
filename = dlg.get_filename()
exporter = opml.Exporter( filename)
if not exporter.write( self.channels):
self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
dlg.destroy()
def on_itemImportChannels_activate(self, widget, *args):
gPodderOpmlLister().get_channels_from_url(gl.config.opml_url, lambda url: self.add_new_channel(url,False), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
def on_btnTransfer_clicked(self, widget, *args):
self.on_treeAvailable_row_activated( widget, args)
def on_homepage_activate(self, widget, *args):
util.open_website(app_website)
def on_wishlist_activate(self, widget, *args):
util.open_website('http://www.amazon.de/gp/registry/2PD2MYGHE6857')
def on_wiki_activate(self, widget, *args):
util.open_website('http://wiki.gpodder.org/')
def on_bug_tracker_activate(self, widget, *args):
util.open_website('http://bugs.gpodder.org/')
def on_itemAbout_activate(self, widget, *args):
dlg = gtk.AboutDialog()
dlg.set_name( app_name)
dlg.set_version( app_version)
dlg.set_authors( app_authors)
dlg.set_copyright( app_copyright)
dlg.set_website( app_website)
dlg.set_translator_credits( _('translator-credits'))
dlg.connect( 'response', lambda dlg, response: dlg.destroy())
try:
dlg.set_logo( gtk.gdk.pixbuf_new_from_file_at_size( scalable_dir, 200, 200))
except:
pass
dlg.run()
def on_wNotebook_switch_page(self, widget, *args):
page_num = args[1]
if page_num == 0:
self.play_or_download()
else:
self.toolDownload.set_sensitive( False)
self.toolPlay.set_sensitive( False)
self.toolTransfer.set_sensitive( False)
self.toolCancel.set_sensitive( services.download_status_manager.has_items())
def on_treeChannels_row_activated(self, widget, *args):
self.on_itemEditChannel_activate( self.treeChannels)
def on_treeChannels_cursor_changed(self, widget, *args):
( model, iter ) = self.treeChannels.get_selection().get_selected()
if model != None and iter != None:
id = model.get_path( iter)[0]
self.active_channel = self.channels[id]
self.itemEditChannel.get_child().set_text( _('Edit "%s"') % ( self.active_channel.title,))
self.itemRemoveChannel.get_child().set_text( _('Remove "%s"') % ( self.active_channel.title,))
self.itemEditChannel.show_all()
self.itemRemoveChannel.show_all()
else:
self.active_channel = None
self.itemEditChannel.hide_all()
self.itemRemoveChannel.hide_all()
self.updateTreeView()
def on_entryAddChannel_changed(self, widget, *args):
active = self.entryAddChannel.get_text() not in ('', _('Enter podcast URL'))
self.btnAddChannel.set_sensitive( active)
def on_btnAddChannel_clicked(self, widget, *args):
url = self.entryAddChannel.get_text()
self.entryAddChannel.set_text('')
self.add_new_channel( url)
def on_btnEditChannel_clicked(self, widget, *args):
self.on_itemEditChannel_activate( widget, args)
def on_treeAvailable_row_activated(self, widget, *args):
try:
selection = self.treeAvailable.get_selection()
selection_tuple = selection.get_selected_rows()
transfer_files = False
episodes = []
if selection.count_selected_rows() > 1:
widget_to_send = None
show_message_dialog = False
else:
widget_to_send = widget
show_message_dialog = True
if widget.get_name() == 'itemTransferSelected' or widget.get_name() == 'toolTransfer':
transfer_files = True
for apath in selection_tuple[1]:
selection_iter = self.treeAvailable.get_model().get_iter( apath)
url = self.treeAvailable.get_model().get_value( selection_iter, 0)
if transfer_files:
episodes.append( self.active_channel.find_episode( url))
else:
self.download_podcast_by_url( url, show_message_dialog, widget_to_send)
if transfer_files and len(episodes):
self.on_sync_to_ipod_activate(None, episodes)
except:
title = _('Nothing selected')
message = _('Please select an episode that you want to download and then click on the download button to start downloading the selected episode.')
self.show_message( message, title)
def on_btnDownload_clicked(self, widget, *args):
self.on_treeAvailable_row_activated( widget, args)
def on_treeAvailable_button_release_event(self, widget, *args):
self.play_or_download()
def on_btnDownloadNewer_clicked(self, widget, *args):
self.new_episodes_show(self.active_channel.get_new_episodes())
def on_btnSelectAllAvailable_clicked(self, widget, *args):
self.treeAvailable.get_selection().select_all()
self.on_treeAvailable_row_activated( self.toolDownload, args)
self.treeAvailable.get_selection().unselect_all()
def auto_update_procedure(self, first_run=False):
log('auto_update_procedure() got called', sender=self)
if not first_run and gl.config.auto_update_feeds and self.minimized:
self.update_feed_cache()
next_update = 60*1000*gl.config.auto_update_frequency
gobject.timeout_add(next_update, self.auto_update_procedure)
def on_treeDownloads_row_activated(self, widget, *args):
cancel_urls = []
if self.wNotebook.get_current_page() > 0:
# Use the download list treeview + model
( tree, column ) = ( self.treeDownloads, 3 )
else:
# Use the available podcasts treeview + model
( tree, column ) = ( self.treeAvailable, 0 )
selection = tree.get_selection()
(model, paths) = selection.get_selected_rows()
for path in paths:
url = model.get_value( model.get_iter( path), column)
cancel_urls.append( url)
if len( cancel_urls) == 0:
log('Nothing selected.', sender = self)
return
if len( cancel_urls) == 1:
title = _('Cancel download?')
message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
else:
title = _('Cancel downloads?')
message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
if self.show_confirmation( message, title):
for url in cancel_urls:
services.download_status_manager.cancel_by_url( url)
def on_btnCancelDownloadStatus_clicked(self, widget, *args):
self.on_treeDownloads_row_activated( widget, None)
def on_btnCancelAll_clicked(self, widget, *args):
self.treeDownloads.get_selection().select_all()
self.on_treeDownloads_row_activated( self.toolCancel, None)
self.treeDownloads.get_selection().unselect_all()
def on_btnDownloadedExecute_clicked(self, widget, *args):
self.on_treeAvailable_row_activated( widget, args)
def on_btnDownloadedDelete_clicked(self, widget, *args):
if self.active_channel is None:
return
channel_url = self.active_channel.url
selection = self.treeAvailable.get_selection()
( model, paths ) = selection.get_selected_rows()
if selection.count_selected_rows() == 0:
log( 'Nothing selected - will not remove any downloaded episode.')
return
if selection.count_selected_rows() == 1:
episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
locked = gl.history_is_locked(model.get_value(model.get_iter(paths[0]), 0))
if locked:
title = _('%s is locked') % episode_title
message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
self.notification(message, title)
return
title = _('Remove %s?') % episode_title
message = _("If you remove this episode, it will be deleted from your computer. If you want to listen to this episode again, you will have to re-download it.")
else:
title = _('Remove %d episodes?') % selection.count_selected_rows()
message = _('If you remove these episodes, they will be deleted from your computer. If you want to listen to any of these episodes again, you will have to re-download the episodes in question.')
locked_count = 0
for path in paths:
url = model.get_value(model.get_iter(path), 0)
if gl.history_is_locked(url):
locked_count += 1
if selection.count_selected_rows() == locked_count:
title = _('Episodes are locked')
message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
self.notification(message, title)
return
elif locked_count > 0:
title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
message = _('The selection contains locked episodes. These will not be deleted. If you want to listen to any of these episodes again, then you will have to re-download them.')
# if user confirms deletion, let's remove some stuff ;)
if self.show_confirmation( message, title):
try:
# iterate over the selection, see also on_treeDownloads_row_activated
for path in paths:
url = model.get_value( model.get_iter( path), 0)
self.active_channel.delete_episode_by_url( url)
gl.history_mark_downloaded(url)
# now, clear local db cache so we can re-read it
self.updateComboBox()
except:
log( 'Error while deleting (some) downloads.')
# only delete partial files if we do not have any downloads in progress
delete_partial = not services.download_status_manager.has_items()
gl.clean_up_downloads(delete_partial)
self.active_channel.force_update_tree_model()
self.updateTreeView()
def on_btnDeleteAll_clicked(self, widget, *args):
self.treeAvailable.get_selection().select_all()
self.on_btnDownloadedDelete_clicked( widget, args)
self.treeAvailable.get_selection().unselect_all()
def window_state_event(self, widget, event):
old_minimized = self.minimized
if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
self.minimized = True
else:
self.minimized = False
if old_minimized != self.minimized and self.tray_icon:
self.gPodder.set_skip_taskbar_hint(self.minimized)
elif not self.tray_icon:
self.gPodder.set_skip_taskbar_hint(False)
if gl.config.minimize_to_tray and self.tray_icon:
self.tray_icon.set_visible(self.minimized)
def uniconify_main_window(self):
if self.minimized:
self.gPodder.present()
def iconify_main_window(self):
if not self.minimized:
self.gPodder.iconify()
class gPodderChannel(GladeWidget):
def new(self):
self.gPodderChannel.set_title( self.channel.title)
self.entryTitle.set_text( self.channel.title)
self.entryURL.set_text( self.channel.url)
self.LabelDownloadTo.set_text( self.channel.save_dir)
self.LabelWebsite.set_text( self.channel.link)
self.channel.load_settings()
self.cbNoSync.set_active( not self.channel.sync_to_devices)
self.musicPlaylist.set_text(self.channel.device_playlist_name)
if self.channel.username:
self.FeedUsername.set_text( self.channel.username)
if self.channel.password:
self.FeedPassword.set_text( self.channel.password)
self.on_btnClearCover_clicked( self.btnClearCover, delete_file = False)
self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = False)
# Hide the website button if we don't have a valid URL
if not self.channel.link:
self.btn_website.hide_all()
b = gtk.TextBuffer()
b.set_text( self.channel.description)
self.channel_description.set_buffer( b)
#Add Drag and Drop Support
flags = gtk.DEST_DEFAULT_ALL
targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
def on_btn_website_clicked(self, widget):
util.open_website(self.channel.link)
def on_btnClearCover_clicked( self, widget, delete_file = True):
self.imgCover.clear()
if delete_file:
util.delete_file( self.channel.cover_file)
self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
self.labelCoverStatus.set_text( _('You can drag a cover file here.'))
self.labelCoverStatus.show()
def on_btnDownloadCover_clicked( self, widget, url = None):
if url is None:
url = self.channel.image
if url != False:
self.btnDownloadCover.set_sensitive( False)
self.labelCoverStatus.show()
gl.get_image_from_url(url, self.imgCover.set_from_pixbuf, self.labelCoverStatus.set_text, self.cover_download_finished, self.channel.cover_file)
def cover_download_finished( self):
self.labelCoverStatus.hide()
self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
def drag_data_received( self, widget, content, x, y, sel, ttype, time):
files = sel.data.strip().split('\n')
if len(files) != 1:
self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
return
file = files[0]
if file.startswith( 'file://') or file.startswith( 'http://'):
self.on_btnClearCover_clicked( self.btnClearCover)
if file.startswith( 'file://'):
filename = file[len('file://'):]
shutil.copyfile( filename, self.channel.cover_file)
self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = file)
return
self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
def on_gPodderChannel_destroy(self, widget, *args):
self.callback_closed()
def on_btnOK_clicked(self, widget, *args):
entered_url = self.entryURL.get_text()
channel_url = self.channel.url
if entered_url != channel_url:
if self.show_confirmation(_('Do you really want to move this channel to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
if hasattr(self, 'callback_change_url'):
self.gPodderChannel.hide_all()
self.callback_change_url(channel_url, entered_url)
self.channel.sync_to_devices = not self.cbNoSync.get_active()
self.channel.device_playlist_name = self.musicPlaylist.get_text()
self.channel.set_custom_title( self.entryTitle.get_text())
self.channel.username = self.FeedUsername.get_text().strip()
self.channel.password = self.FeedPassword.get_text()
self.channel.save_settings()
self.gPodderChannel.destroy()
class gPodderProperties(GladeWidget):
def new(self):
if not hasattr( self, 'callback_finished'):
self.callback_finished = None
gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
gl.config.connect_gtk_editable( 'player', self.openApp)
gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
gl.config.connect_gtk_editable( 'opml_url', self.opmlURL)
gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
gl.config.connect_gtk_togglebutton( 'use_gnome_bittorrent', self.radio_gnome_bittorrent)
gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
gl.config.connect_gtk_spinbutton( 'max_downloads', self.spinMaxDownloads)
gl.config.connect_gtk_togglebutton( 'max_downloads_enabled', self.cbMaxDownloads)
gl.config.connect_gtk_spinbutton( 'limit_rate_value', self.spinLimitDownloads)
gl.config.connect_gtk_togglebutton( 'limit_rate', self.cbLimitDownloads)
gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
gl.config.connect_gtk_filechooser( 'bittorrent_dir', self.chooserBitTorrentTo)
gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
gl.config.connect_gtk_togglebutton('bluetooth_enabled', self.bluetooth_enabled)
gl.config.connect_gtk_togglebutton('bluetooth_ask_always', self.bluetooth_ask_always)
gl.config.connect_gtk_togglebutton('bluetooth_ask_never', self.bluetooth_ask_never)
gl.config.connect_gtk_togglebutton('bluetooth_use_converter', self.bluetooth_use_converter)
gl.config.connect_gtk_filechooser( 'bluetooth_converter', self.bluetooth_converter, is_for_files=True)
self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
self.radio_copy_torrents.set_active( not self.radio_gnome_bittorrent.get_active())
self.iPodMountpoint.set_label( gl.config.ipod_mount)
self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
self.chooserDownloadTo.set_current_folder(gl.downloaddir)
if tagging_supported():
gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
else:
self.updatetags.set_sensitive( False)
new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
self.updatetags.set_label( new_label)
# device type
self.comboboxDeviceType.set_active( 0)
if gl.config.device_type == 'ipod':
self.comboboxDeviceType.set_active( 1)
elif gl.config.device_type == 'filesystem':
self.comboboxDeviceType.set_active( 2)
# setup cell renderers
cellrenderer = gtk.CellRendererPixbuf()
self.comboAudioPlayerApp.pack_start(cellrenderer, False)
self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
cellrenderer = gtk.CellRendererText()
self.comboAudioPlayerApp.pack_start(cellrenderer, True)
self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
cellrenderer = gtk.CellRendererPixbuf()
self.comboVideoPlayerApp.pack_start(cellrenderer, False)
self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
cellrenderer = gtk.CellRendererText()
self.comboVideoPlayerApp.pack_start(cellrenderer, True)
self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
def update_mountpoint( self, ipod):
if ipod is None or ipod.mount_point is None:
self.iPodMountpoint.set_label( '')
else:
self.iPodMountpoint.set_label( ipod.mount_point)
def on_bluetooth_select_device_clicked(self, widget):
self.bluetooth_select_device.set_sensitive(False)
gtk.main_iteration(False)
for name, address in util.discover_bluetooth_devices():
if self.show_confirmation('Use this device as your bluetooth device?', name):
gl.config.bluetooth_device_name = name
gl.config.bluetooth_device_address = address
self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
self.bluetooth_select_device.set_sensitive(True)
return
self.show_message('No more devices found', 'Scan finished')
self.bluetooth_select_device.set_sensitive(True)
def set_uar(self, uar):
self.comboAudioPlayerApp.set_model(uar.get_applications_as_model())
index = self.find_active_audio_app()
self.comboAudioPlayerApp.set_active(index)
def set_video_uar(self, uar):
self.comboVideoPlayerApp.set_model(uar.get_applications_as_model())
index = self.find_active_video_app()
self.comboVideoPlayerApp.set_active(index)
def find_active_audio_app(self):
model = self.comboAudioPlayerApp.get_model()
iter = model.get_iter_first()
index = 0
while iter is not None:
command = model.get_value(iter, 1)
if command == self.openApp.get_text():
return index
iter = model.iter_next(iter)
index += 1
# return last item = custom command
return index-1
def find_active_video_app( self):
model = self.comboVideoPlayerApp.get_model()
iter = model.get_iter_first()
index = 0
while iter is not None:
command = model.get_value(iter, 1)
if command == self.openVideoApp.get_text():
return index
iter = model.iter_next(iter)
index += 1
# return last item = custom command
return index-1
def set_download_dir( self, new_download_dir, event = None):
gl.downloaddir = self.chooserDownloadTo.get_filename()
if gl.downloaddir != self.chooserDownloadTo.get_filename():
self.notification(_('There has been an error moving your downloads to the specified location. The old download directory will be used instead.'), _('Error moving downloads'))
if event:
event.set()
def on_auto_update_feeds_toggled( self, widget, *args):
self.auto_update_frequency.set_sensitive(widget.get_active())
def on_display_tray_icon_toggled( self, widget, *args):
self.enable_notifications.set_sensitive(widget.get_active())
self.minimize_to_tray.set_sensitive(widget.get_active())
def on_cbCustomSyncName_toggled( self, widget, *args):
self.entryCustomSyncName.set_sensitive( widget.get_active())
def on_btnCustomSyncNameHelp_clicked( self, widget):
examples = [
'<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
'<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
'<i>{episode.published}</i> -&gt; <b>20070908</b>'
]
info = [
_('You can specify a custom format string for the file names on your MP3 player here.'),
_('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
'\n'.join( [ ' %s' % s for s in examples ])
]
self.show_message( '\n\n'.join( info), _('Custom format strings'))
def on_gPodderProperties_destroy(self, widget, *args):
self.on_btnOK_clicked( widget, *args)
def on_btnConfigEditor_clicked(self, widget, *args):
self.on_btnOK_clicked(widget, *args)
gPodderConfigEditor()
def on_comboAudioPlayerApp_changed(self, widget, *args):
# find out which one
iter = self.comboAudioPlayerApp.get_active_iter()
model = self.comboAudioPlayerApp.get_model()
command = model.get_value( iter, 1)
if command == '':
self.openApp.set_sensitive( True)
self.openApp.show()
self.labelCustomCommand.show()
else:
self.openApp.set_text( command)
self.openApp.set_sensitive( False)
self.openApp.hide()
self.labelCustomCommand.hide()
def on_comboVideoPlayerApp_changed(self, widget, *args):
# find out which one
iter = self.comboVideoPlayerApp.get_active_iter()
model = self.comboVideoPlayerApp.get_model()
command = model.get_value(iter, 1)
if command == '':
self.openVideoApp.set_sensitive(True)
self.openVideoApp.show()
self.labelCustomCommand.show()
else:
self.openVideoApp.set_text(command)
self.openVideoApp.set_sensitive(False)
self.openVideoApp.hide()
self.labelCustomCommand.hide()
def on_cbMaxDownloads_toggled(self, widget, *args):
self.spinMaxDownloads.set_sensitive( self.cbMaxDownloads.get_active())
def on_cbLimitDownloads_toggled(self, widget, *args):
self.spinLimitDownloads.set_sensitive( self.cbLimitDownloads.get_active())
def on_cbEnvironmentVariables_toggled(self, widget, *args):
sens = not self.cbEnvironmentVariables.get_active()
self.httpProxy.set_sensitive( sens)
self.ftpProxy.set_sensitive( sens)
def on_comboboxDeviceType_changed(self, widget, *args):
active_item = self.comboboxDeviceType.get_active()
# None
sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
self.imageSyncOptions, self. separatorSyncOptions,
self.on_sync_mark_played, self.on_sync_delete,
self.on_sync_leave, self.label_after_sync, )
for widget in sync_widgets:
if active_item == 0:
widget.hide_all()
else:
widget.show_all()
# iPod
ipod_widgets = ( self.ipodLabel, self.btn_iPodMountpoint )
for widget in ipod_widgets:
if active_item == 1:
widget.show_all()
else:
widget.hide_all()
# filesystem-based MP3 player
fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
self.cbChannelSubfolder, self.cbCustomSyncName,
self.entryCustomSyncName, self.btnCustomSyncNameHelp )
for widget in fs_widgets:
if active_item == 2:
widget.show_all()
else:
widget.hide_all()
def on_btn_iPodMountpoint_clicked(self, widget, *args):
fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
fs.set_current_folder(self.iPodMountpoint.get_label())
if fs.run() == gtk.RESPONSE_OK:
self.iPodMountpoint.set_label( fs.get_filename())
fs.destroy()
def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
fs.set_current_folder(self.filesystemMountpoint.get_label())
if fs.run() == gtk.RESPONSE_OK:
self.filesystemMountpoint.set_label( fs.get_filename())
fs.destroy()
def on_btnOK_clicked(self, widget, *args):
gl.config.ipod_mount = self.iPodMountpoint.get_label()
gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
if gl.downloaddir != self.chooserDownloadTo.get_filename():
new_download_dir = self.chooserDownloadTo.get_filename()
download_dir_size = util.calculate_size( gl.downloaddir)
download_dir_size_string = gl.format_filesize( download_dir_size)
event = Event()
dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
dlg.vbox.set_spacing( 5)
dlg.set_border_width( 5)
label = gtk.Label()
label.set_line_wrap( True)
label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
myprogressbar = gtk.ProgressBar()
# put it all together
dlg.vbox.pack_start( label)
dlg.vbox.pack_end( myprogressbar)
# switch windows
dlg.show_all()
self.gPodderProperties.hide_all()
# hide action area and separator line
dlg.action_area.hide()
dlg.set_has_separator( False)
args = ( new_download_dir, event, )
thread = Thread( target = self.set_download_dir, args = args)
thread.start()
while not event.isSet():
try:
new_download_dir_size = util.calculate_size( new_download_dir)
except:
new_download_dir_size = 0
if download_dir_size > 0:
fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
else:
fract = 0.0
if fract < 0.99:
myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
else:
myprogressbar.set_text( _('Finishing... please wait.'))
myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
event.wait( 0.1)
while gtk.events_pending():
gtk.main_iteration( False)
dlg.destroy()
device_type = self.comboboxDeviceType.get_active()
if device_type == 0:
gl.config.device_type = 'none'
elif device_type == 1:
gl.config.device_type = 'ipod'
elif device_type == 2:
gl.config.device_type = 'filesystem'
self.gPodderProperties.destroy()
if self.callback_finished:
self.callback_finished()
class gPodderEpisode(GladeWidget):
def new(self):
services.download_status_manager.register( 'list-changed', self.on_download_status_changed)
services.download_status_manager.register( 'progress-detail', self.on_download_status_progress)
self.episode_title.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils.escape( self.episode.title))
b = gtk.TextBuffer()
b.set_text( strip( self.episode.description))
self.episode_description.set_buffer( b)
self.gPodderEpisode.set_title( self.episode.title)
self.LabelDownloadLink.set_text( self.episode.url)
self.LabelWebsiteLink.set_text( self.episode.link)
self.labelPubDate.set_text( self.episode.pubDate)
# Hide the "Go to website" button if we don't have a valid URL
if self.episode.link == self.episode.url or not self.episode.link:
self.btn_website.hide_all()
self.channel_title.set_markup( _('<i>from %s</i>') % saxutils.escape( self.channel.title))
self.hide_show_widgets()
services.download_status_manager.request_progress_detail( self.episode.url)
def on_btnCancel_clicked( self, widget):
services.download_status_manager.cancel_by_url( self.episode.url)
def on_gPodderEpisode_destroy( self, widget):
services.download_status_manager.unregister( 'list-changed', self.on_download_status_changed)
services.download_status_manager.unregister( 'progress-detail', self.on_download_status_progress)
def on_download_status_changed( self):
self.hide_show_widgets()
def on_btn_website_clicked(self, widget):
util.open_website(self.episode.link)
def on_download_status_progress( self, url, progress, speed):
if url == self.episode.url:
progress = float(min(100.0,max(0.0,progress)))
self.progress_bar.set_fraction(progress/100.0)
self.progress_bar.set_text( 'Downloading: %d%% (%s)' % ( progress, speed, ))
def hide_show_widgets( self):
is_downloading = services.download_status_manager.is_download_in_progress( self.episode.url)
if is_downloading:
self.progress_bar.show_all()
self.btnCancel.show_all()
self.btnPlay.hide_all()
self.btnSaveFile.hide_all()
self.btnDownload.hide_all()
else:
self.progress_bar.hide_all()
self.btnCancel.hide_all()
if os.path.exists( self.episode.local_filename()):
self.btnPlay.show_all()
self.btnSaveFile.show_all()
self.btnDownload.hide_all()
else:
self.btnPlay.hide_all()
self.btnSaveFile.hide_all()
self.btnDownload.show_all()
def on_btnCloseWindow_clicked(self, widget, *args):
self.gPodderEpisode.destroy()
def on_btnDownload_clicked(self, widget, *args):
if self.download_callback:
self.download_callback()
def on_btnPlay_clicked(self, widget, *args):
if self.play_callback:
self.play_callback()
self.gPodderEpisode.destroy()
def on_btnSaveFile_clicked(self, widget, *args):
self.show_copy_dialog( src_filename = self.episode.local_filename(), dst_filename = self.episode.sync_filename())
class gPodderSync(GladeWidget):
def new(self):
util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
self.device.register('progress', self.on_progress)
self.device.register('sub-progress', self.on_sub_progress)
self.device.register('status', self.on_status)
self.device.register('done', self.on_done)
def on_progress(self, pos, max):
util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
util.idle_add(self.progressbar.set_text, _('%d of %d done') % (pos, max))
def on_sub_progress(self, percentage):
util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
def on_status(self, status):
util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
def on_done(self):
util.idle_add(self.gPodderSync.destroy)
if not self.gPodder.minimized:
util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
def on_gPodderSync_destroy(self, widget, *args):
self.device.unregister('progress', self.on_progress)
self.device.unregister('sub-progress', self.on_sub_progress)
self.device.unregister('status', self.on_status)
self.device.unregister('done', self.on_done)
self.device.cancel()
def on_cancel_button_clicked(self, widget, *args):
self.device.cancel()
class gPodderOpmlLister(GladeWidget):
def new(self):
# initiate channels list
self.channels = []
self.callback_for_channel = None
self.callback_finished = None
togglecell = gtk.CellRendererToggle()
togglecell.set_property( 'activatable', True)
togglecell.connect( 'toggled', self.callback_edited)
togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
titlecell = gtk.CellRendererText()
titlecolumn = gtk.TreeViewColumn( _('Channel'), titlecell, markup=1)
for itemcolumn in ( togglecolumn, titlecolumn ):
self.treeviewChannelChooser.append_column( itemcolumn)
def callback_edited( self, cell, path):
model = self.treeviewChannelChooser.get_model()
url = model[path][2]
model[path][0] = not model[path][0]
if model[path][0]:
self.channels.append( url)
else:
self.channels.remove( url)
self.btnOK.set_sensitive( bool(len(self.channels)))
def thread_finished(self, model):
self.treeviewChannelChooser.set_model(model)
self.labelStatus.set_label('')
self.btnDownloadOpml.set_sensitive(True)
self.entryURL.set_sensitive(True)
self.treeviewChannelChooser.set_sensitive(True)
self.channels = []
def thread_func(self):
url = self.entryURL.get_text()
importer = opml.Importer(url)
model = importer.get_model()
if len(model) == 0:
self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
util.idle_add(self.thread_finished, model)
def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
if callback_for_channel:
self.callback_for_channel = callback_for_channel
if callback_finished:
self.callback_finished = callback_finished
self.labelStatus.set_label( _('Downloading, please wait...'))
self.entryURL.set_text( url)
self.btnDownloadOpml.set_sensitive( False)
self.entryURL.set_sensitive( False)
self.btnOK.set_sensitive( False)
self.treeviewChannelChooser.set_sensitive( False)
Thread( target = self.thread_func).start()
def on_gPodderOpmlLister_destroy(self, widget, *args):
pass
def on_btnDownloadOpml_clicked(self, widget, *args):
self.get_channels_from_url( self.entryURL.get_text())
def on_btnOK_clicked(self, widget, *args):
self.gPodderOpmlLister.destroy()
# add channels that have been selected
for url in self.channels:
if self.callback_for_channel:
self.callback_for_channel( url)
if self.callback_finished:
self.callback_finished()
def on_btnCancel_clicked(self, widget, *args):
self.gPodderOpmlLister.destroy()
class gPodderEpisodeSelector( GladeWidget):
"""Episode selection dialog
Optional keyword arguments that modify the behaviour of this dialog:
- callback: Function that takes 1 parameter which is a list of
the selected episodes (or empty list when none selected)
- episodes: List of episodes that are presented for selection
- selected: (optional) List of boolean variables that define the
default checked state for the given episodes
- selected_default: (optional) The default boolean value for the
checked state if no other value is set
(default is False)
- columns: List of (name,caption) pairs for the columns, the name
is the attribute name of the episode to be read from
each episode object and the caption attribute is the
text that appear as column caption
(default is [('title','Episode'),])
- title: (optional) The title of the window + heading
- instructions: (optional) A one-line text describing what the
user should select / what the selection is for
- stock_ok_button: (optional) Will replace the "OK" button with
another GTK+ stock item to be used for the
affirmative button of the dialog (e.g. can
be gtk.STOCK_DELETE when the episodes to be
selected will be deleted after closing the
dialog)
- selection_buttons: (optional) A dictionary with labels as
keys and callbacks as values; for each
key a button will be generated, and when
the button is clicked, the callback will
be called for each episode and the return
value of the callback (True or False) will
be the new selected state of the episode
- size_attribute: (optional) The name of an attribute of the
supplied episode objects that can be used to
calculate the size of an episode; set this to
None if no total size calculation should be
done (in cases where total size is useless)
(default is 'length')
"""
COLUMN_TOGGLE = 0
COLUMN_ADDITIONAL = 1
def new( self):
if not hasattr( self, 'callback'):
self.callback = None
if not hasattr( self, 'episodes'):
self.episodes = []
if not hasattr( self, 'size_attribute'):
self.size_attribute = 'length'
if not hasattr( self, 'selection_buttons'):
self.selection_buttons = {}
if not hasattr( self, 'selected_default'):
self.selected_default = False
if not hasattr( self, 'selected'):
self.selected = [self.selected_default]*len(self.episodes)
if len(self.selected) < len(self.episodes):
self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
if not hasattr( self, 'columns'):
self.columns = ( ('title', _('Episode')), )
if hasattr( self, 'title'):
self.gPodderEpisodeSelector.set_title( self.title)
self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
if hasattr( self, 'instructions'):
self.labelInstructions.set_text( self.instructions)
self.labelInstructions.show_all()
if hasattr( self, 'stock_ok_button'):
self.btnOK.set_label( self.stock_ok_button)
self.btnOK.set_use_stock( True)
toggle_cell = gtk.CellRendererToggle()
toggle_cell.connect( 'toggled', self.toggle_cell_handler)
self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
next_column = self.COLUMN_ADDITIONAL
for name, caption in self.columns:
renderer = gtk.CellRendererText()
if next_column > self.COLUMN_ADDITIONAL:
renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
column = gtk.TreeViewColumn( caption, renderer, text=next_column)
column.set_resizable( True)
column.set_expand( True)
self.treeviewEpisodes.append_column( column)
next_column += 1
column_types = [ gobject.TYPE_BOOLEAN ] + [ gobject.TYPE_STRING ] * len(self.columns)
self.model = gtk.ListStore( *column_types)
for index, episode in enumerate( self.episodes):
row = [ self.selected[index] ]
for name, caption in self.columns:
row.append( getattr( episode, name))
self.model.append( row)
for label in self.selection_buttons:
button = gtk.Button( label)
button.connect( 'clicked', self.custom_selection_button_clicked)
self.hboxButtons.pack_start( button, expand = False)
button.show_all()
self.treeviewEpisodes.set_rules_hint( True)
self.treeviewEpisodes.set_model( self.model)
self.treeviewEpisodes.columns_autosize()
self.calculate_total_size()
def calculate_total_size( self):
if self.size_attribute is not None:
total_size = 0
for index, row in enumerate( self.model):
if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
try:
total_size += int(getattr( self.episodes[index], self.size_attribute))
except:
log( 'Cannot get size for %s', self.episodes[index].title, sender = self)
if total_size > 0:
self.labelTotalSize.set_text( _('Total size: %s') % gl.format_filesize( total_size))
else:
self.labelTotalSize.set_text( '')
self.labelTotalSize.show_all()
else:
self.labelTotalSize.hide_all()
def toggle_cell_handler( self, cell, path):
model = self.treeviewEpisodes.get_model()
model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
if self.size_attribute is not None:
self.calculate_total_size()
def custom_selection_button_clicked( self, button):
label = button.get_label()
callback = self.selection_buttons[label]
for index, row in enumerate( self.model):
new_value = callback( self.episodes[index])
self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
self.calculate_total_size()
def on_btnCheckAll_clicked( self, widget):
for row in self.model:
self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
self.calculate_total_size()
def on_btnCheckNone_clicked( self, widget):
for row in self.model:
self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
self.calculate_total_size()
def get_selected_episodes( self):
selected_episodes = []
for index, row in enumerate( self.model):
if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
selected_episodes.append( self.episodes[index])
return selected_episodes
def on_btnOK_clicked( self, widget):
self.gPodderEpisodeSelector.destroy()
if self.callback is not None:
self.callback( self.get_selected_episodes())
def on_btnCancel_clicked( self, widget):
self.gPodderEpisodeSelector.destroy()
if self.callback is not None:
self.callback([])
class gPodderConfigEditor(GladeWidget):
def new(self):
name_column = gtk.TreeViewColumn(_('Variable'))
name_renderer = gtk.CellRendererText()
name_column.pack_start(name_renderer)
name_column.add_attribute(name_renderer, 'text', 0)
name_column.add_attribute(name_renderer, 'weight', 5)
self.configeditor.append_column(name_column)
type_column = gtk.TreeViewColumn(_('Type'))
type_renderer = gtk.CellRendererText()
type_column.pack_start(type_renderer)
type_column.add_attribute(type_renderer, 'text', 1)
type_column.add_attribute(type_renderer, 'weight', 5)
self.configeditor.append_column(type_column)
value_column = gtk.TreeViewColumn(_('Value'))
value_renderer = gtk.CellRendererText()
value_column.pack_start(value_renderer)
value_column.add_attribute(value_renderer, 'text', 2)
value_column.add_attribute(value_renderer, 'editable', 4)
value_column.add_attribute(value_renderer, 'weight', 5)
value_renderer.connect('edited', self.value_edited)
self.configeditor.append_column(value_column)
self.model = gl.config.model()
self.filter = self.model.filter_new()
self.filter.set_visible_func(self.visible_func)
self.configeditor.set_model(self.filter)
self.configeditor.set_rules_hint(True)
def visible_func(self, model, iter, user_data=None):
text = self.entryFilter.get_text().lower()
if text == '':
return True
else:
# either the variable name or its value
return (text in model.get_value(iter, 0).lower() or
text in model.get_value(iter, 2).lower())
def value_edited(self, renderer, path, new_text):
model = self.configeditor.get_model()
iter = model.get_iter(path)
name = model.get_value(iter, 0)
type_cute = model.get_value(iter, 1)
if not gl.config.update_field(name, new_text):
self.notification(_('Cannot set value of <b>%s</b> to <i>%s</i>.\n\nNeeded data type: %s') % (saxutils.escape(name), saxutils.escape(new_text), saxutils.escape(type_cute)), _('Error updating %s') % saxutils.escape(name))
def on_entryFilter_changed(self, widget):
self.filter.refilter()
def on_btnShowAll_clicked(self, widget):
self.entryFilter.set_text('')
self.entryFilter.grab_focus()
def on_configeditor_row_activated(self, treeview, path, view_column):
model = treeview.get_model()
it = model.get_iter(path)
field_name = model.get_value(it, 0)
field_type = model.get_value(it, 3)
# Flip the boolean config flag
if field_type == bool:
gl.config.toggle_flag(field_name)
def on_btnClose_clicked(self, widget):
self.gPodderConfigEditor.destroy()
def main():
gobject.threads_init()
gtk.window_set_default_icon_name( 'gpodder')
gPodder().run()