Optionally remove old episodes from iPod
[gpodder.git] / src / gpodder / config.py
blobe3db8c3f2ff9207e3fc5bb9a58f93f3bdcecce17
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://www.gpodder.org/directory.opml' ),
50 'http_proxy': ( str, '' ),
51 'ftp_proxy': ( str, '' ),
52 'custom_sync_name': ( str, '{episode.basename}' ),
53 'custom_sync_name_enabled': ( bool, True ),
54 'max_downloads': ( int, 3 ),
55 'max_downloads_enabled': ( bool, False ),
56 'limit_rate': ( bool, False ),
57 'limit_rate_value': ( float, 500.0 ),
58 'episode_old_age': ( int, 7 ),
60 # Boolean config flags
61 'update_on_startup': ( bool, False ),
62 'auto_download_when_minimized': (bool, False),
63 'only_sync_not_played': ( bool, False ),
64 'proxy_use_environment': ( bool, True ),
65 'update_tags': ( bool, False ),
66 'fssync_channel_subfolders': ( bool, True ),
67 'on_sync_mark_played': ( bool, False ),
68 'on_sync_delete': ( bool, False ),
69 'auto_remove_old_episodes': ( bool, False ),
70 'auto_update_feeds': (bool, False),
71 'auto_update_frequency': (int, 20),
72 'episode_list_descriptions': (bool, True),
73 'show_toolbar': (bool, True),
74 'ipod_write_gtkpod_extended': (bool, False),
75 'ipod_purge_old_episodes': (bool, False),
76 'mp3_player_delete_played': (bool, False),
78 # Tray icon and notification settings
79 'display_tray_icon': (bool, False),
80 'minimize_to_tray': (bool, False),
81 'start_iconified': (bool, False),
82 'enable_notifications': (bool, True),
83 'on_quit_ask': (bool, True),
85 # Bluetooth-related settings
86 'bluetooth_enabled': (bool, False),
87 'bluetooth_ask_always': (bool, True),
88 'bluetooth_ask_never': (bool, False),
89 'bluetooth_device_name': (str, 'No device'),
90 'bluetooth_device_address': (str, '00:00:00:00:00:00'),
91 'bluetooth_use_converter': (bool, False),
92 'bluetooth_converter': (str, ''),
94 # Settings that are updated directly in code
95 'ipod_mount': ( str, '/media/ipod' ),
96 'mp3_player_folder': ( str, '/media/usbdisk' ),
97 'device_type': ( str, 'none' ),
98 'download_dir': (str, default_download_dir),
100 # Playlist Management settings
101 'mp3_player_playlist_file': (str, 'PLAYLISTS/gpodder.m3u'),
102 'mp3_player_playlist_absolute_path': (bool, False),
103 'mp3_player_playlist_win_path': (bool, True),
105 # Special settings (not in preferences)
106 'default_new': ( int, 1 ),
107 'use_si_units': ( bool, False ),
108 'on_quit_systray': (bool, False),
109 'create_m3u_playlists': (bool, False),
110 'max_episodes_per_feed': (int, 200),
111 'mp3_player_use_scrobbler_log': (bool, False),
112 'mp3_player_max_filename_length': (int, 100),
113 'show_url_entry_in_podcast_list': (bool, False),
114 'maemo_allow_custom_player': (bool, False),
115 'rockbox_copy_coverart' : (bool, False),
116 'rockbox_coverart_size' : (int, 100),
117 'custom_player_copy_coverart' : (bool, False),
118 'custom_player_coverart_size' : (int, 176),
119 'custom_player_coverart_name' : (str, 'folder.jpg'),
120 'custom_player_coverart_format' : (str, 'JPEG'),
121 'experimental_file_naming': (bool, False),
122 'podcast_list_icon_size': (int, 32),
123 'cmd_all_downloads_complete': (str, ''),
124 'cmd_download_complete': (str, ''),
125 'enable_streaming': (bool, False),
126 'max_simulaneous_feeds_updating': (int, 3),
127 'color_updating_feeds': (str, '#7db023'),
128 'log_sqlite': (bool, False),
130 # Hide the cover/pill from the podcast sidebar when it gets too small
131 'podcast_sidebar_save_space': (bool, False),
133 # Window and paned positions
134 'main_window_x': ( int, 100 ),
135 'main_window_y': ( int, 100 ),
136 'main_window_width': ( int, 700 ),
137 'main_window_height': ( int, 500 ),
138 'paned_position': ( int, 200 ),
141 class Config(dict):
142 Settings = gPodderSettings
144 # Number of seconds after which settings are auto-saved
145 WRITE_TO_DISK_TIMEOUT = 60
147 def __init__( self, filename = 'gpodder.conf'):
148 dict.__init__( self)
149 self.__save_thread = None
150 self.__filename = filename
151 self.__section = 'gpodder-conf-1'
152 self.__ignore_window_events = False
153 self.__observers = []
154 # Name, Type, Value, Type(python type), Editable?, Font style, Boolean?, Boolean value
155 self.__model = gtk.ListStore(str, str, str, object, bool, int, bool, bool)
157 atexit.register( self.__atexit)
159 self.load()
161 def __getattr__( self, name):
162 if name in self.Settings:
163 ( fieldtype, default ) = self.Settings[name]
164 return self[name]
165 else:
166 raise AttributeError
168 def add_observer(self, callback):
170 Add a callback function as observer. This callback
171 will be called when a setting changes. It should
172 have this signature:
174 observer(name, old_value, new_value)
176 The "name" is the setting name, the "old_value" is
177 the value that has been overwritten with "new_value".
179 if callback not in self.__observers:
180 self.__observers.append(callback)
181 else:
182 log('Observer already added: %s', repr(callback), sender=self)
184 def connect_gtk_editable( self, name, editable):
185 if name in self.Settings:
186 editable.delete_text( 0, -1)
187 editable.insert_text( str(getattr( self, name)))
188 editable.connect( 'changed', lambda editable: setattr( self, name, editable.get_chars( 0, -1)))
189 else:
190 raise ValueError( '%s is not a setting' % name)
192 def connect_gtk_spinbutton( self, name, spinbutton):
193 if name in self.Settings:
194 spinbutton.set_value( getattr( self, name))
195 spinbutton.connect( 'value-changed', lambda spinbutton: setattr( self, name, spinbutton.get_value()))
196 else:
197 raise ValueError( '%s is not a setting' % name)
199 def connect_gtk_paned( self, name, paned):
200 if name in self.Settings:
201 paned.set_position( getattr( self, name))
202 paned_child = paned.get_child1()
203 paned_child.connect( 'size-allocate', lambda x, y: setattr( self, name, paned.get_position()))
204 else:
205 raise ValueError( '%s is not a setting' % name)
207 def connect_gtk_togglebutton( self, name, togglebutton):
208 if name in self.Settings:
209 togglebutton.set_active( getattr( self, name))
210 togglebutton.connect( 'toggled', lambda togglebutton: setattr( self, name, togglebutton.get_active()))
211 else:
212 raise ValueError( '%s is not a setting' % name)
214 def filechooser_selection_changed(self, name, filechooser):
215 filename = filechooser.get_filename()
216 if filename is not None:
217 setattr(self, name, filename)
219 def connect_gtk_filechooser(self, name, filechooser, is_for_files=False):
220 if name in self.Settings:
221 if is_for_files:
222 # A FileChooser for a single file
223 filechooser.set_filename(getattr(self, name))
224 else:
225 # A FileChooser for a folder
226 filechooser.set_current_folder(getattr(self, name))
227 filechooser.connect('selection-changed', lambda filechooser: self.filechooser_selection_changed(name, filechooser))
228 else:
229 raise ValueError('%s is not a setting'%name)
231 def receive_configure_event( self, widget, event, config_prefix):
232 ( x, y, width, height ) = map( lambda x: config_prefix + '_' + x, [ 'x', 'y', 'width', 'height' ])
233 ( x_pos, y_pos ) = widget.get_position()
234 ( width_size, height_size ) = widget.get_size()
235 if not self.__ignore_window_events:
236 setattr( self, x, x_pos)
237 setattr( self, y, y_pos)
238 setattr( self, width, width_size)
239 setattr( self, height, height_size)
241 def enable_window_events(self):
242 self.__ignore_window_events = False
244 def disable_window_events(self):
245 self.__ignore_window_events = True
247 def connect_gtk_window( self, window, config_prefix = 'main_window'):
248 ( x, y, width, height ) = map( lambda x: config_prefix + '_' + x, [ 'x', 'y', 'width', 'height' ])
249 if set( ( x, y, width, height )).issubset( set( self.Settings)):
250 window.resize( getattr( self, width), getattr( self, height))
251 window.move( getattr( self, x), getattr( self, y))
252 self.disable_window_events()
253 util.idle_add(self.enable_window_events)
254 window.connect( 'configure-event', self.receive_configure_event, config_prefix)
255 else:
256 raise ValueError( 'Missing settings in set: %s' % ', '.join( ( x, y, width, height )))
258 def schedule_save( self):
259 if self.__save_thread is None:
260 self.__save_thread = threading.Thread( target = self.save_thread_proc)
261 self.__save_thread.start()
263 def save_thread_proc( self):
264 for i in range(self.WRITE_TO_DISK_TIMEOUT*10):
265 if self.__save_thread is not None:
266 time.sleep( .1)
267 if self.__save_thread is not None:
268 self.save()
270 def __atexit( self):
271 if self.__save_thread is not None:
272 self.save()
274 def save( self, filename = None):
275 if filename is not None:
276 self.__filename = filename
278 log( 'Flushing settings to disk', sender = self)
280 parser = ConfigParser.RawConfigParser()
281 parser.add_section( self.__section)
283 for ( key, ( fieldtype, default ) ) in self.Settings.items():
284 parser.set( self.__section, key, getattr( self, key, default))
286 try:
287 parser.write( open( self.__filename, 'w'))
288 except:
289 raise IOError( 'Cannot write to file: %s' % self.__filename)
291 self.__save_thread = None
293 def load( self, filename = None):
294 if filename is not None:
295 self.__filename = filename
297 self.__model.clear()
299 parser = ConfigParser.RawConfigParser()
300 try:
301 parser.read( self.__filename)
302 except:
303 pass
305 for key in sorted(self.Settings):
306 (fieldtype, default) = self.Settings[key]
307 try:
308 if fieldtype == int:
309 value = parser.getint( self.__section, key)
310 elif fieldtype == float:
311 value = parser.getfloat( self.__section, key)
312 elif fieldtype == bool:
313 value = parser.getboolean( self.__section, key)
314 else:
315 value = fieldtype(parser.get( self.__section, key))
316 except:
317 value = default
319 self[key] = value
320 if value == default:
321 style = pango.STYLE_NORMAL
322 else:
323 style = pango.STYLE_ITALIC
325 self.__model.append([key, self.type_as_string(fieldtype), str(value), fieldtype, fieldtype is not bool, style, fieldtype is bool, bool(value)])
327 def model(self):
328 return self.__model
330 def toggle_flag(self, name):
331 if name in self.Settings:
332 (fieldtype, default) = self.Settings[name]
333 if fieldtype == bool:
334 setattr(self, name, not getattr(self, name))
335 else:
336 log('Cannot toggle value: %s (not boolean)', name, sender=self)
337 else:
338 log('Invalid setting name: %s', name, sender=self)
340 def update_field(self, name, new_value):
341 if name in self.Settings:
342 (fieldtype, default) = self.Settings[name]
343 try:
344 new_value = fieldtype(new_value)
345 except:
346 log('Cannot convert "%s" to %s. Ignoring.', str(new_value), fieldtype.__name__, sender=self)
347 return False
348 setattr(self, name, new_value)
349 return True
350 else:
351 log('Invalid setting name: %s', name, sender=self)
352 return False
354 def type_as_string(self, type):
355 if type == int:
356 return _('Integer')
357 elif type == float:
358 return _('Float')
359 elif type == bool:
360 return _('Boolean')
361 else:
362 return _('String')
364 def __setattr__( self, name, value):
365 if name in self.Settings:
366 ( fieldtype, default ) = self.Settings[name]
367 try:
368 if self[name] != fieldtype(value):
369 log( 'Update: %s = %s', name, value, sender = self)
370 old_value = self[name]
371 self[name] = fieldtype(value)
372 for observer in self.__observers:
373 try:
374 # Notify observer about config change
375 observer(name, old_value, self[name])
376 except:
377 log('Error while calling observer: %s', repr(observer), sender=self)
378 for row in self.__model:
379 if row[0] == name:
380 value = fieldtype(value)
381 row[2] = str(value)
382 row[7] = bool(value)
383 if self[name] == default:
384 style = pango.STYLE_NORMAL
385 else:
386 style = pango.STYLE_ITALIC
387 row[5] = style
388 self.schedule_save()
389 except:
390 raise ValueError( '%s has to be of type %s' % ( name, fieldtype.__name__ ))
391 else:
392 object.__setattr__( self, name, value)