Support for YouTube in CoverDownloader.
[gpodder.git] / src / gpodder / config.py
blob304ae4b06a6350553fc24ebc80bded864e1c9b25
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_bittorrent_dir = '/media/mmc2/gpodder/torrents'
42 default_download_dir = '/media/mmc2/gpodder/downloads'
43 else:
44 default_bittorrent_dir = os.path.expanduser('~/gpodder-downloads/torrents')
45 default_download_dir = os.path.expanduser('~/gpodder-downloads')
47 gPodderSettings = {
48 # General settings
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'),
121 'log_sqlite': (bool, False),
123 # Hide the cover/pill from the podcast sidebar when it gets too small
124 'podcast_sidebar_save_space': (bool, False),
126 # Window and paned positions
127 'main_window_x': ( int, 100 ),
128 'main_window_y': ( int, 100 ),
129 'main_window_width': ( int, 700 ),
130 'main_window_height': ( int, 500 ),
131 'paned_position': ( int, 200 ),
134 class Config(dict):
135 Settings = gPodderSettings
137 # Number of seconds after which settings are auto-saved
138 WRITE_TO_DISK_TIMEOUT = 60
140 def __init__( self, filename = 'gpodder.conf'):
141 dict.__init__( self)
142 self.__save_thread = None
143 self.__filename = filename
144 self.__section = 'gpodder-conf-1'
145 self.__ignore_window_events = False
146 self.__observers = []
147 # Name, Type, Value, Type(python type), Editable?, Font style, Boolean?, Boolean value
148 self.__model = gtk.ListStore(str, str, str, object, bool, int, bool, bool)
150 atexit.register( self.__atexit)
152 self.load()
154 def __getattr__( self, name):
155 if name in self.Settings:
156 ( fieldtype, default ) = self.Settings[name]
157 return self[name]
158 else:
159 raise AttributeError
161 def add_observer(self, callback):
163 Add a callback function as observer. This callback
164 will be called when a setting changes. It should
165 have this signature:
167 observer(name, old_value, new_value)
169 The "name" is the setting name, the "old_value" is
170 the value that has been overwritten with "new_value".
172 if callback not in self.__observers:
173 self.__observers.append(callback)
174 else:
175 log('Observer already added: %s', repr(callback), sender=self)
177 def connect_gtk_editable( self, name, editable):
178 if name in self.Settings:
179 editable.delete_text( 0, -1)
180 editable.insert_text( str(getattr( self, name)))
181 editable.connect( 'changed', lambda editable: setattr( self, name, editable.get_chars( 0, -1)))
182 else:
183 raise ValueError( '%s is not a setting' % name)
185 def connect_gtk_spinbutton( self, name, spinbutton):
186 if name in self.Settings:
187 spinbutton.set_value( getattr( self, name))
188 spinbutton.connect( 'value-changed', lambda spinbutton: setattr( self, name, spinbutton.get_value()))
189 else:
190 raise ValueError( '%s is not a setting' % name)
192 def connect_gtk_paned( self, name, paned):
193 if name in self.Settings:
194 paned.set_position( getattr( self, name))
195 paned_child = paned.get_child1()
196 paned_child.connect( 'size-allocate', lambda x, y: setattr( self, name, paned.get_position()))
197 else:
198 raise ValueError( '%s is not a setting' % name)
200 def connect_gtk_togglebutton( self, name, togglebutton):
201 if name in self.Settings:
202 togglebutton.set_active( getattr( self, name))
203 togglebutton.connect( 'toggled', lambda togglebutton: setattr( self, name, togglebutton.get_active()))
204 else:
205 raise ValueError( '%s is not a setting' % name)
207 def filechooser_selection_changed(self, name, filechooser):
208 filename = filechooser.get_filename()
209 if filename is not None:
210 setattr(self, name, filename)
212 def connect_gtk_filechooser(self, name, filechooser, is_for_files=False):
213 if name in self.Settings:
214 if is_for_files:
215 # A FileChooser for a single file
216 filechooser.set_filename(getattr(self, name))
217 else:
218 # A FileChooser for a folder
219 filechooser.set_current_folder(getattr(self, name))
220 filechooser.connect('selection-changed', lambda filechooser: self.filechooser_selection_changed(name, filechooser))
221 else:
222 raise ValueError('%s is not a setting'%name)
224 def receive_configure_event( self, widget, event, config_prefix):
225 ( x, y, width, height ) = map( lambda x: config_prefix + '_' + x, [ 'x', 'y', 'width', 'height' ])
226 ( x_pos, y_pos ) = widget.get_position()
227 ( width_size, height_size ) = widget.get_size()
228 if not self.__ignore_window_events:
229 setattr( self, x, x_pos)
230 setattr( self, y, y_pos)
231 setattr( self, width, width_size)
232 setattr( self, height, height_size)
234 def enable_window_events(self):
235 self.__ignore_window_events = False
237 def disable_window_events(self):
238 self.__ignore_window_events = True
240 def connect_gtk_window( self, window, config_prefix = 'main_window'):
241 ( x, y, width, height ) = map( lambda x: config_prefix + '_' + x, [ 'x', 'y', 'width', 'height' ])
242 if set( ( x, y, width, height )).issubset( set( self.Settings)):
243 window.resize( getattr( self, width), getattr( self, height))
244 window.move( getattr( self, x), getattr( self, y))
245 self.disable_window_events()
246 util.idle_add(self.enable_window_events)
247 window.connect( 'configure-event', self.receive_configure_event, config_prefix)
248 else:
249 raise ValueError( 'Missing settings in set: %s' % ', '.join( ( x, y, width, height )))
251 def schedule_save( self):
252 if self.__save_thread is None:
253 self.__save_thread = threading.Thread( target = self.save_thread_proc)
254 self.__save_thread.start()
256 def save_thread_proc( self):
257 for i in range(self.WRITE_TO_DISK_TIMEOUT*10):
258 if self.__save_thread is not None:
259 time.sleep( .1)
260 if self.__save_thread is not None:
261 self.save()
263 def __atexit( self):
264 if self.__save_thread is not None:
265 self.save()
267 def save( self, filename = None):
268 if filename is not None:
269 self.__filename = filename
271 log( 'Flushing settings to disk', sender = self)
273 parser = ConfigParser.RawConfigParser()
274 parser.add_section( self.__section)
276 for ( key, ( fieldtype, default ) ) in self.Settings.items():
277 parser.set( self.__section, key, getattr( self, key, default))
279 try:
280 parser.write( open( self.__filename, 'w'))
281 except:
282 raise IOError( 'Cannot write to file: %s' % self.__filename)
284 self.__save_thread = None
286 def load( self, filename = None):
287 if filename is not None:
288 self.__filename = filename
290 self.__model.clear()
292 parser = ConfigParser.RawConfigParser()
293 try:
294 parser.read( self.__filename)
295 except:
296 pass
298 for key in sorted(self.Settings):
299 (fieldtype, default) = self.Settings[key]
300 try:
301 if fieldtype == int:
302 value = parser.getint( self.__section, key)
303 elif fieldtype == float:
304 value = parser.getfloat( self.__section, key)
305 elif fieldtype == bool:
306 value = parser.getboolean( self.__section, key)
307 else:
308 value = fieldtype(parser.get( self.__section, key))
309 except:
310 value = default
312 self[key] = value
313 if value == default:
314 style = pango.STYLE_NORMAL
315 else:
316 style = pango.STYLE_ITALIC
318 self.__model.append([key, self.type_as_string(fieldtype), str(value), fieldtype, fieldtype is not bool, style, fieldtype is bool, bool(value)])
320 def model(self):
321 return self.__model
323 def toggle_flag(self, name):
324 if name in self.Settings:
325 (fieldtype, default) = self.Settings[name]
326 if fieldtype == bool:
327 setattr(self, name, not getattr(self, name))
328 else:
329 log('Cannot toggle value: %s (not boolean)', name, sender=self)
330 else:
331 log('Invalid setting name: %s', name, sender=self)
333 def update_field(self, name, new_value):
334 if name in self.Settings:
335 (fieldtype, default) = self.Settings[name]
336 try:
337 new_value = fieldtype(new_value)
338 except:
339 log('Cannot convert "%s" to %s. Ignoring.', str(new_value), fieldtype.__name__, sender=self)
340 return False
341 setattr(self, name, new_value)
342 return True
343 else:
344 log('Invalid setting name: %s', name, sender=self)
345 return False
347 def type_as_string(self, type):
348 if type == int:
349 return _('Integer')
350 elif type == float:
351 return _('Float')
352 elif type == bool:
353 return _('Boolean')
354 else:
355 return _('String')
357 def __setattr__( self, name, value):
358 if name in self.Settings:
359 ( fieldtype, default ) = self.Settings[name]
360 try:
361 if self[name] != fieldtype(value):
362 log( 'Update: %s = %s', name, value, sender = self)
363 old_value = self[name]
364 self[name] = fieldtype(value)
365 for observer in self.__observers:
366 try:
367 # Notify observer about config change
368 observer(name, old_value, self[name])
369 except:
370 log('Error while calling observer: %s', repr(observer), sender=self)
371 for row in self.__model:
372 if row[0] == name:
373 value = fieldtype(value)
374 row[2] = str(value)
375 row[7] = bool(value)
376 if self[name] == default:
377 style = pango.STYLE_NORMAL
378 else:
379 style = pango.STYLE_ITALIC
380 row[5] = style
381 self.schedule_save()
382 except:
383 raise ValueError( '%s has to be of type %s' % ( name, fieldtype.__name__ ))
384 else:
385 object.__setattr__( self, name, value)