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_download_dir
= '/media/mmc2/gpodder'
43 default_download_dir
= os
.path
.expanduser('~/gpodder-downloads')
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 ),
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'):
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
)
161 def __getattr__( self
, name
):
162 if name
in self
.Settings
:
163 ( fieldtype
, default
) = self
.Settings
[name
]
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
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
)
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)))
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()))
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()))
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()))
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
:
222 # A FileChooser for a single file
223 filechooser
.set_filename(getattr(self
, name
))
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
))
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
)
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:
267 if self
.__save
_thread
is not None:
271 if self
.__save
_thread
is not None:
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
))
287 parser
.write( open( self
.__filename
, 'w'))
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
299 parser
= ConfigParser
.RawConfigParser()
301 parser
.read( self
.__filename
)
305 for key
in sorted(self
.Settings
):
306 (fieldtype
, default
) = self
.Settings
[key
]
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
)
315 value
= fieldtype(parser
.get( self
.__section
, key
))
321 style
= pango
.STYLE_NORMAL
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
)])
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
))
336 log('Cannot toggle value: %s (not boolean)', name
, sender
=self
)
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
]
344 new_value
= fieldtype(new_value
)
346 log('Cannot convert "%s" to %s. Ignoring.', str(new_value
), fieldtype
.__name
__, sender
=self
)
348 setattr(self
, name
, new_value
)
351 log('Invalid setting name: %s', name
, sender
=self
)
354 def type_as_string(self
, type):
364 def __setattr__( self
, name
, value
):
365 if name
in self
.Settings
:
366 ( fieldtype
, default
) = self
.Settings
[name
]
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
:
374 # Notify observer about config change
375 observer(name
, old_value
, self
[name
])
377 log('Error while calling observer: %s', repr(observer
), sender
=self
)
378 for row
in self
.__model
:
380 value
= fieldtype(value
)
383 if self
[name
] == default
:
384 style
= pango
.STYLE_NORMAL
386 style
= pango
.STYLE_ITALIC
390 raise ValueError( '%s has to be of type %s' % ( name
, fieldtype
.__name
__ ))
392 object.__setattr
__( self
, name
, value
)