1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 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
28 from gpodder
import util
29 from gpodder
.liblogger
import log
39 if gpodder
.ui
.fremantle
:
40 default_download_dir
= os
.path
.join(os
.path
.expanduser('~'), 'MyDocs/Podcasts')
41 elif gpodder
.ui
.diablo
:
42 default_download_dir
= '/media/mmc2/gpodder'
44 default_download_dir
= os
.path
.join(os
.path
.expanduser('~'), 'gpodder-downloads')
48 'player': (str, 'default',
49 ("The default player for all media, if set to 'default' this will "
50 "attempt to use xdg-open on linux or the built-in media player on maemo.")),
51 'videoplayer': (str, 'default',
52 ("The default player for video")),
53 'opml_url': (str, 'http://gpodder.org/directory.opml',
54 ("A URL pointing to an OPML file which can be used to bulk-add podcasts.")),
55 'toplist_url': (str, 'http://gpodder.org/toplist.opml',
56 ("A URL pointing to a gPodder web services top podcasts list")),
57 'custom_sync_name': ( str, '{episode.basename}',
58 ("The name used when copying a file to a FS-based device. Available "
59 "options are: episode.basename, episode.title, episode.published")),
60 'custom_sync_name_enabled': ( bool, True,
61 ("Enables renaming files when transfered to an FS-based device with "
62 "respect to the 'custom_sync_name'.")),
63 'max_downloads': ( int, 1,
64 ("The maximum number of simultaneous downloads allowed at a single "
65 "time. Requires 'max_downloads_enabled'.")),
66 'max_downloads_enabled': ( bool, True,
67 ("The 'max_downloads' setting will only work if this is set to 'True'.")),
68 'limit_rate': ( bool, False,
69 ("The 'limit_rate_value' setting will only work if this is set to 'True'.")),
70 'limit_rate_value': ( float, 500.0,
71 ("Set a global speed limit (in KB/s) when downloading files. "
72 "Requires 'limit_rate'.")),
73 'episode_old_age': ( int, 7,
74 ("The number of days before an episode is considered old.")),
76 # Boolean config flags
77 'update_on_startup': ( bool, False,
78 ("Update the feed cache on startup.")),
79 'only_sync_not_played': ( bool, False,
80 ("Only sync episodes to a device that have not been marked played in gPodder.")),
81 'fssync_channel_subfolders': ( bool, True,
82 ("Create a directory for every feed when syncing to an FS-based device "
83 "instead of putting all the episodes in a single directory.")),
84 'on_sync_mark_played': ( bool, False,
85 ("After syncing an episode, mark it as played in gPodder.")),
86 'on_sync_delete': ( bool, False,
87 ("After syncing an episode, delete it from gPodder.")),
88 'auto_remove_played_episodes': ( bool, False,
89 ("Auto-remove old episodes that are played.")),
90 'auto_remove_unplayed_episodes': ( bool, False,
91 ("Auto-remove old episodes that are unplayed.")),
92 'auto_update_feeds': (bool, False,
93 ("Automatically update feeds when gPodder is minimized. "
94 "See 'auto_update_frequency' and 'auto_download'.")),
95 'auto_update_frequency': (int, 20,
96 ("The frequency (in minutes) at which gPodder will update all feeds "
97 "if 'auto_update_feeds' is enabled.")),
98 'auto_cleanup_downloads': (bool, True,
99 ('Automatically removed cancelled and finished downloads from the list')),
100 'episode_list_descriptions': (bool, True,
101 ("Display the episode's description under the episode title in the GUI.")),
102 'episode_list_thumbnails': (bool, True,
103 ("Display thumbnails of downloaded image-feed episodes in the list")),
104 'show_toolbar': (bool, True,
105 ("Show the toolbar in the GUI's main window.")),
106 'ipod_purge_old_episodes': (bool, False,
107 ("Remove episodes from an iPod device if they've been marked as played "
108 "on the device and they have no rating set (the rating can be set on "
109 "the device by the user to prevent deletion).")),
110 'ipod_delete_played_from_db': (bool, False,
111 ("Remove episodes from gPodder if they've been marked as played "
112 "on the device and they have no rating set (the rating can be set on "
113 "the device by the user to prevent deletion).")),
114 'ipod_write_gtkpod_extended': (bool, False,
115 ("Write gtkpod extended database.")),
116 'mp3_player_delete_played': (bool, False,
117 ("Removes episodes from an FS-based device that have been marked as "
118 "played in gPodder. Note: only works if 'only_sync_not_played' is "
120 'disable_pre_sync_conversion': (bool, False,
121 ("Disable pre-synchronization conversion of OGG files. This should be "
122 "enabled for deviced that natively support OGG. Eg. Rockbox, iAudio")),
124 # Tray icon and notification settings
125 'display_tray_icon': (bool, False,
126 ("Whether or not gPodder should display an icon in the system tray.")),
127 'minimize_to_tray': (bool, False,
128 ("If 'display_tray_icon' is enabled, when gPodder is minimized it will "
129 "not be visible in the window list.")),
130 'start_iconified': (bool, False,
131 ("When gPodder starts, send it to the tray immediately.")),
132 'enable_notifications': (bool, True,
133 ("Let gPodder use notification bubbles when it can completed certain "
134 "tasks like downloading an episode or finishing syncing to a device.")),
135 'on_quit_ask': (bool, True,
136 ("Ask the user to confirm quitting the application.")),
137 'auto_download': (str, 'never',
138 ("Auto download episodes (never, minimized, always) - Fremantle also supports 'quiet'")),
139 'do_not_show_new_episodes_dialog': (bool, False,
140 ("Do not show the new episodes dialog after updating feed cache when "
141 "gPodder is not minimized")),
144 # Settings that are updated directly in code
145 'ipod_mount': ( str, '/media/ipod',
146 ("The moint point for an iPod Device.")),
147 'mp3_player_folder': ( str, '/media/usbdisk',
148 ("The moint point for an FS-based device.")),
149 'device_type': ( str, 'none',
150 ("The device type: 'mtp', 'filesystem' or 'ipod'")),
151 'download_dir': (str, default_download_dir
,
152 ("The default directory that podcast episodes are downloaded to.")),
154 # Playlist Management settings
155 'mp3_player_playlist_file': (str, 'PLAYLISTS/gpodder.m3u',
156 ("The relative path to where the playlist is stored on an FS-based device.")),
157 'mp3_player_playlist_absolute_path': (bool, True,
158 ("Whether or not the the playlist should contain relative or absolute "
159 "paths; this is dependent on the player.")),
160 'mp3_player_playlist_win_path': (bool, True,
161 ("Whether or not the player requires Windows-style paths in the playlist.")),
163 # Special settings (not in preferences)
164 'on_quit_systray': (bool, False,
165 ("When the 'X' button is clicked do not quit, send gPodder to the tray.")),
166 'max_episodes_per_feed': (int, 200,
167 ("The maximum number of episodes that gPodder will display in the episode "
168 "list. Note: Set this to a lower value on slower hardware to speed up "
169 "rendering of the episode list.")),
170 'mp3_player_use_scrobbler_log': (bool, False,
171 ("Attempt to use a Device's scrobbler.log to mark episodes as played in "
172 "gPodder. Useful for Rockbox players.")),
173 'mp3_player_max_filename_length': (int, 100,
174 ("The maximum filename length for FS-based devices.")),
175 'rockbox_copy_coverart' : (bool, False,
176 ("Create rockbox-compatible coverart and copy it to the device when "
177 "syncing. See: 'rockbox_coverart_size'.")),
178 'rockbox_coverart_size' : (int, 100,
179 ("The width of the coverart for the user's Rockbox player/skin.")),
180 'custom_player_copy_coverart' : (bool, False,
181 ("Create custom coverart for FS-based players.")),
182 'custom_player_coverart_size' : (int, 176,
183 ("The width of the coverart for the user's FS-based player.")),
184 'custom_player_coverart_name' : (str, 'folder.jpg',
185 ("The name of the coverart file accepted by the user's FS-based player.")),
186 'custom_player_coverart_format' : (str, 'JPEG',
187 ("The image format accepted by the user's FS-based player.")),
188 'cmd_all_downloads_complete': (str, '',
189 ("The path to a command that gets run after all downloads are completed.")),
190 'cmd_download_complete': (str, '',
191 ("The path to a command that gets run after a single download completes. "
192 "See http://wiki.gpodder.org/wiki/Time_stretching for more info.")),
193 'enable_html_shownotes': (bool, True,
194 ("Allow HTML to be rendered in the episode information dialog.")),
195 'maemo_enable_gestures': (bool, False,
196 ("Enable fancy gestures on Maemo.")),
197 'sync_disks_after_transfer': (bool, True,
198 ("Call 'sync' after tranfering episodes to a device.")),
199 'enable_fingerscroll': (bool, False,
200 ("Enable the use of finger-scrollable widgets on Maemo.")),
201 'double_click_episode_action': (str, 'shownotes',
202 ("Episode double-click/enter action handler (shownotes, download, stream)")),
203 'on_drag_mark_played': (bool, False,
204 ("Mark episode as played when using drag'n'drop to copy/open it")),
205 'open_torrent_after_download': (bool, False,
206 ("Automatically open torrents after they have finished downloading")),
208 'mtp_audio_folder': (str, '',
209 ("The relative path to where audio podcasts are stored on an MTP device.")),
210 'mtp_video_folder': (str, '',
211 ("The relative path to where video podcasts are stored on an MTP device.")),
212 'mtp_image_folder': (str, '',
213 ("The relative path to where image podcasts are stored on an MTP device.")),
214 'mtp_podcast_folders': (bool, False,
215 ("Whether to create a folder per podcast on MTP devices.")),
217 'allow_empty_feeds': (bool, True,
218 ('Allow subscribing to feeds without episodes')),
220 'episode_list_view_mode': (int, 1, # "Hide deleted episodes" (see gtkui/model.py)
221 ('Internally used (current view mode)')),
222 'podcast_list_view_mode': (int, 1, # Only on Fremantle
223 ('Internally used (current view mode)')),
224 'podcast_list_hide_boring': (bool, False,
225 ('Hide podcasts in the main window for which the episode list is empty')),
226 'podcast_list_view_all': (bool, True,
227 ('Show an additional entry in the podcast list that contains all episodes')),
229 'audio_played_dbus': (bool, False,
230 ('Set to True if the audio player notifies gPodder about played episodes')),
231 'video_played_dbus': (bool, False,
232 ('Set to True if the video player notifies gPodder about played episodes')),
234 'rotation_mode': (int, 0,
235 ('Internally used on Maemo 5 for the current rotation mode')),
237 'youtube_preferred_fmt_id': (int, 18,
238 ('The preferred video format that should be downloaded from YouTube.')),
240 # gpodder.net general settings
241 'mygpo_username': (str, '',
242 ("The user's gPodder web services username.")),
243 'mygpo_password': (str, '',
244 ("The user's gPodder web services password.")),
245 'mygpo_enabled': (bool, False,
246 ("Synchronize subscriptions with the web service.")),
247 'mygpo_server': (str, 'gpodder.net',
248 ('The hostname of the mygpo server in use.')),
250 # gpodder.net device-specific settings
251 'mygpo_device_uid': (str, util
.get_hostname(),
252 ("The UID that is assigned to this installation.")),
253 'mygpo_device_caption': (str, _('gPodder on %s') % util
.get_hostname(),
254 ("The human-readable name of this installation.")),
255 'mygpo_device_type': (str, 'desktop',
256 ("The type of the device gPodder is running on.")),
259 'paned_position': ( int, 200,
260 ("The width of the channel list.")),
262 # Preferred mime types for podcasts with multiple content types
263 'mimetype_prefs': (str, '',
264 ("A comma-separated list of mimetypes, descending order of preference")),
267 # Helper function to add window-specific properties (position and size)
268 def window_props(config_prefix
, x
=-1, y
=-1, width
=700, height
=500):
270 config_prefix
+'_x': (int, x
),
271 config_prefix
+'_y': (int, y
),
272 config_prefix
+'_width': (int, width
),
273 config_prefix
+'_height': (int, height
),
274 config_prefix
+'_maximized': (bool, False),
277 # Register window-specific properties
278 gPodderSettings
.update(window_props('main_window', width
=700, height
=500))
279 gPodderSettings
.update(window_props('episode_selector', width
=600, height
=400))
280 gPodderSettings
.update(window_props('episode_window', width
=500, height
=400))
284 Settings
= gPodderSettings
286 # Number of seconds after which settings are auto-saved
287 WRITE_TO_DISK_TIMEOUT
= 60
289 def __init__(self
, filename
='gpodder.conf'):
291 self
.__save
_thread
= None
292 self
.__filename
= filename
293 self
.__section
= 'gpodder-conf-1'
294 self
.__observers
= []
299 download_dir
= os
.environ
.get('GPODDER_DOWNLOAD_DIR', None)
300 if download_dir
is not None:
301 log('Setting download_dir from environment: %s', download_dir
, sender
=self
)
302 self
.download_dir
= download_dir
304 atexit
.register( self
.__atexit
)
306 def apply_fixes(self
):
307 # Here you can add fixes in case syntax changes. These will be
308 # applied whenever a configuration file is loaded.
309 if '{channel' in self
.custom_sync_name
:
310 log('Fixing OLD syntax {channel.*} => {podcast.*} in custom_sync_name.', sender
=self
)
311 self
.custom_sync_name
= self
.custom_sync_name
.replace('{channel.', '{podcast.')
313 def __getattr__(self
, name
):
314 if name
in self
.Settings
:
317 raise AttributeError('%s is not a setting' % name
)
319 def get_description(self
, option_name
):
320 description
= _('No description available.')
322 if self
.Settings
.get(option_name
) is not None:
323 row
= self
.Settings
[option_name
]
329 def add_observer(self
, callback
):
331 Add a callback function as observer. This callback
332 will be called when a setting changes. It should
335 observer(name, old_value, new_value)
337 The "name" is the setting name, the "old_value" is
338 the value that has been overwritten with "new_value".
340 if callback
not in self
.__observers
:
341 self
.__observers
.append(callback
)
343 log('Observer already added: %s', repr(callback
), sender
=self
)
345 def remove_observer(self
, callback
):
347 Remove an observer previously added to this object.
349 if callback
in self
.__observers
:
350 self
.__observers
.remove(callback
)
352 log('Observer not added :%s', repr(callback
), sender
=self
)
354 def schedule_save(self
):
355 if self
.__save
_thread
is None:
356 self
.__save
_thread
= threading
.Thread(target
=self
.save_thread_proc
)
357 self
.__save
_thread
.setDaemon(True)
358 self
.__save
_thread
.start()
360 def save_thread_proc(self
):
361 time
.sleep(self
.WRITE_TO_DISK_TIMEOUT
)
362 if self
.__save
_thread
is not None:
366 if self
.__save
_thread
is not None:
369 def get_backup(self
):
370 """Create a backup of the current settings
372 Returns a dictionary with the current settings which can
373 be used with "restore_backup" (see below) to restore the
374 state of the configuration object at a future point in time.
378 def restore_backup(self
, backup
):
379 """Restore a previously-created backup
381 Restore a previously-created configuration backup (created
382 with "get_backup" above) and notify any observer about the
385 for key
, value
in backup
.iteritems():
386 setattr(self
, key
, value
)
388 def save(self
, filename
=None):
390 filename
= self
.__filename
392 log('Flushing settings to disk', sender
=self
)
394 parser
= ConfigParser
.RawConfigParser()
395 parser
.add_section(self
.__section
)
397 for key
, value
in self
.Settings
.items():
398 fieldtype
, default
= value
[:2]
399 parser
.set(self
.__section
, key
, getattr(self
, key
, default
))
402 parser
.write(open(filename
, 'w'))
404 log('Cannot write settings to %s', filename
, sender
=self
)
405 raise IOError('Cannot write to file: %s' % filename
)
407 self
.__save
_thread
= None
409 def load(self
, filename
=None):
410 if filename
is not None:
411 self
.__filename
= filename
413 parser
= ConfigParser
.RawConfigParser()
415 if os
.path
.exists(self
.__filename
):
417 parser
.read(self
.__filename
)
419 log('Cannot parse config file: %s', self
.__filename
,
420 sender
=self
, traceback
=True)
422 for key
, value
in self
.Settings
.items():
423 fieldtype
, default
= value
[:2]
425 if not parser
.has_section(self
.__section
):
427 elif fieldtype
== int:
428 value
= parser
.getint(self
.__section
, key
)
429 elif fieldtype
== float:
430 value
= parser
.getfloat(self
.__section
, key
)
431 elif fieldtype
== bool:
432 value
= parser
.getboolean(self
.__section
, key
)
434 value
= fieldtype(parser
.get(self
.__section
, key
))
436 log('Invalid value in %s for %s: %s', self
.__filename
,
437 key
, value
, sender
=self
, traceback
=True)
442 def toggle_flag(self
, name
):
443 if name
in self
.Settings
:
444 (fieldtype
, default
) = self
.Settings
[name
][:2]
445 if fieldtype
== bool:
446 setattr(self
, name
, not getattr(self
, name
))
448 log('Cannot toggle value: %s (not boolean)', name
, sender
=self
)
450 log('Invalid setting name: %s', name
, sender
=self
)
452 def update_field(self
, name
, new_value
):
453 if name
in self
.Settings
:
454 (fieldtype
, default
) = self
.Settings
[name
][:2]
456 new_value
= fieldtype(new_value
)
458 log('Cannot convert "%s" to %s. Ignoring.', str(new_value
), fieldtype
.__name
__, sender
=self
)
460 setattr(self
, name
, new_value
)
463 log('Invalid setting name: %s', name
, sender
=self
)
466 def __setattr__(self
, name
, value
):
467 if name
in self
.Settings
:
468 fieldtype
, default
= self
.Settings
[name
][:2]
470 if self
[name
] != fieldtype(value
):
471 old_value
= self
[name
]
472 log('Update %s: %s => %s', name
, old_value
, value
, sender
=self
)
473 self
[name
] = fieldtype(value
)
474 for observer
in self
.__observers
:
476 # Notify observer about config change
477 observer(name
, old_value
, self
[name
])
479 log('Error while calling observer: %s',
480 repr(observer
), sender
=self
,
484 raise ValueError('%s has to be of type %s' % (name
, fieldtype
.__name
__))
486 object.__setattr
__(self
, name
, value
)