gpodder/src/gpodder/config.py

418 lines
13 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
2018-01-28 19:39:53 +01:00
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
# config.py -- gPodder Configuration Manager
# Thomas Perl <thp@perli.net> 2007-11-02
#
import atexit
import logging
import os
import time
import gpodder
from gpodder import jsonconfig, util
_ = gpodder.gettext
defaults = {
2010-12-20 01:21:41 +01:00
# External applications used for playback
'player': {
'audio': 'default',
'video': 'default',
},
2010-12-20 01:21:41 +01:00
# gpodder.net settings
'mygpo': {
'enabled': False,
'server': 'gpodder.net',
'username': '',
'password': '',
'device': {
'uid': util.get_hostname(),
'type': 'desktop',
'caption': _('gPodder on %s') % util.get_hostname(),
},
},
# Various limits (downloading, updating, etc..)
'limit': {
'bandwidth': {
'enabled': False,
'kbps': 500.0, # maximum kB/s per download
},
'downloads': {
'enabled': True,
'concurrent': 1,
'concurrent_max': 16,
},
'episodes': 200, # max episodes per feed
},
# Behavior of downloads
'downloads': {
'chronological_order': True, # download older episodes first
},
# Automatic feed updates, download removal and retry on download timeout
'auto': {
'update': {
'enabled': False,
'frequency': 20, # minutes
},
'cleanup': {
'days': 7,
'played': False,
'unplayed': False,
'unfinished': True,
},
'retries': 3, # number of retries when downloads time out
},
'check_connection': True,
# Software updates from gpodder.org
'software_update': {
'check_on_startup': True, # check for updates on start
'last_check': 0, # unix timestamp of last update check
'interval': 5, # interval (in days) to check for updates
},
'ui': {
# Settings for the Command-Line Interface
'cli': {
'colors': True,
},
# Settings for the Gtk UI
'gtk': {
'state': {
'main_window': {
'width': 700,
'height': 500,
'x': -1, 'y': -1, 'maximized': False,
'paned_position': 200,
'episode_list_size': 200,
'episode_column_sort_id': 0,
'episode_column_sort_order': False,
'episode_column_order': [],
},
'preferences': {
'width': -1,
'height': -1,
'x': -1, 'y': -1, 'maximized': False,
},
'config_editor': {
'width': -1,
'height': -1,
'x': -1, 'y': -1, 'maximized': False,
},
2021-07-18 05:50:20 +02:00
'channel_editor': {
'width': -1,
'height': -1,
'x': -1, 'y': -1, 'maximized': False,
},
'episode_selector': {
'width': 600,
'height': 400,
'x': -1, 'y': -1, 'maximized': False,
},
'episode_window': {
'width': 500,
'height': 400,
'x': -1, 'y': -1, 'maximized': False,
},
'export_to_local_folder': {
'width': 500,
'height': 400,
'x': -1, 'y': -1, 'maximized': False,
}
},
'toolbar': False,
'new_episodes': 'show', # ignore, show, queue, download
'only_added_are_new': False, # Only just added episodes are considered new after an update
'live_search_delay': 200,
2020-04-05 17:36:14 +02:00
'search_always_visible': False,
'find_as_you_type': True,
'podcast_list': {
'view_mode': 1,
'hide_empty': False,
'all_episodes': True,
'sections': True,
},
'episode_list': {
'view_mode': 1,
'always_show_new': True,
'trim_title_prefix': True,
'descriptions': True,
'show_released_time': False,
'right_align_released_column': False,
'ctrl_click_to_sort': False,
'columns': int('110', 2), # bitfield of visible columns
},
'download_list': {
'remove_finished': True,
},
2017-01-29 22:38:02 +01:00
2017-02-05 20:49:38 +01:00
'html_shownotes': True, # enable webkit renderer
},
},
# Synchronization with portable devices (MP3 players, etc..)
'device_sync': {
'device_type': 'none', # Possible values: 'none', 'filesystem', 'ipod'
2018-02-01 07:59:22 +01:00
'device_folder': '/media',
'one_folder_per_podcast': True,
'skip_played_episodes': True,
'delete_played_episodes': False,
'delete_deleted_episodes': False,
'max_filename_length': 120,
'custom_sync_name': '{episode.sortdate}_{episode.title}',
'custom_sync_name_enabled': False,
'after_sync': {
'mark_episodes_played': False,
'delete_episodes': False,
'sync_disks': False,
},
2013-02-01 04:36:21 +01:00
'playlists': {
'create': True,
'two_way_sync': False,
'use_absolute_path': True,
'folder': 'Playlists',
'extension': 'm3u',
2013-02-01 04:36:21 +01:00
}
},
'youtube': {
'preferred_fmt_id': 18, # default fmt_id (see fallbacks in youtube.py)
'preferred_fmt_ids': [], # for advanced uses (custom fallback sequence)
2020-09-12 10:34:39 +02:00
'preferred_hls_fmt_id': 93, # default fmt_id (see fallbacks in youtube.py)
'preferred_hls_fmt_ids': [], # for advanced uses (custom fallback sequence)
},
'vimeo': {
'fileformat': '720p', # preferred file format (see vimeo.py)
},
'extensions': {
'enabled': [],
},
}
logger = logging.getLogger(__name__)
def config_value_to_string(config_value):
config_type = type(config_value)
if config_type == list:
return ','.join(map(config_value_to_string, config_value))
2016-11-21 23:13:46 +01:00
elif config_type in (str, str):
return config_value
else:
return str(config_value)
def string_to_config_value(new_value, old_value):
config_type = type(old_value)
if config_type == list:
2016-11-21 23:13:46 +01:00
return [_f for _f in [x.strip() for x in new_value.split(',')] if _f]
elif config_type == bool:
return (new_value.strip().lower() in ('1', 'true'))
else:
2012-02-27 12:22:40 +01:00
return config_type(new_value)
class Config(object):
Sun, 06 Apr 2008 02:05:34 +0200 <thp@perli.net> Initial upstream support for the Maemo platform (Nokia Internet Tablets) * bin/gpodder: Add "--maemo/-m" option to enable running as a Maemo application (this is only useful on Nokia Internet Tablets or in the Maemo SDK environment); determine interface type and set the correct variables on startup (gpodder.interface) * data/gpodder.glade: Increase the default size of some widgets to better fit the screens on Maemo (it won't do any harm on the "big" Desktop screen * data/icons/26/gpodder.png: Added * data/icons/40/gpodder.png: Added * data/maemo/gpodder.desktop: Added * Makefile: Help2man variable; new "make mtest" target that runs gPodder in Maemo scratchbox (probably useless for all other things); update the command descriptions; don't run the "generators" target from the "install" target; don't run "gen_graphics" from the "generators" target, but make it depend on the 24-pixel logo, which itself depends on the 22-pixel logo; this way, all should work out well when trying to install on systems without ImageMagick installed; remove *.pyo files on "make clean" * setup.py: Support for build targets; use "TARGET=maemo" to enable Maemo-specific installation options and files * src/gpodder/config.py: Increase the WRITE_TO_DISK_TIMEOUT to 60 seconds, so we don't unnecessarily stress memory cards (on ITs); modify default path variables on Maemo (/media/mmc2) * src/gpodder/gui.py: Maemo-specific changes; clean-up the main window a bit and make message and confirmation dialogs Hildon-compatible * src/gpodder/__init__.py: Add enums for interface types: CLI, GUI and MAEMO; remove the "interface_is_gui" variable and replace with "interface", which is now used to determine where we are running * src/gpodder/libgpodder.py: Use /media/mmc2/gpodder/ as configuration folder on Maemo; use Nokia's Media player to playback files on Maemo * src/gpodder/libpodcasts.py: Icon name changes (Maemo-specific) * src/gpodder/trayicon.py: Maemo support; swap popup menu on Maemo; Add support for hildon banners instead of pynotify on Maemo * src/gpodder/util.py: Icon name changes (Maemo-specific); use new gpodder.interface variable in idle_add git-svn-id: svn://svn.berlios.de/gpodder/trunk@654 b0d088ad-0a06-0410-aad2-9ed5178a7e87
2008-04-06 02:19:03 +02:00
# Number of seconds after which settings are auto-saved
WRITE_TO_DISK_TIMEOUT = 60
def __init__(self, filename='gpodder.json'):
self.__json_config = jsonconfig.JsonConfig(default=defaults,
on_key_changed=self._on_key_changed)
self.__save_thread = None
self.__filename = filename
Fri, 02 May 2008 17:28:22 +0200 <thp@perli.net> Change "Channel" to "Podcast"; new main menu; URL entry updated; +niceties * data/gpodder.glade: Change "Channel" to "Podcast"; re-structure main menu in gPodder window * src/gpodder/config.py: Add "show_podcast_url_entry" configuration option that controls whether the podcast url entry box is shown in the main window or not; add observer functionality to the configuration manager, so UI elements can "watch" the configuration manager for changes of UI-related configuration options * src/gpodder/console.py: Change "Channel" to "Podcast" * src/gpodder/gui.py: Hildon-specific file open/save dialogs; default "Enter podcast URL..." test for the URL entry box; size-dependent showing and hiding of podcast icon and downloaded count pixmap also for the Desktop version; change "Channel" to "Podcast"; offer first-time users to see a list of example podcasts to subscribe to; dynamic main menu; code supporting the main menu changes; add code for sending the subscription list via e-mail; import from OPML file in addition to import from OPML URL; remove unneeded callbacks (wishlist, select all, ...); optionally set title and hide url entry in the gPodderOpmlLister (OPML import GUI); Add Frank Harper to list of contributors (initial reporter of bug #82) * src/gpodder/libgpodder.py: Add "send_subscriptions" function that sends the user's channels.opml file via E-Mail (using xdg-email); rename "Channel" to "Podcast" (Closes: http://bugs.gpodder.org/show_bug.cgi?id=82) (Closes: http://bugs.gpodder.org/show_bug.cgi?id=103) git-svn-id: svn://svn.berlios.de/gpodder/trunk@700 b0d088ad-0a06-0410-aad2-9ed5178a7e87
2008-05-02 17:36:43 +02:00
self.__observers = []
self.load()
self.migrate_defaults()
# If there is no configuration file, we create one here (bug 1511)
if not os.path.exists(self.__filename):
self.save()
atexit.register(self.__atexit)
def register_defaults(self, defaults):
"""
Register default configuration options (e.g. for extensions)
This function takes a dictionary that will be merged into the
current configuration if the keys don't yet exist. This can
be used to add a default configuration for extension modules.
"""
self.__json_config._merge_keys(defaults)
Fri, 02 May 2008 17:28:22 +0200 <thp@perli.net> Change "Channel" to "Podcast"; new main menu; URL entry updated; +niceties * data/gpodder.glade: Change "Channel" to "Podcast"; re-structure main menu in gPodder window * src/gpodder/config.py: Add "show_podcast_url_entry" configuration option that controls whether the podcast url entry box is shown in the main window or not; add observer functionality to the configuration manager, so UI elements can "watch" the configuration manager for changes of UI-related configuration options * src/gpodder/console.py: Change "Channel" to "Podcast" * src/gpodder/gui.py: Hildon-specific file open/save dialogs; default "Enter podcast URL..." test for the URL entry box; size-dependent showing and hiding of podcast icon and downloaded count pixmap also for the Desktop version; change "Channel" to "Podcast"; offer first-time users to see a list of example podcasts to subscribe to; dynamic main menu; code supporting the main menu changes; add code for sending the subscription list via e-mail; import from OPML file in addition to import from OPML URL; remove unneeded callbacks (wishlist, select all, ...); optionally set title and hide url entry in the gPodderOpmlLister (OPML import GUI); Add Frank Harper to list of contributors (initial reporter of bug #82) * src/gpodder/libgpodder.py: Add "send_subscriptions" function that sends the user's channels.opml file via E-Mail (using xdg-email); rename "Channel" to "Podcast" (Closes: http://bugs.gpodder.org/show_bug.cgi?id=82) (Closes: http://bugs.gpodder.org/show_bug.cgi?id=103) git-svn-id: svn://svn.berlios.de/gpodder/trunk@700 b0d088ad-0a06-0410-aad2-9ed5178a7e87
2008-05-02 17:36:43 +02:00
def add_observer(self, callback):
"""
Add a callback function as observer. This callback
will be called when a setting changes. It should
Fri, 02 May 2008 17:28:22 +0200 <thp@perli.net> Change "Channel" to "Podcast"; new main menu; URL entry updated; +niceties * data/gpodder.glade: Change "Channel" to "Podcast"; re-structure main menu in gPodder window * src/gpodder/config.py: Add "show_podcast_url_entry" configuration option that controls whether the podcast url entry box is shown in the main window or not; add observer functionality to the configuration manager, so UI elements can "watch" the configuration manager for changes of UI-related configuration options * src/gpodder/console.py: Change "Channel" to "Podcast" * src/gpodder/gui.py: Hildon-specific file open/save dialogs; default "Enter podcast URL..." test for the URL entry box; size-dependent showing and hiding of podcast icon and downloaded count pixmap also for the Desktop version; change "Channel" to "Podcast"; offer first-time users to see a list of example podcasts to subscribe to; dynamic main menu; code supporting the main menu changes; add code for sending the subscription list via e-mail; import from OPML file in addition to import from OPML URL; remove unneeded callbacks (wishlist, select all, ...); optionally set title and hide url entry in the gPodderOpmlLister (OPML import GUI); Add Frank Harper to list of contributors (initial reporter of bug #82) * src/gpodder/libgpodder.py: Add "send_subscriptions" function that sends the user's channels.opml file via E-Mail (using xdg-email); rename "Channel" to "Podcast" (Closes: http://bugs.gpodder.org/show_bug.cgi?id=82) (Closes: http://bugs.gpodder.org/show_bug.cgi?id=103) git-svn-id: svn://svn.berlios.de/gpodder/trunk@700 b0d088ad-0a06-0410-aad2-9ed5178a7e87
2008-05-02 17:36:43 +02:00
have this signature:
observer(name, old_value, new_value)
The "name" is the setting name, the "old_value" is
the value that has been overwritten with "new_value".
"""
if callback not in self.__observers:
self.__observers.append(callback)
else:
logger.warning('Observer already added: %s', repr(callback))
Fri, 02 May 2008 17:28:22 +0200 <thp@perli.net> Change "Channel" to "Podcast"; new main menu; URL entry updated; +niceties * data/gpodder.glade: Change "Channel" to "Podcast"; re-structure main menu in gPodder window * src/gpodder/config.py: Add "show_podcast_url_entry" configuration option that controls whether the podcast url entry box is shown in the main window or not; add observer functionality to the configuration manager, so UI elements can "watch" the configuration manager for changes of UI-related configuration options * src/gpodder/console.py: Change "Channel" to "Podcast" * src/gpodder/gui.py: Hildon-specific file open/save dialogs; default "Enter podcast URL..." test for the URL entry box; size-dependent showing and hiding of podcast icon and downloaded count pixmap also for the Desktop version; change "Channel" to "Podcast"; offer first-time users to see a list of example podcasts to subscribe to; dynamic main menu; code supporting the main menu changes; add code for sending the subscription list via e-mail; import from OPML file in addition to import from OPML URL; remove unneeded callbacks (wishlist, select all, ...); optionally set title and hide url entry in the gPodderOpmlLister (OPML import GUI); Add Frank Harper to list of contributors (initial reporter of bug #82) * src/gpodder/libgpodder.py: Add "send_subscriptions" function that sends the user's channels.opml file via E-Mail (using xdg-email); rename "Channel" to "Podcast" (Closes: http://bugs.gpodder.org/show_bug.cgi?id=82) (Closes: http://bugs.gpodder.org/show_bug.cgi?id=103) git-svn-id: svn://svn.berlios.de/gpodder/trunk@700 b0d088ad-0a06-0410-aad2-9ed5178a7e87
2008-05-02 17:36:43 +02:00
def remove_observer(self, callback):
"""
Remove an observer previously added to this object.
"""
if callback in self.__observers:
self.__observers.remove(callback)
else:
logger.warning('Observer not added: %s', repr(callback))
def all_keys(self):
return self.__json_config._keys_iter()
def schedule_save(self):
if self.__save_thread is None:
self.__save_thread = util.run_in_background(self.save_thread_proc, True)
def save_thread_proc(self):
time.sleep(self.WRITE_TO_DISK_TIMEOUT)
if self.__save_thread is not None:
self.save()
def __atexit(self):
if self.__save_thread is not None:
self.save()
def save(self, filename=None):
if filename is None:
filename = self.__filename
logger.info('Flushing settings to disk')
try:
# revoke unix group/world permissions (this has no effect under windows)
umask = os.umask(0o077)
with open(filename + '.tmp', 'wt') as fp:
fp.write(repr(self.__json_config))
2018-04-15 18:59:20 +02:00
util.atomic_rename(filename + '.tmp', filename)
except:
logger.error('Cannot write settings to %s', filename)
2018-04-15 18:59:20 +02:00
util.delete_file(filename + '.tmp')
raise
finally:
os.umask(umask)
self.__save_thread = None
def load(self, filename=None):
if filename is not None:
self.__filename = filename
if os.path.exists(self.__filename):
try:
with open(self.__filename, 'rt') as f:
data = f.read()
new_keys_added = self.__json_config._restore(data)
except:
logger.warning('Cannot parse config file: %s',
self.__filename, exc_info=True)
new_keys_added = False
if new_keys_added:
logger.info('New default keys added - saving config.')
self.save()
def toggle_flag(self, name):
setattr(self, name, not getattr(self, name))
def update_field(self, name, new_value):
"""Update a config field, converting strings to the right types"""
old_value = self._lookup(name)
new_value = string_to_config_value(new_value, old_value)
setattr(self, name, new_value)
return True
def _on_key_changed(self, name, old_value, value):
if 'ui.gtk.state' not in name:
# Only log non-UI state changes
logger.debug('%s: %s -> %s', name, old_value, value)
for observer in self.__observers:
try:
observer(name, old_value, value)
2016-11-21 23:13:46 +01:00
except Exception as exception:
logger.error('Error while calling observer %r: %s',
observer, exception, exc_info=True)
self.schedule_save()
def __getattr__(self, name):
return getattr(self.__json_config, name)
def __setattr__(self, name, value):
if name.startswith('_'):
object.__setattr__(self, name, value)
return
setattr(self.__json_config, name, value)
def migrate_defaults(self):
""" change default values in config """
if self.device_sync.max_filename_length == 999:
logger.debug("setting config.device_sync.max_filename_length=120"
" (999 is bad for NTFS and ext{2-4})")
self.device_sync.max_filename_length = 120
def clamp_range(self, name, min, max):
value = getattr(self, name)
if value < min:
setattr(self, name, min)
return True
if value > max:
setattr(self, name, max)
return True
return False