Properly update existing episodes (bug 211)
[gpodder.git] / src / gpodder / config.py
blobb9db0f8b59e48b94c2bd7a06d2c45b3014b02061
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'),
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 ),
133 class Config(dict):
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'):
140 dict.__init__( self)
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)
151 self.load()
153 def __getattr__( self, name):
154 if name in self.Settings:
155 ( fieldtype, default ) = self.Settings[name]
156 return self[name]
157 else:
158 raise AttributeError
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
164 have this signature:
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)
173 else:
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)))
181 else:
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()))
188 else:
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()))
196 else:
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()))
203 else:
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:
213 if is_for_files:
214 # A FileChooser for a single file
215 filechooser.set_filename(getattr(self, name))
216 else:
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))
220 else:
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)
247 else:
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:
258 time.sleep( .1)
259 if self.__save_thread is not None:
260 self.save()
262 def __atexit( self):
263 if self.__save_thread is not None:
264 self.save()
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))
278 try:
279 parser.write( open( self.__filename, 'w'))
280 except:
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
289 self.__model.clear()
291 parser = ConfigParser.RawConfigParser()
292 try:
293 parser.read( self.__filename)
294 except:
295 pass
297 for key in sorted(self.Settings):
298 (fieldtype, default) = self.Settings[key]
299 try:
300 if fieldtype == int:
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)
306 else:
307 value = fieldtype(parser.get( self.__section, key))
308 except:
309 value = default
311 self[key] = value
312 if value == default:
313 style = pango.STYLE_NORMAL
314 else:
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)])
319 def model(self):
320 return self.__model
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))
327 else:
328 log('Cannot toggle value: %s (not boolean)', name, sender=self)
329 else:
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]
335 try:
336 new_value = fieldtype(new_value)
337 except:
338 log('Cannot convert "%s" to %s. Ignoring.', str(new_value), fieldtype.__name__, sender=self)
339 return False
340 setattr(self, name, new_value)
341 return True
342 else:
343 log('Invalid setting name: %s', name, sender=self)
344 return False
346 def type_as_string(self, type):
347 if type == int:
348 return _('Integer')
349 elif type == float:
350 return _('Float')
351 elif type == bool:
352 return _('Boolean')
353 else:
354 return _('String')
356 def __setattr__( self, name, value):
357 if name in self.Settings:
358 ( fieldtype, default ) = self.Settings[name]
359 try:
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:
365 try:
366 # Notify observer about config change
367 observer(name, old_value, self[name])
368 except:
369 log('Error while calling observer: %s', repr(observer), sender=self)
370 for row in self.__model:
371 if row[0] == name:
372 value = fieldtype(value)
373 row[2] = str(value)
374 row[7] = bool(value)
375 if self[name] == default:
376 style = pango.STYLE_NORMAL
377 else:
378 style = pango.STYLE_ITALIC
379 row[5] = style
380 self.schedule_save()
381 except:
382 raise ValueError( '%s has to be of type %s' % ( name, fieldtype.__name__ ))
383 else:
384 object.__setattr__( self, name, value)