M3U write support for Sandisk Sansa (bug 251)
[gpodder.git] / src / gpodder / config.py
blob7eeb0db723e27e3756a6b93ee229893c4d2831ec
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
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),
129 # Hide the cover/pill from the podcast sidebar when it gets too small
130 'podcast_sidebar_save_space': (bool, False),
132 # Settings for my.gpodder.org
133 'my_gpodder_username': (str, ''),
134 'my_gpodder_password': (str, ''),
135 'my_gpodder_autoupload': (bool, False),
137 # Paned position
138 'paned_position': ( int, 200 ),
141 # Helper function to add window-specific properties (position and size)
142 def window_props(config_prefix, x=100, y=100, width=700, height=500):
143 return {
144 config_prefix+'_x': (int, x),
145 config_prefix+'_y': (int, y),
146 config_prefix+'_width': (int, width),
147 config_prefix+'_height': (int, height),
148 config_prefix+'_maximized': (bool, False),
151 # Register window-specific properties
152 gPodderSettings.update(window_props('main_window', width=700, height=500))
153 gPodderSettings.update(window_props('episode_selector', width=600, height=400))
154 gPodderSettings.update(window_props('episode_window', width=500, height=400))
157 class Config(dict):
158 Settings = gPodderSettings
160 # Number of seconds after which settings are auto-saved
161 WRITE_TO_DISK_TIMEOUT = 60
163 def __init__( self, filename = 'gpodder.conf'):
164 dict.__init__( self)
165 self.__save_thread = None
166 self.__filename = filename
167 self.__section = 'gpodder-conf-1'
168 self.__ignore_window_events = False
169 self.__observers = []
170 # Name, Type, Value, Type(python type), Editable?, Font style, Boolean?, Boolean value
171 self.__model = gtk.ListStore(str, str, str, object, bool, int, bool, bool)
173 atexit.register( self.__atexit)
175 self.load()
177 def __getattr__( self, name):
178 if name in self.Settings:
179 ( fieldtype, default ) = self.Settings[name]
180 return self[name]
181 else:
182 raise AttributeError
184 def add_observer(self, callback):
186 Add a callback function as observer. This callback
187 will be called when a setting changes. It should
188 have this signature:
190 observer(name, old_value, new_value)
192 The "name" is the setting name, the "old_value" is
193 the value that has been overwritten with "new_value".
195 if callback not in self.__observers:
196 self.__observers.append(callback)
197 else:
198 log('Observer already added: %s', repr(callback), sender=self)
200 def connect_gtk_editable( self, name, editable):
201 if name in self.Settings:
202 editable.delete_text( 0, -1)
203 editable.insert_text( str(getattr( self, name)))
204 editable.connect( 'changed', lambda editable: setattr( self, name, editable.get_chars( 0, -1)))
205 else:
206 raise ValueError( '%s is not a setting' % name)
208 def connect_gtk_spinbutton( self, name, spinbutton):
209 if name in self.Settings:
210 spinbutton.set_value( getattr( self, name))
211 spinbutton.connect( 'value-changed', lambda spinbutton: setattr( self, name, spinbutton.get_value()))
212 else:
213 raise ValueError( '%s is not a setting' % name)
215 def connect_gtk_paned( self, name, paned):
216 if name in self.Settings:
217 paned.set_position( getattr( self, name))
218 paned_child = paned.get_child1()
219 paned_child.connect( 'size-allocate', lambda x, y: setattr( self, name, paned.get_position()))
220 else:
221 raise ValueError( '%s is not a setting' % name)
223 def connect_gtk_togglebutton( self, name, togglebutton):
224 if name in self.Settings:
225 togglebutton.set_active( getattr( self, name))
226 togglebutton.connect( 'toggled', lambda togglebutton: setattr( self, name, togglebutton.get_active()))
227 else:
228 raise ValueError( '%s is not a setting' % name)
230 def filechooser_selection_changed(self, name, filechooser):
231 filename = filechooser.get_filename()
232 if filename is not None:
233 setattr(self, name, filename)
235 def connect_gtk_filechooser(self, name, filechooser, is_for_files=False):
236 if name in self.Settings:
237 if is_for_files:
238 # A FileChooser for a single file
239 filechooser.set_filename(getattr(self, name))
240 else:
241 # A FileChooser for a folder
242 filechooser.set_current_folder(getattr(self, name))
243 filechooser.connect('selection-changed', lambda filechooser: self.filechooser_selection_changed(name, filechooser))
244 else:
245 raise ValueError('%s is not a setting'%name)
247 def receive_configure_event( self, widget, event, config_prefix):
248 (x, y, width, height, maximized) = map(lambda x: config_prefix + '_' + x, ['x', 'y', 'width', 'height', 'maximized'])
249 ( x_pos, y_pos ) = widget.get_position()
250 ( width_size, height_size ) = widget.get_size()
251 if not self.__ignore_window_events and not (hasattr(self, maximized) and getattr(self, maximized)):
252 setattr( self, x, x_pos)
253 setattr( self, y, y_pos)
254 setattr( self, width, width_size)
255 setattr( self, height, height_size)
257 def receive_window_state(self, widget, event, config_prefix):
258 if hasattr(self, config_prefix+'_maximized'):
259 setattr(self, config_prefix+'_maximized', bool(event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED))
261 def enable_window_events(self):
262 self.__ignore_window_events = False
264 def disable_window_events(self):
265 self.__ignore_window_events = True
267 def connect_gtk_window( self, window, config_prefix, show_window=False):
268 (x, y, width, height, maximized) = map(lambda x: config_prefix + '_' + x, ['x', 'y', 'width', 'height', 'maximized'])
269 if set( ( x, y, width, height )).issubset( set( self.Settings)):
270 window.resize( getattr( self, width), getattr( self, height))
271 window.move( getattr( self, x), getattr( self, y))
272 self.disable_window_events()
273 util.idle_add(self.enable_window_events)
274 window.connect('configure-event', self.receive_configure_event, config_prefix)
275 window.connect('window-state-event', self.receive_window_state, config_prefix)
276 if show_window:
277 window.show()
278 if hasattr(self, maximized) and getattr(self, maximized) == True:
279 window.maximize()
280 else:
281 raise ValueError( 'Missing settings in set: %s' % ', '.join( ( x, y, width, height )))
283 def schedule_save( self):
284 if self.__save_thread is None:
285 self.__save_thread = threading.Thread( target = self.save_thread_proc)
286 self.__save_thread.start()
288 def save_thread_proc( self):
289 for i in range(self.WRITE_TO_DISK_TIMEOUT*10):
290 if self.__save_thread is not None:
291 time.sleep( .1)
292 if self.__save_thread is not None:
293 self.save()
295 def __atexit( self):
296 if self.__save_thread is not None:
297 self.save()
299 def save( self, filename = None):
300 if filename is not None:
301 self.__filename = filename
303 log( 'Flushing settings to disk', sender = self)
305 parser = ConfigParser.RawConfigParser()
306 parser.add_section( self.__section)
308 for ( key, ( fieldtype, default ) ) in self.Settings.items():
309 parser.set( self.__section, key, getattr( self, key, default))
311 try:
312 parser.write( open( self.__filename, 'w'))
313 except:
314 raise IOError( 'Cannot write to file: %s' % self.__filename)
316 self.__save_thread = None
318 def load( self, filename = None):
319 if filename is not None:
320 self.__filename = filename
322 self.__model.clear()
324 parser = ConfigParser.RawConfigParser()
325 try:
326 parser.read( self.__filename)
327 except:
328 pass
330 for key in sorted(self.Settings):
331 (fieldtype, default) = self.Settings[key]
332 try:
333 if fieldtype == int:
334 value = parser.getint( self.__section, key)
335 elif fieldtype == float:
336 value = parser.getfloat( self.__section, key)
337 elif fieldtype == bool:
338 value = parser.getboolean( self.__section, key)
339 else:
340 value = fieldtype(parser.get( self.__section, key))
341 except:
342 value = default
344 self[key] = value
345 if value == default:
346 style = pango.STYLE_NORMAL
347 else:
348 style = pango.STYLE_ITALIC
350 self.__model.append([key, self.type_as_string(fieldtype), str(value), fieldtype, fieldtype is not bool, style, fieldtype is bool, bool(value)])
352 def model(self):
353 return self.__model
355 def toggle_flag(self, name):
356 if name in self.Settings:
357 (fieldtype, default) = self.Settings[name]
358 if fieldtype == bool:
359 setattr(self, name, not getattr(self, name))
360 else:
361 log('Cannot toggle value: %s (not boolean)', name, sender=self)
362 else:
363 log('Invalid setting name: %s', name, sender=self)
365 def update_field(self, name, new_value):
366 if name in self.Settings:
367 (fieldtype, default) = self.Settings[name]
368 try:
369 new_value = fieldtype(new_value)
370 except:
371 log('Cannot convert "%s" to %s. Ignoring.', str(new_value), fieldtype.__name__, sender=self)
372 return False
373 setattr(self, name, new_value)
374 return True
375 else:
376 log('Invalid setting name: %s', name, sender=self)
377 return False
379 def type_as_string(self, type):
380 if type == int:
381 return _('Integer')
382 elif type == float:
383 return _('Float')
384 elif type == bool:
385 return _('Boolean')
386 else:
387 return _('String')
389 def __setattr__( self, name, value):
390 if name in self.Settings:
391 ( fieldtype, default ) = self.Settings[name]
392 try:
393 if self[name] != fieldtype(value):
394 log( 'Update: %s = %s', name, value, sender = self)
395 old_value = self[name]
396 self[name] = fieldtype(value)
397 for observer in self.__observers:
398 try:
399 # Notify observer about config change
400 observer(name, old_value, self[name])
401 except:
402 log('Error while calling observer: %s', repr(observer), sender=self)
403 for row in self.__model:
404 if row[0] == name:
405 value = fieldtype(value)
406 row[2] = str(value)
407 row[7] = bool(value)
408 if self[name] == default:
409 style = pango.STYLE_NORMAL
410 else:
411 style = pango.STYLE_ITALIC
412 row[5] = style
413 self.schedule_save()
414 except:
415 raise ValueError( '%s has to be of type %s' % ( name, fieldtype.__name__ ))
416 else:
417 object.__setattr__( self, name, value)