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://gpodder.org/directory.opml'),
50 'toplist_url': (str, 'http://gpodder.org/toplist.opml'),
51 'http_proxy': ( str, '' ),
52 'ftp_proxy': ( str, '' ),
53 'custom_sync_name': ( str, '{episode.basename}' ),
54 'custom_sync_name_enabled': ( bool, True ),
55 'max_downloads': ( int, 3 ),
56 'max_downloads_enabled': ( bool, False ),
57 'limit_rate': ( bool, False ),
58 'limit_rate_value': ( float, 500.0 ),
59 'episode_old_age': ( int, 7 ),
61 # Boolean config flags
62 'update_on_startup': ( bool, False ),
63 'auto_download_when_minimized': (bool, False),
64 'only_sync_not_played': ( bool, False ),
65 'proxy_use_environment': ( bool, True ),
66 'update_tags': ( bool, False ),
67 'fssync_channel_subfolders': ( bool, True ),
68 'on_sync_mark_played': ( bool, False ),
69 'on_sync_delete': ( bool, False ),
70 'auto_remove_old_episodes': ( bool, False ),
71 'auto_update_feeds': (bool, False),
72 'auto_update_frequency': (int, 20),
73 'episode_list_descriptions': (bool, True),
74 'show_toolbar': (bool, True),
75 'ipod_write_gtkpod_extended': (bool, False),
76 'ipod_purge_old_episodes': (bool, False),
77 'mp3_player_delete_played': (bool, False),
79 # Tray icon and notification settings
80 'display_tray_icon': (bool, False),
81 'minimize_to_tray': (bool, False),
82 'start_iconified': (bool, False),
83 'enable_notifications': (bool, True),
84 'on_quit_ask': (bool, True),
86 # Bluetooth-related settings
87 'bluetooth_use_device_address': (bool, False),
88 'bluetooth_device_address': (str, '00:00:00:00:00:00'),
89 'bluetooth_use_converter': (bool, False),
90 'bluetooth_converter': (str, ''),
92 # Settings that are updated directly in code
93 'ipod_mount': ( str, '/media/ipod' ),
94 'mp3_player_folder': ( str, '/media/usbdisk' ),
95 'device_type': ( str, 'none' ),
96 'download_dir': (str, default_download_dir
),
98 # Playlist Management settings
99 'mp3_player_playlist_file': (str, 'PLAYLISTS/gpodder.m3u'),
100 'mp3_player_playlist_absolute_path': (bool, True),
101 'mp3_player_playlist_win_path': (bool, True),
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_url_entry_in_podcast_list': (bool, False),
112 'maemo_allow_custom_player': (bool, False),
113 'rockbox_copy_coverart' : (bool, False),
114 'rockbox_coverart_size' : (int, 100),
115 'custom_player_copy_coverart' : (bool, False),
116 'custom_player_coverart_size' : (int, 176),
117 'custom_player_coverart_name' : (str, 'folder.jpg'),
118 'custom_player_coverart_format' : (str, 'JPEG'),
119 'experimental_file_naming': (bool, False),
120 'podcast_list_icon_size': (int, 32),
121 'cmd_all_downloads_complete': (str, ''),
122 'cmd_download_complete': (str, ''),
123 'enable_streaming': (bool, False),
124 'max_simulaneous_feeds_updating': (int, 3),
125 'color_updating_feeds': (str, '#7db023'),
126 'log_sqlite': (bool, False),
127 'enable_html_shownotes': (bool, True),
129 # Hide the cover/pill from the podcast sidebar when it gets too small
130 'podcast_sidebar_save_space': (bool, False),
132 # Settings for my.gpodder.org
133 'my_gpodder_username': (str, ''),
134 'my_gpodder_password': (str, ''),
135 'my_gpodder_autoupload': (bool, False),
138 'paned_position': ( int, 200 ),
141 # Helper function to add window-specific properties (position and size)
142 def window_props(config_prefix
, x
=100, y
=100, width
=700, height
=500):
144 config_prefix
+'_x': (int, x
),
145 config_prefix
+'_y': (int, y
),
146 config_prefix
+'_width': (int, width
),
147 config_prefix
+'_height': (int, height
),
148 config_prefix
+'_maximized': (bool, False),
151 # Register window-specific properties
152 gPodderSettings
.update(window_props('main_window', width
=700, height
=500))
153 gPodderSettings
.update(window_props('episode_selector', width
=600, height
=400))
154 gPodderSettings
.update(window_props('episode_window', width
=500, height
=400))
158 Settings
= gPodderSettings
160 # Number of seconds after which settings are auto-saved
161 WRITE_TO_DISK_TIMEOUT
= 60
163 def __init__( self
, filename
= 'gpodder.conf'):
165 self
.__save
_thread
= None
166 self
.__filename
= filename
167 self
.__section
= 'gpodder-conf-1'
168 self
.__ignore
_window
_events
= False
169 self
.__observers
= []
170 # Name, Type, Value, Type(python type), Editable?, Font style, Boolean?, Boolean value
171 self
.__model
= gtk
.ListStore(str, str, str, object, bool, int, bool, bool)
173 atexit
.register( self
.__atexit
)
177 def __getattr__( self
, name
):
178 if name
in self
.Settings
:
179 ( fieldtype
, default
) = self
.Settings
[name
]
184 def add_observer(self
, callback
):
186 Add a callback function as observer. This callback
187 will be called when a setting changes. It should
190 observer(name, old_value, new_value)
192 The "name" is the setting name, the "old_value" is
193 the value that has been overwritten with "new_value".
195 if callback
not in self
.__observers
:
196 self
.__observers
.append(callback
)
198 log('Observer already added: %s', repr(callback
), sender
=self
)
200 def connect_gtk_editable( self
, name
, editable
):
201 if name
in self
.Settings
:
202 editable
.delete_text( 0, -1)
203 editable
.insert_text( str(getattr( self
, name
)))
204 editable
.connect( 'changed', lambda editable
: setattr( self
, name
, editable
.get_chars( 0, -1)))
206 raise ValueError( '%s is not a setting' % name
)
208 def connect_gtk_spinbutton( self
, name
, spinbutton
):
209 if name
in self
.Settings
:
210 spinbutton
.set_value( getattr( self
, name
))
211 spinbutton
.connect( 'value-changed', lambda spinbutton
: setattr( self
, name
, spinbutton
.get_value()))
213 raise ValueError( '%s is not a setting' % name
)
215 def connect_gtk_paned( self
, name
, paned
):
216 if name
in self
.Settings
:
217 paned
.set_position( getattr( self
, name
))
218 paned_child
= paned
.get_child1()
219 paned_child
.connect( 'size-allocate', lambda x
, y
: setattr( self
, name
, paned
.get_position()))
221 raise ValueError( '%s is not a setting' % name
)
223 def connect_gtk_togglebutton( self
, name
, togglebutton
):
224 if name
in self
.Settings
:
225 togglebutton
.set_active( getattr( self
, name
))
226 togglebutton
.connect( 'toggled', lambda togglebutton
: setattr( self
, name
, togglebutton
.get_active()))
228 raise ValueError( '%s is not a setting' % name
)
230 def filechooser_selection_changed(self
, name
, filechooser
):
231 filename
= filechooser
.get_filename()
232 if filename
is not None:
233 setattr(self
, name
, filename
)
235 def connect_gtk_filechooser(self
, name
, filechooser
, is_for_files
=False):
236 if name
in self
.Settings
:
238 # A FileChooser for a single file
239 filechooser
.set_filename(getattr(self
, name
))
241 # A FileChooser for a folder
242 filechooser
.set_current_folder(getattr(self
, name
))
243 filechooser
.connect('selection-changed', lambda filechooser
: self
.filechooser_selection_changed(name
, filechooser
))
245 raise ValueError('%s is not a setting'%name
)
247 def receive_configure_event( self
, widget
, event
, config_prefix
):
248 (x
, y
, width
, height
, maximized
) = map(lambda x
: config_prefix
+ '_' + x
, ['x', 'y', 'width', 'height', 'maximized'])
249 ( x_pos
, y_pos
) = widget
.get_position()
250 ( width_size
, height_size
) = widget
.get_size()
251 if not self
.__ignore
_window
_events
and not (hasattr(self
, maximized
) and getattr(self
, maximized
)):
252 setattr( self
, x
, x_pos
)
253 setattr( self
, y
, y_pos
)
254 setattr( self
, width
, width_size
)
255 setattr( self
, height
, height_size
)
257 def receive_window_state(self
, widget
, event
, config_prefix
):
258 if hasattr(self
, config_prefix
+'_maximized'):
259 setattr(self
, config_prefix
+'_maximized', bool(event
.new_window_state
& gtk
.gdk
.WINDOW_STATE_MAXIMIZED
))
261 def enable_window_events(self
):
262 self
.__ignore
_window
_events
= False
264 def disable_window_events(self
):
265 self
.__ignore
_window
_events
= True
267 def connect_gtk_window( self
, window
, config_prefix
, show_window
=False):
268 (x
, y
, width
, height
, maximized
) = map(lambda x
: config_prefix
+ '_' + x
, ['x', 'y', 'width', 'height', 'maximized'])
269 if set( ( x
, y
, width
, height
)).issubset( set( self
.Settings
)):
270 window
.resize( getattr( self
, width
), getattr( self
, height
))
271 window
.move( getattr( self
, x
), getattr( self
, y
))
272 self
.disable_window_events()
273 util
.idle_add(self
.enable_window_events
)
274 window
.connect('configure-event', self
.receive_configure_event
, config_prefix
)
275 window
.connect('window-state-event', self
.receive_window_state
, config_prefix
)
278 if hasattr(self
, maximized
) and getattr(self
, maximized
) == True:
281 raise ValueError( 'Missing settings in set: %s' % ', '.join( ( x
, y
, width
, height
)))
283 def schedule_save( self
):
284 if self
.__save
_thread
is None:
285 self
.__save
_thread
= threading
.Thread( target
= self
.save_thread_proc
)
286 self
.__save
_thread
.start()
288 def save_thread_proc( self
):
289 for i
in range(self
.WRITE_TO_DISK_TIMEOUT
*10):
290 if self
.__save
_thread
is not None:
292 if self
.__save
_thread
is not None:
296 if self
.__save
_thread
is not None:
299 def save( self
, filename
= None):
300 if filename
is not None:
301 self
.__filename
= filename
303 log( 'Flushing settings to disk', sender
= self
)
305 parser
= ConfigParser
.RawConfigParser()
306 parser
.add_section( self
.__section
)
308 for ( key
, ( fieldtype
, default
) ) in self
.Settings
.items():
309 parser
.set( self
.__section
, key
, getattr( self
, key
, default
))
312 parser
.write( open( self
.__filename
, 'w'))
314 raise IOError( 'Cannot write to file: %s' % self
.__filename
)
316 self
.__save
_thread
= None
318 def load( self
, filename
= None):
319 if filename
is not None:
320 self
.__filename
= filename
324 parser
= ConfigParser
.RawConfigParser()
326 parser
.read( self
.__filename
)
330 for key
in sorted(self
.Settings
):
331 (fieldtype
, default
) = self
.Settings
[key
]
334 value
= parser
.getint( self
.__section
, key
)
335 elif fieldtype
== float:
336 value
= parser
.getfloat( self
.__section
, key
)
337 elif fieldtype
== bool:
338 value
= parser
.getboolean( self
.__section
, key
)
340 value
= fieldtype(parser
.get( self
.__section
, key
))
346 style
= pango
.STYLE_NORMAL
348 style
= pango
.STYLE_ITALIC
350 self
.__model
.append([key
, self
.type_as_string(fieldtype
), str(value
), fieldtype
, fieldtype
is not bool, style
, fieldtype
is bool, bool(value
)])
355 def toggle_flag(self
, name
):
356 if name
in self
.Settings
:
357 (fieldtype
, default
) = self
.Settings
[name
]
358 if fieldtype
== bool:
359 setattr(self
, name
, not getattr(self
, name
))
361 log('Cannot toggle value: %s (not boolean)', name
, sender
=self
)
363 log('Invalid setting name: %s', name
, sender
=self
)
365 def update_field(self
, name
, new_value
):
366 if name
in self
.Settings
:
367 (fieldtype
, default
) = self
.Settings
[name
]
369 new_value
= fieldtype(new_value
)
371 log('Cannot convert "%s" to %s. Ignoring.', str(new_value
), fieldtype
.__name
__, sender
=self
)
373 setattr(self
, name
, new_value
)
376 log('Invalid setting name: %s', name
, sender
=self
)
379 def type_as_string(self
, type):
389 def __setattr__( self
, name
, value
):
390 if name
in self
.Settings
:
391 ( fieldtype
, default
) = self
.Settings
[name
]
393 if self
[name
] != fieldtype(value
):
394 log( 'Update: %s = %s', name
, value
, sender
= self
)
395 old_value
= self
[name
]
396 self
[name
] = fieldtype(value
)
397 for observer
in self
.__observers
:
399 # Notify observer about config change
400 observer(name
, old_value
, self
[name
])
402 log('Error while calling observer: %s', repr(observer
), sender
=self
)
403 for row
in self
.__model
:
405 value
= fieldtype(value
)
408 if self
[name
] == default
:
409 style
= pango
.STYLE_NORMAL
411 style
= pango
.STYLE_ITALIC
415 raise ValueError( '%s has to be of type %s' % ( name
, fieldtype
.__name
__ ))
417 object.__setattr
__( self
, name
, value
)