1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # config.py -- gPodder Configuration Manager
23 # Thomas Perl <thp@perli.net> 2007-11-02
31 from gpodder
import util
32 from gpodder
.liblogger
import log
40 if gpodder
.interface
== gpodder
.MAEMO
:
41 default_bittorrent_dir
= '/media/mmc2/gpodder/torrents'
42 default_download_dir
= '/media/mmc2/gpodder/downloads'
44 default_bittorrent_dir
= os
.path
.expanduser('~/gpodder-downloads/torrents')
45 default_download_dir
= os
.path
.expanduser('~/gpodder-downloads')
49 'player': ( str, 'xdg-open' ),
50 'videoplayer': (str, 'unspecified'),
51 'opml_url': ( str, 'http://www.gpodder.org/directory.opml' ),
52 'http_proxy': ( str, '' ),
53 'ftp_proxy': ( str, '' ),
54 'custom_sync_name': ( str, '{episode.basename}' ),
55 'custom_sync_name_enabled': ( bool, True ),
56 'max_downloads': ( int, 3 ),
57 'max_downloads_enabled': ( bool, False ),
58 'limit_rate': ( bool, False ),
59 'limit_rate_value': ( float, 500.0 ),
60 'bittorrent_dir': (str, default_bittorrent_dir
),
61 'episode_old_age': ( int, 7 ),
63 # Boolean config flags
64 'update_on_startup': ( bool, False ),
65 'auto_download_when_minimized': (bool, False),
66 'use_gnome_bittorrent': ( bool, True ),
67 'only_sync_not_played': ( bool, False ),
68 'proxy_use_environment': ( bool, True ),
69 'update_tags': ( bool, False ),
70 'fssync_channel_subfolders': ( bool, True ),
71 'on_sync_mark_played': ( bool, False ),
72 'on_sync_delete': ( bool, False ),
73 'auto_remove_old_episodes': ( bool, False ),
74 'auto_update_feeds': (bool, False),
75 'auto_update_frequency': (int, 20),
76 'episode_list_descriptions': (bool, True),
77 'show_toolbar': (bool, True),
78 'ipod_write_gtkpod_extended': (bool, False),
79 'mp3_player_delete_played': (bool, False),
81 # Tray icon and notification settings
82 'display_tray_icon': (bool, False),
83 'minimize_to_tray': (bool, False),
84 'start_iconified': (bool, False),
85 'enable_notifications': (bool, True),
86 'on_quit_ask': (bool, True),
88 # Bluetooth-related settings
89 'bluetooth_enabled': (bool, False),
90 'bluetooth_ask_always': (bool, True),
91 'bluetooth_ask_never': (bool, False),
92 'bluetooth_device_name': (str, 'No device'),
93 'bluetooth_device_address': (str, '00:00:00:00:00:00'),
94 'bluetooth_use_converter': (bool, False),
95 'bluetooth_converter': (str, ''),
97 # Settings that are updated directly in code
98 'ipod_mount': ( str, '/media/ipod' ),
99 'mp3_player_folder': ( str, '/media/usbdisk' ),
100 'device_type': ( str, 'none' ),
101 'download_dir': (str, default_download_dir
),
103 # Special settings (not in preferences)
104 'default_new': ( int, 1 ),
105 'use_si_units': ( bool, False ),
106 'on_quit_systray': (bool, False),
107 'create_m3u_playlists': (bool, False),
108 'max_episodes_per_feed': (int, 200),
109 'mp3_player_use_scrobbler_log': (bool, False),
110 'mp3_player_max_filename_length': (int, 100),
111 'show_podcast_url_entry': (bool, True),
112 'maemo_allow_custom_player': (bool, False),
113 'rockbox_copy_coverart' : (bool, False),
114 'rockbox_coverart_size' : (int, 100),
115 'experimental_file_naming': (bool, False),
116 'podcast_list_icon_size': (int, 32),
117 'cmd_all_downloads_complete': (str, ''),
118 'cmd_download_complete': (str, ''),
119 'max_simulaneous_feeds_updating': (int, 3),
120 'color_updating_feeds': (str, '#7db023'),
122 # Hide the cover/pill from the podcast sidebar when it gets too small
123 'podcast_sidebar_save_space': (bool, False),
125 # Window and paned positions
126 'main_window_x': ( int, 100 ),
127 'main_window_y': ( int, 100 ),
128 'main_window_width': ( int, 700 ),
129 'main_window_height': ( int, 500 ),
130 'paned_position': ( int, 200 ),
134 Settings
= gPodderSettings
136 # Number of seconds after which settings are auto-saved
137 WRITE_TO_DISK_TIMEOUT
= 60
139 def __init__( self
, filename
= 'gpodder.conf'):
141 self
.__save
_thread
= None
142 self
.__filename
= filename
143 self
.__section
= 'gpodder-conf-1'
144 self
.__ignore
_window
_events
= False
145 self
.__observers
= []
146 # Name, Type, Value, Type(python type), Editable?, Font style, Boolean?, Boolean value
147 self
.__model
= gtk
.ListStore(str, str, str, object, bool, int, bool, bool)
149 atexit
.register( self
.__atexit
)
153 def __getattr__( self
, name
):
154 if name
in self
.Settings
:
155 ( fieldtype
, default
) = self
.Settings
[name
]
160 def add_observer(self
, callback
):
162 Add a callback function as observer. This callback
163 will be called when a setting changes. It should
166 observer(name, old_value, new_value)
168 The "name" is the setting name, the "old_value" is
169 the value that has been overwritten with "new_value".
171 if callback
not in self
.__observers
:
172 self
.__observers
.append(callback
)
174 log('Observer already added: %s', repr(callback
), sender
=self
)
176 def connect_gtk_editable( self
, name
, editable
):
177 if name
in self
.Settings
:
178 editable
.delete_text( 0, -1)
179 editable
.insert_text( str(getattr( self
, name
)))
180 editable
.connect( 'changed', lambda editable
: setattr( self
, name
, editable
.get_chars( 0, -1)))
182 raise ValueError( '%s is not a setting' % name
)
184 def connect_gtk_spinbutton( self
, name
, spinbutton
):
185 if name
in self
.Settings
:
186 spinbutton
.set_value( getattr( self
, name
))
187 spinbutton
.connect( 'value-changed', lambda spinbutton
: setattr( self
, name
, spinbutton
.get_value()))
189 raise ValueError( '%s is not a setting' % name
)
191 def connect_gtk_paned( self
, name
, paned
):
192 if name
in self
.Settings
:
193 paned
.set_position( getattr( self
, name
))
194 paned_child
= paned
.get_child1()
195 paned_child
.connect( 'size-allocate', lambda x
, y
: setattr( self
, name
, paned
.get_position()))
197 raise ValueError( '%s is not a setting' % name
)
199 def connect_gtk_togglebutton( self
, name
, togglebutton
):
200 if name
in self
.Settings
:
201 togglebutton
.set_active( getattr( self
, name
))
202 togglebutton
.connect( 'toggled', lambda togglebutton
: setattr( self
, name
, togglebutton
.get_active()))
204 raise ValueError( '%s is not a setting' % name
)
206 def filechooser_selection_changed(self
, name
, filechooser
):
207 filename
= filechooser
.get_filename()
208 if filename
is not None:
209 setattr(self
, name
, filename
)
211 def connect_gtk_filechooser(self
, name
, filechooser
, is_for_files
=False):
212 if name
in self
.Settings
:
214 # A FileChooser for a single file
215 filechooser
.set_filename(getattr(self
, name
))
217 # A FileChooser for a folder
218 filechooser
.set_current_folder(getattr(self
, name
))
219 filechooser
.connect('selection-changed', lambda filechooser
: self
.filechooser_selection_changed(name
, filechooser
))
221 raise ValueError('%s is not a setting'%name
)
223 def receive_configure_event( self
, widget
, event
, config_prefix
):
224 ( x
, y
, width
, height
) = map( lambda x
: config_prefix
+ '_' + x
, [ 'x', 'y', 'width', 'height' ])
225 ( x_pos
, y_pos
) = widget
.get_position()
226 ( width_size
, height_size
) = widget
.get_size()
227 if not self
.__ignore
_window
_events
:
228 setattr( self
, x
, x_pos
)
229 setattr( self
, y
, y_pos
)
230 setattr( self
, width
, width_size
)
231 setattr( self
, height
, height_size
)
233 def enable_window_events(self
):
234 self
.__ignore
_window
_events
= False
236 def disable_window_events(self
):
237 self
.__ignore
_window
_events
= True
239 def connect_gtk_window( self
, window
, config_prefix
= 'main_window'):
240 ( x
, y
, width
, height
) = map( lambda x
: config_prefix
+ '_' + x
, [ 'x', 'y', 'width', 'height' ])
241 if set( ( x
, y
, width
, height
)).issubset( set( self
.Settings
)):
242 window
.resize( getattr( self
, width
), getattr( self
, height
))
243 window
.move( getattr( self
, x
), getattr( self
, y
))
244 self
.disable_window_events()
245 util
.idle_add(self
.enable_window_events
)
246 window
.connect( 'configure-event', self
.receive_configure_event
, config_prefix
)
248 raise ValueError( 'Missing settings in set: %s' % ', '.join( ( x
, y
, width
, height
)))
250 def schedule_save( self
):
251 if self
.__save
_thread
is None:
252 self
.__save
_thread
= threading
.Thread( target
= self
.save_thread_proc
)
253 self
.__save
_thread
.start()
255 def save_thread_proc( self
):
256 for i
in range(self
.WRITE_TO_DISK_TIMEOUT
*10):
257 if self
.__save
_thread
is not None:
259 if self
.__save
_thread
is not None:
263 if self
.__save
_thread
is not None:
266 def save( self
, filename
= None):
267 if filename
is not None:
268 self
.__filename
= filename
270 log( 'Flushing settings to disk', sender
= self
)
272 parser
= ConfigParser
.RawConfigParser()
273 parser
.add_section( self
.__section
)
275 for ( key
, ( fieldtype
, default
) ) in self
.Settings
.items():
276 parser
.set( self
.__section
, key
, getattr( self
, key
, default
))
279 parser
.write( open( self
.__filename
, 'w'))
281 raise IOError( 'Cannot write to file: %s' % self
.__filename
)
283 self
.__save
_thread
= None
285 def load( self
, filename
= None):
286 if filename
is not None:
287 self
.__filename
= filename
291 parser
= ConfigParser
.RawConfigParser()
293 parser
.read( self
.__filename
)
297 for key
in sorted(self
.Settings
):
298 (fieldtype
, default
) = self
.Settings
[key
]
301 value
= parser
.getint( self
.__section
, key
)
302 elif fieldtype
== float:
303 value
= parser
.getfloat( self
.__section
, key
)
304 elif fieldtype
== bool:
305 value
= parser
.getboolean( self
.__section
, key
)
307 value
= fieldtype(parser
.get( self
.__section
, key
))
313 style
= pango
.STYLE_NORMAL
315 style
= pango
.STYLE_ITALIC
317 self
.__model
.append([key
, self
.type_as_string(fieldtype
), str(value
), fieldtype
, fieldtype
is not bool, style
, fieldtype
is bool, bool(value
)])
322 def toggle_flag(self
, name
):
323 if name
in self
.Settings
:
324 (fieldtype
, default
) = self
.Settings
[name
]
325 if fieldtype
== bool:
326 setattr(self
, name
, not getattr(self
, name
))
328 log('Cannot toggle value: %s (not boolean)', name
, sender
=self
)
330 log('Invalid setting name: %s', name
, sender
=self
)
332 def update_field(self
, name
, new_value
):
333 if name
in self
.Settings
:
334 (fieldtype
, default
) = self
.Settings
[name
]
336 new_value
= fieldtype(new_value
)
338 log('Cannot convert "%s" to %s. Ignoring.', str(new_value
), fieldtype
.__name
__, sender
=self
)
340 setattr(self
, name
, new_value
)
343 log('Invalid setting name: %s', name
, sender
=self
)
346 def type_as_string(self
, type):
356 def __setattr__( self
, name
, value
):
357 if name
in self
.Settings
:
358 ( fieldtype
, default
) = self
.Settings
[name
]
360 if self
[name
] != fieldtype(value
):
361 log( 'Update: %s = %s', name
, value
, sender
= self
)
362 old_value
= self
[name
]
363 self
[name
] = fieldtype(value
)
364 for observer
in self
.__observers
:
366 # Notify observer about config change
367 observer(name
, old_value
, self
[name
])
369 log('Error while calling observer: %s', repr(observer
), sender
=self
)
370 for row
in self
.__model
:
372 value
= fieldtype(value
)
375 if self
[name
] == default
:
376 style
= pango
.STYLE_NORMAL
378 style
= pango
.STYLE_ITALIC
382 raise ValueError( '%s has to be of type %s' % ( name
, fieldtype
.__name
__ ))
384 object.__setattr
__( self
, name
, value
)