1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2018 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
33 from gpodder
import jsonconfig
, util
38 # External applications used for playback
44 # gpodder.net settings
47 'server': 'gpodder.net',
51 'uid': util
.get_hostname(),
53 'caption': _('gPodder on %s') % util
.get_hostname(),
57 # Various limits (downloading, updating, etc..)
61 'kbps': 500.0, # maximum kB/s per download
68 'episodes': 200, # max episodes per feed
71 # Behavior of downloads
73 'chronological_order': True, # download older episodes first
76 # Automatic feed updates, download removal and retry on download timeout
80 'frequency': 20, # minutes
90 'retries': 3, # number of retries when downloads time out
93 'check_connection': True,
95 # Software updates from gpodder.org
97 'check_on_startup': True, # check for updates on start
98 'last_check': 0, # unix timestamp of last update check
99 'interval': 5, # interval (in days) to check for updates
103 # Settings for the Command-Line Interface
108 # Settings for the Gtk UI
114 'x': -1, 'y': -1, 'maximized': False,
116 'paned_position': 200,
117 'episode_list_size': 200,
119 'episode_column_sort_id': 0,
120 'episode_column_sort_order': False,
121 'episode_column_order': [],
126 'x': -1, 'y': -1, 'maximized': False,
131 'x': -1, 'y': -1, 'maximized': False,
136 'x': -1, 'y': -1, 'maximized': False,
138 'episode_selector': {
141 'x': -1, 'y': -1, 'maximized': False,
146 'x': -1, 'y': -1, 'maximized': False,
148 'export_to_local_folder': {
151 'x': -1, 'y': -1, 'maximized': False,
156 'new_episodes': 'show', # ignore, show, queue, download
157 'only_added_are_new': False, # Only just added episodes are considered new after an update
158 'live_search_delay': 200,
159 'search_always_visible': False,
160 'find_as_you_type': True,
165 'all_episodes': True,
171 'always_show_new': True,
172 'trim_title_prefix': True,
173 'descriptions': True,
174 'show_released_time': False,
175 'right_align_released_column': False,
176 'ctrl_click_to_sort': False,
177 'columns': int('110', 2), # bitfield of visible columns
181 'remove_finished': True,
184 'html_shownotes': True, # enable webkit renderer
185 'color_scheme': None, # system, light or dark. Initialized in app.py
189 # Synchronization with portable devices (MP3 players, etc..)
191 'device_type': 'none', # Possible values: 'none', 'filesystem', 'ipod'
192 'device_folder': '/media',
194 'one_folder_per_podcast': True,
195 'skip_played_episodes': True,
196 'delete_played_episodes': False,
197 'delete_deleted_episodes': False,
199 'max_filename_length': 120,
201 'compare_episode_filesize': True,
203 'custom_sync_name': '{episode.sortdate}_{episode.title}',
204 'custom_sync_name_enabled': False,
207 'mark_episodes_played': False,
208 'delete_episodes': False,
213 'two_way_sync': False,
214 'use_absolute_path': True,
215 'folder': 'Playlists',
222 'preferred_fmt_id': 18, # default fmt_id (see fallbacks in youtube.py)
223 'preferred_fmt_ids': [], # for advanced uses (custom fallback sequence)
224 'preferred_hls_fmt_id': 93, # default fmt_id (see fallbacks in youtube.py)
225 'preferred_hls_fmt_ids': [], # for advanced uses (custom fallback sequence)
229 'fileformat': '720p', # preferred file format (see vimeo.py)
234 'proxy_type': 'socks5h', # Possible values: socks5h (routes dns through the proxy), socks5, http
235 'proxy_hostname': '127.0.0.1',
236 'proxy_port': '8123',
237 'proxy_use_username_password': False,
238 'proxy_username': '',
239 'proxy_password': '',
246 'custom_file_format': '{episode.title}',
247 'custom_file_format_enabled': False,
251 logger
= logging
.getLogger(__name__
)
253 # Global variable for network proxies. Updated when the network proxy in the config changes
257 def get_network_proxy_observer(config
):
258 """Return an observer function inside a closure containing given config instance."""
260 def get_proxies_from_config(config
):
262 if config
.network
.use_proxy
:
263 protocol
= config
.network
.proxy_type
265 if config
.network
.proxy_use_username_password
:
266 user_pass
= f
"{config.network.proxy_username}:{config.network.proxy_password}@"
267 proxy_url
= f
"{protocol}://{user_pass}{config.network.proxy_hostname}:{config.network.proxy_port}"
268 proxies
= {"http": proxy_url
, "https": proxy_url
}
269 logger
.debug(f
"config observer returning proxies: {proxies}")
272 def network_proxy_observer(name
, old_value
, new_value
):
274 if name
.startswith("network."):
275 _proxies
= get_proxies_from_config(config
)
277 return network_proxy_observer
280 def config_value_to_string(config_value
):
281 config_type
= type(config_value
)
283 if config_type
== list:
284 return ','.join(map(config_value_to_string
, config_value
))
285 elif config_type
in (str, str):
288 return str(config_value
)
291 def string_to_config_value(new_value
, old_value
):
292 config_type
= type(old_value
)
294 if config_type
== list:
295 return [_f
for _f
in [x
.strip() for x
in new_value
.split(',')] if _f
]
296 elif config_type
== bool:
297 return (new_value
.strip().lower() in ('1', 'true'))
299 return config_type(new_value
)
302 class Config(object):
303 # Number of seconds after which settings are auto-saved
304 WRITE_TO_DISK_TIMEOUT
= 60
306 def __init__(self
, filename
='gpodder.json'):
307 self
.__json
_config
= jsonconfig
.JsonConfig(default
=defaults
,
308 on_key_changed
=self
._on
_key
_changed
)
309 self
.__save
_thread
= None
310 self
.__filename
= filename
311 self
.__observers
= []
314 self
.migrate_defaults()
316 # If there is no configuration file, we create one here (bug 1511)
317 if not os
.path
.exists(self
.__filename
):
320 atexit
.register(self
.__atexit
)
322 def register_defaults(self
, defaults
):
324 Register default configuration options (e.g. for extensions)
326 This function takes a dictionary that will be merged into the
327 current configuration if the keys don't yet exist. This can
328 be used to add a default configuration for extension modules.
330 self
.__json
_config
._merge
_keys
(defaults
)
332 def add_observer(self
, callback
):
334 Add a callback function as observer. This callback
335 will be called when a setting changes. It should
338 observer(name, old_value, new_value)
340 The "name" is the setting name, the "old_value" is
341 the value that has been overwritten with "new_value".
343 if callback
not in self
.__observers
:
344 self
.__observers
.append(callback
)
346 logger
.warning('Observer already added: %s', repr(callback
))
348 def remove_observer(self
, callback
):
350 Remove an observer previously added to this object.
352 if callback
in self
.__observers
:
353 self
.__observers
.remove(callback
)
355 logger
.warning('Observer not added: %s', repr(callback
))
358 return self
.__json
_config
._keys
_iter
()
360 def schedule_save(self
):
361 if self
.__save
_thread
is None:
362 self
.__save
_thread
= util
.run_in_background(self
.save_thread_proc
, True)
364 def save_thread_proc(self
):
365 time
.sleep(self
.WRITE_TO_DISK_TIMEOUT
)
366 if self
.__save
_thread
is not None:
370 if self
.__save
_thread
is not None:
373 def save(self
, filename
=None):
375 filename
= self
.__filename
377 logger
.info('Flushing settings to disk')
380 # revoke unix group/world permissions (this has no effect under windows)
381 umask
= os
.umask(0o077)
382 with
open(filename
+ '.tmp', 'wt') as fp
:
383 fp
.write(repr(self
.__json
_config
))
384 util
.atomic_rename(filename
+ '.tmp', filename
)
386 logger
.error('Cannot write settings to %s', filename
)
387 util
.delete_file(filename
+ '.tmp')
392 self
.__save
_thread
= None
394 def load(self
, filename
=None):
395 if filename
is not None:
396 self
.__filename
= filename
398 if os
.path
.exists(self
.__filename
):
400 with
open(self
.__filename
, 'rt') as f
:
402 new_keys_added
= self
.__json
_config
._restore
(data
)
404 logger
.warning('Cannot parse config file: %s',
405 self
.__filename
, exc_info
=True)
406 new_keys_added
= False
409 logger
.info('New default keys added - saving config.')
412 def toggle_flag(self
, name
):
413 setattr(self
, name
, not getattr(self
, name
))
415 def update_field(self
, name
, new_value
):
416 """Update a config field, converting strings to the right types"""
417 old_value
= self
._lookup
(name
)
418 new_value
= string_to_config_value(new_value
, old_value
)
419 setattr(self
, name
, new_value
)
422 def _on_key_changed(self
, name
, old_value
, value
):
423 if 'ui.gtk.state' not in name
:
424 # Only log non-UI state changes
425 logger
.debug('%s: %s -> %s', name
, old_value
, value
)
426 for observer
in self
.__observers
:
428 observer(name
, old_value
, value
)
429 except Exception as exception
:
430 logger
.error('Error while calling observer %r: %s',
431 observer
, exception
, exc_info
=True)
435 def __getattr__(self
, name
):
436 return getattr(self
.__json
_config
, name
)
438 def __setattr__(self
, name
, value
):
439 if name
.startswith('_'):
440 object.__setattr
__(self
, name
, value
)
443 setattr(self
.__json
_config
, name
, value
)
445 def migrate_defaults(self
):
446 """ change default values in config """
447 if self
.device_sync
.max_filename_length
== 999:
448 logger
.debug("setting config.device_sync.max_filename_length=120"
449 " (999 is bad for NTFS and ext{2-4})")
450 self
.device_sync
.max_filename_length
= 120
452 def clamp_range(self
, name
, minval
, maxval
):
453 value
= getattr(self
, name
)
455 setattr(self
, name
, minval
)
458 setattr(self
, name
, maxval
)