Update copyright info from 2005-2008 to 2005-2009
[gpodder.git] / src / gpodder / config.py
blob6a72880ef8cddbbcf6e59c0e125afad8c091d57e
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2009 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
27 import gtk
28 import pango
30 import gpodder
31 from gpodder import util
32 from gpodder.liblogger import log
34 import atexit
35 import os.path
36 import time
37 import threading
38 import ConfigParser
40 if gpodder.interface == gpodder.MAEMO:
41 default_download_dir = '/media/mmc2/gpodder'
42 else:
43 default_download_dir = os.path.expanduser('~/gpodder-downloads')
45 gPodderSettings = {
46 # General settings
47 'player': (str, 'default'),
48 'videoplayer': (str, 'unspecified'),
49 'opml_url': (str, 'http://gpodder.org/directory.opml'),
50 'toplist_url': (str, 'http://gpodder.org/toplist.opml'),
51 'http_proxy': ( str, '' ),
52 'ftp_proxy': ( str, '' ),
53 'custom_sync_name': ( str, '{episode.basename}' ),
54 'custom_sync_name_enabled': ( bool, True ),
55 'max_downloads': ( int, 3 ),
56 'max_downloads_enabled': ( bool, False ),
57 'limit_rate': ( bool, False ),
58 'limit_rate_value': ( float, 500.0 ),
59 'episode_old_age': ( int, 7 ),
61 # Boolean config flags
62 'update_on_startup': ( bool, False ),
63 'auto_download_when_minimized': (bool, False),
64 'only_sync_not_played': ( bool, False ),
65 'proxy_use_environment': ( bool, True ),
66 'update_tags': ( bool, False ),
67 'fssync_channel_subfolders': ( bool, True ),
68 'on_sync_mark_played': ( bool, False ),
69 'on_sync_delete': ( bool, False ),
70 'auto_remove_old_episodes': ( bool, False ),
71 'auto_update_feeds': (bool, False),
72 'auto_update_frequency': (int, 20),
73 'episode_list_descriptions': (bool, True),
74 'show_toolbar': (bool, True),
75 'ipod_write_gtkpod_extended': (bool, False),
76 'ipod_purge_old_episodes': (bool, False),
77 'mp3_player_delete_played': (bool, False),
79 # Tray icon and notification settings
80 'display_tray_icon': (bool, False),
81 'minimize_to_tray': (bool, False),
82 'start_iconified': (bool, False),
83 'enable_notifications': (bool, True),
84 'on_quit_ask': (bool, True),
86 # Bluetooth-related settings
87 'bluetooth_use_device_address': (bool, False),
88 'bluetooth_device_address': (str, '00:00:00:00:00:00'),
89 'bluetooth_use_converter': (bool, False),
90 'bluetooth_converter': (str, ''),
92 # Settings that are updated directly in code
93 'ipod_mount': ( str, '/media/ipod' ),
94 'mp3_player_folder': ( str, '/media/usbdisk' ),
95 'device_type': ( str, 'none' ),
96 'download_dir': (str, default_download_dir),
98 # Playlist Management settings
99 'mp3_player_playlist_file': (str, 'PLAYLISTS/gpodder.m3u'),
100 'mp3_player_playlist_absolute_path': (bool, True),
101 'mp3_player_playlist_win_path': (bool, True),
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_url_entry_in_podcast_list': (bool, False),
112 'maemo_allow_custom_player': (bool, False),
113 'rockbox_copy_coverart' : (bool, False),
114 'rockbox_coverart_size' : (int, 100),
115 'custom_player_copy_coverart' : (bool, False),
116 'custom_player_coverart_size' : (int, 176),
117 'custom_player_coverart_name' : (str, 'folder.jpg'),
118 'custom_player_coverart_format' : (str, 'JPEG'),
119 'experimental_file_naming': (bool, False),
120 'podcast_list_icon_size': (int, 32),
121 'cmd_all_downloads_complete': (str, ''),
122 'cmd_download_complete': (str, ''),
123 'enable_streaming': (bool, False),
124 'max_simulaneous_feeds_updating': (int, 3),
125 'color_updating_feeds': (str, '#7db023'),
126 'log_sqlite': (bool, False),
127 'enable_html_shownotes': (bool, True),
128 'maemo_enable_gestures': (bool, False),
130 # Hide the cover/pill from the podcast sidebar when it gets too small
131 'podcast_sidebar_save_space': (bool, False),
133 # Settings for my.gpodder.org
134 'my_gpodder_username': (str, ''),
135 'my_gpodder_password': (str, ''),
136 'my_gpodder_autoupload': (bool, False),
138 # Paned position
139 'paned_position': ( int, 200 ),
142 # Helper function to add window-specific properties (position and size)
143 def window_props(config_prefix, x=100, y=100, width=700, height=500):
144 return {
145 config_prefix+'_x': (int, x),
146 config_prefix+'_y': (int, y),
147 config_prefix+'_width': (int, width),
148 config_prefix+'_height': (int, height),
149 config_prefix+'_maximized': (bool, False),
152 # Register window-specific properties
153 gPodderSettings.update(window_props('main_window', width=700, height=500))
154 gPodderSettings.update(window_props('episode_selector', width=600, height=400))
155 gPodderSettings.update(window_props('episode_window', width=500, height=400))
158 class Config(dict):
159 Settings = gPodderSettings
161 # Number of seconds after which settings are auto-saved
162 WRITE_TO_DISK_TIMEOUT = 60
164 def __init__( self, filename = 'gpodder.conf'):
165 dict.__init__( self)
166 self.__save_thread = None
167 self.__filename = filename
168 self.__section = 'gpodder-conf-1'
169 self.__ignore_window_events = False
170 self.__observers = []
171 # Name, Type, Value, Type(python type), Editable?, Font style, Boolean?, Boolean value
172 self.__model = gtk.ListStore(str, str, str, object, bool, int, bool, bool)
174 atexit.register( self.__atexit)
176 self.load()
178 def __getattr__( self, name):
179 if name in self.Settings:
180 ( fieldtype, default ) = self.Settings[name]
181 return self[name]
182 else:
183 raise AttributeError('%s is not a setting' % name)
185 def add_observer(self, callback):
187 Add a callback function as observer. This callback
188 will be called when a setting changes. It should
189 have this signature:
191 observer(name, old_value, new_value)
193 The "name" is the setting name, the "old_value" is
194 the value that has been overwritten with "new_value".
196 if callback not in self.__observers:
197 self.__observers.append(callback)
198 else:
199 log('Observer already added: %s', repr(callback), sender=self)
201 def connect_gtk_editable( self, name, editable):
202 if name in self.Settings:
203 editable.delete_text( 0, -1)
204 editable.insert_text( str(getattr( self, name)))
205 editable.connect( 'changed', lambda editable: setattr( self, name, editable.get_chars( 0, -1)))
206 else:
207 raise ValueError( '%s is not a setting' % name)
209 def connect_gtk_spinbutton( self, name, spinbutton):
210 if name in self.Settings:
211 spinbutton.set_value( getattr( self, name))
212 spinbutton.connect( 'value-changed', lambda spinbutton: setattr( self, name, spinbutton.get_value()))
213 else:
214 raise ValueError( '%s is not a setting' % name)
216 def connect_gtk_paned( self, name, paned):
217 if name in self.Settings:
218 paned.set_position( getattr( self, name))
219 paned_child = paned.get_child1()
220 paned_child.connect( 'size-allocate', lambda x, y: setattr( self, name, paned.get_position()))
221 else:
222 raise ValueError( '%s is not a setting' % name)
224 def connect_gtk_togglebutton( self, name, togglebutton):
225 if name in self.Settings:
226 togglebutton.set_active( getattr( self, name))
227 togglebutton.connect( 'toggled', lambda togglebutton: setattr( self, name, togglebutton.get_active()))
228 else:
229 raise ValueError( '%s is not a setting' % name)
231 def filechooser_selection_changed(self, name, filechooser):
232 filename = filechooser.get_filename()
233 if filename is not None:
234 setattr(self, name, filename)
236 def connect_gtk_filechooser(self, name, filechooser, is_for_files=False):
237 if name in self.Settings:
238 if is_for_files:
239 # A FileChooser for a single file
240 filechooser.set_filename(getattr(self, name))
241 else:
242 # A FileChooser for a folder
243 filechooser.set_current_folder(getattr(self, name))
244 filechooser.connect('selection-changed', lambda filechooser: self.filechooser_selection_changed(name, filechooser))
245 else:
246 raise ValueError('%s is not a setting'%name)
248 def receive_configure_event( self, widget, event, config_prefix):
249 (x, y, width, height, maximized) = map(lambda x: config_prefix + '_' + x, ['x', 'y', 'width', 'height', 'maximized'])
250 ( x_pos, y_pos ) = widget.get_position()
251 ( width_size, height_size ) = widget.get_size()
252 if not self.__ignore_window_events and not (hasattr(self, maximized) and getattr(self, maximized)):
253 setattr( self, x, x_pos)
254 setattr( self, y, y_pos)
255 setattr( self, width, width_size)
256 setattr( self, height, height_size)
258 def receive_window_state(self, widget, event, config_prefix):
259 if hasattr(self, config_prefix+'_maximized'):
260 setattr(self, config_prefix+'_maximized', bool(event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED))
262 def enable_window_events(self):
263 self.__ignore_window_events = False
265 def disable_window_events(self):
266 self.__ignore_window_events = True
268 def connect_gtk_window( self, window, config_prefix, show_window=False):
269 (x, y, width, height, maximized) = map(lambda x: config_prefix + '_' + x, ['x', 'y', 'width', 'height', 'maximized'])
270 if set( ( x, y, width, height )).issubset( set( self.Settings)):
271 window.resize( getattr( self, width), getattr( self, height))
272 window.move( getattr( self, x), getattr( self, y))
273 self.disable_window_events()
274 util.idle_add(self.enable_window_events)
275 window.connect('configure-event', self.receive_configure_event, config_prefix)
276 window.connect('window-state-event', self.receive_window_state, config_prefix)
277 if show_window:
278 window.show()
279 if hasattr(self, maximized) and getattr(self, maximized) == True:
280 window.maximize()
281 else:
282 raise ValueError( 'Missing settings in set: %s' % ', '.join( ( x, y, width, height )))
284 def schedule_save( self):
285 if self.__save_thread is None:
286 self.__save_thread = threading.Thread( target = self.save_thread_proc)
287 self.__save_thread.start()
289 def save_thread_proc( self):
290 for i in range(self.WRITE_TO_DISK_TIMEOUT*10):
291 if self.__save_thread is not None:
292 time.sleep( .1)
293 if self.__save_thread is not None:
294 self.save()
296 def __atexit( self):
297 if self.__save_thread is not None:
298 self.save()
300 def save( self, filename = None):
301 if filename is not None:
302 self.__filename = filename
304 log( 'Flushing settings to disk', sender = self)
306 parser = ConfigParser.RawConfigParser()
307 parser.add_section( self.__section)
309 for ( key, ( fieldtype, default ) ) in self.Settings.items():
310 parser.set( self.__section, key, getattr( self, key, default))
312 try:
313 parser.write( open( self.__filename, 'w'))
314 except:
315 raise IOError( 'Cannot write to file: %s' % self.__filename)
317 self.__save_thread = None
319 def load( self, filename = None):
320 if filename is not None:
321 self.__filename = filename
323 self.__model.clear()
325 parser = ConfigParser.RawConfigParser()
326 try:
327 parser.read( self.__filename)
328 except:
329 pass
331 for key in sorted(self.Settings):
332 (fieldtype, default) = self.Settings[key]
333 try:
334 if fieldtype == int:
335 value = parser.getint( self.__section, key)
336 elif fieldtype == float:
337 value = parser.getfloat( self.__section, key)
338 elif fieldtype == bool:
339 value = parser.getboolean( self.__section, key)
340 else:
341 value = fieldtype(parser.get( self.__section, key))
342 except:
343 value = default
345 self[key] = value
346 if value == default:
347 style = pango.STYLE_NORMAL
348 else:
349 style = pango.STYLE_ITALIC
351 self.__model.append([key, self.type_as_string(fieldtype), str(value), fieldtype, fieldtype is not bool, style, fieldtype is bool, bool(value)])
353 def model(self):
354 return self.__model
356 def toggle_flag(self, name):
357 if name in self.Settings:
358 (fieldtype, default) = self.Settings[name]
359 if fieldtype == bool:
360 setattr(self, name, not getattr(self, name))
361 else:
362 log('Cannot toggle value: %s (not boolean)', name, sender=self)
363 else:
364 log('Invalid setting name: %s', name, sender=self)
366 def update_field(self, name, new_value):
367 if name in self.Settings:
368 (fieldtype, default) = self.Settings[name]
369 try:
370 new_value = fieldtype(new_value)
371 except:
372 log('Cannot convert "%s" to %s. Ignoring.', str(new_value), fieldtype.__name__, sender=self)
373 return False
374 setattr(self, name, new_value)
375 return True
376 else:
377 log('Invalid setting name: %s', name, sender=self)
378 return False
380 def type_as_string(self, type):
381 if type == int:
382 return _('Integer')
383 elif type == float:
384 return _('Float')
385 elif type == bool:
386 return _('Boolean')
387 else:
388 return _('String')
390 def __setattr__( self, name, value):
391 if name in self.Settings:
392 ( fieldtype, default ) = self.Settings[name]
393 try:
394 if self[name] != fieldtype(value):
395 log( 'Update: %s = %s', name, value, sender = self)
396 old_value = self[name]
397 self[name] = fieldtype(value)
398 for observer in self.__observers:
399 try:
400 # Notify observer about config change
401 observer(name, old_value, self[name])
402 except:
403 log('Error while calling observer: %s', repr(observer), sender=self)
404 for row in self.__model:
405 if row[0] == name:
406 value = fieldtype(value)
407 row[2] = str(value)
408 row[7] = bool(value)
409 if self[name] == default:
410 style = pango.STYLE_NORMAL
411 else:
412 style = pango.STYLE_ITALIC
413 row[5] = style
414 self.schedule_save()
415 except:
416 raise ValueError( '%s has to be of type %s' % ( name, fieldtype.__name__ ))
417 else:
418 object.__setattr__( self, name, value)