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
31 from gpodder
import util
32 from gpodder
.liblogger
import log
40 if gpodder
.interface
== gpodder
.MAEMO
:
41 default_bittorrent_dir
= '/media/mmc2/gpodder/torrents'
42 default_download_dir
= '/media/mmc2/gpodder/downloads'
44 default_bittorrent_dir
= os
.path
.expanduser('~/gpodder-downloads/torrents')
45 default_download_dir
= os
.path
.expanduser('~/gpodder-downloads')
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 ),
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'):
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
)
154 def __getattr__( self
, name
):
155 if name
in self
.Settings
:
156 ( fieldtype
, default
) = self
.Settings
[name
]
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
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
)
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)))
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()))
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()))
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()))
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
:
215 # A FileChooser for a single file
216 filechooser
.set_filename(getattr(self
, name
))
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
))
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
)
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:
260 if self
.__save
_thread
is not None:
264 if self
.__save
_thread
is not None:
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
))
280 parser
.write( open( self
.__filename
, 'w'))
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
292 parser
= ConfigParser
.RawConfigParser()
294 parser
.read( self
.__filename
)
298 for key
in sorted(self
.Settings
):
299 (fieldtype
, default
) = self
.Settings
[key
]
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
)
308 value
= fieldtype(parser
.get( self
.__section
, key
))
314 style
= pango
.STYLE_NORMAL
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
)])
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
))
329 log('Cannot toggle value: %s (not boolean)', name
, sender
=self
)
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
]
337 new_value
= fieldtype(new_value
)
339 log('Cannot convert "%s" to %s. Ignoring.', str(new_value
), fieldtype
.__name
__, sender
=self
)
341 setattr(self
, name
, new_value
)
344 log('Invalid setting name: %s', name
, sender
=self
)
347 def type_as_string(self
, type):
357 def __setattr__( self
, name
, value
):
358 if name
in self
.Settings
:
359 ( fieldtype
, default
) = self
.Settings
[name
]
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
:
367 # Notify observer about config change
368 observer(name
, old_value
, self
[name
])
370 log('Error while calling observer: %s', repr(observer
), sender
=self
)
371 for row
in self
.__model
:
373 value
= fieldtype(value
)
376 if self
[name
] == default
:
377 style
= pango
.STYLE_NORMAL
379 style
= pango
.STYLE_ITALIC
383 raise ValueError( '%s has to be of type %s' % ( name
, fieldtype
.__name
__ ))
385 object.__setattr
__( self
, name
, value
)