First try on the new JSON-based config module

This commit is contained in:
Thomas Perl 2012-01-03 23:59:19 +01:00
parent 025349c781
commit 990cebe9b9
16 changed files with 398 additions and 373 deletions

View File

@ -237,14 +237,6 @@
<property name="label" translatable="yes">Use sections for podcast list</property>
</object>
</child>
<child>
<object class="GtkToggleAction" id="item_sidebar">
<property name="active">True</property>
<property name="label" translatable="yes">Podcast list</property>
<signal handler="on_view_sidebar_toggled" name="activate"/>
</object>
<accelerator key="F9"/>
</child>
<child>
<object class="GtkToggleAction" id="itemShowToolbar">
<property name="active">True</property>
@ -365,7 +357,6 @@
<menuitem action="item_episode_details"/>
</menu>
<menu action="menuView">
<menuitem action="item_sidebar"/>
<menuitem action="itemShowAllEpisodes"/>
<menuitem action="item_podcast_sections"/>
<separator/>
@ -726,7 +717,7 @@
</child>
</object>
<packing>
<property name="shrink">True</property>
<property name="shrink">False</property>
<property name="resize">False</property>
</packing>
</child>

View File

@ -144,7 +144,7 @@ def set_home(new_home):
global home, config_file, database_file, downloads
home = os.path.abspath(new_home)
config_file = os.path.join(home, 'Settings')
config_file = os.path.join(home, 'Settings.json')
database_file = os.path.join(home, 'Database')
downloads = os.path.join(home, 'Downloads')

View File

@ -108,8 +108,7 @@ class Podcast(object):
Downloads the podcast feed (using the feed cache), and
adds new episodes and updated information to the database.
"""
self._podcast.update(self._manager._config.max_episodes_per_feed, \
self._manager._config.mimetype_prefs)
self._podcast.update(self._manager._config.max_episodes_per_feed)
def feed_update_status_msg(self):
"""Show the feed update status
@ -228,8 +227,7 @@ class PodcastClient(object):
return None
podcast = self._model.load_podcast(url, create=True, \
max_episodes=self._config.max_episodes_per_feed, \
mimetype_prefs=self._config.mimetype_prefs)
max_episodes=self._config.max_episodes_per_feed)
if podcast is not None:
if title is not None:
podcast.rename(title)

View File

@ -29,105 +29,253 @@ from gpodder import util
import atexit
import os
import shutil
import time
import threading
import ConfigParser
import logging
import json
import copy
_ = gpodder.gettext
gPodderSettings = {
defaults = {
# External applications used for playback
'player': 'default',
'videoplayer': 'default',
'player': {
'audio': 'default',
'video': 'default',
},
# gpodder.net settings
'mygpo_enabled': False,
'mygpo_server': 'gpodder.net',
'mygpo_username': '',
'mygpo_password': '',
'mygpo_device_uid': util.get_hostname(),
'mygpo_device_type': 'desktop',
'mygpo_device_caption': _('gPodder on %s') % util.get_hostname(),
'mygpo': {
'enabled': False,
'server': 'gpodder.net',
'username': '',
'password': '',
'device': {
'uid': util.get_hostname(),
'type': 'desktop',
'caption': _('gPodder on %s') % util.get_hostname(),
},
},
# Download options
'limit_rate': False,
'limit_rate_value': 500.0,
'max_downloads_enabled': True,
'max_downloads': 1,
# Various limits (downloading, updating, etc..)
'limit': {
'bandwidth': {
'enabled': False,
'kbps': 500.0, # maximum kB/s per download
},
'downloads': {
'enabled': True,
'concurrent': 1,
},
'episodes': 200, # max episodes per feed
},
# Automatic removal of downloads
'episode_old_age': 7,
'auto_remove_played_episodes': False,
'auto_remove_unfinished_episodes': True,
'auto_remove_unplayed_episodes': False,
# Automatic feed updates and download removal
'auto': {
'update': {
'enabled': False,
'frequency': 20, # minutes
},
# Periodic check for new episodes
'auto_update_feeds': False,
'auto_update_frequency': 20,
'cleanup': {
'days': 7,
'played': False,
'unplayed': False,
'unfinished': True,
},
},
# Limits
'max_episodes_per_feed': 200,
'ui': {
# Settings for the Gtk UI
'gtk': {
'state': {
'main_window': {
'width': 700,
'height': 500,
'x': -1, 'y': -1, 'maximized': False,
# View settings
'show_toolbar': True,
'episode_list_descriptions': True,
'podcast_list_view_all': True,
'podcast_list_sections': True,
'enable_html_shownotes': True,
'enable_notifications': True,
'paned_position': 200,
},
'episode_selector': {
'width': 600,
'height': 400,
'x': -1, 'y': -1, 'maximized': False,
},
'episode_window': {
'width': 500,
'height': 400,
'x': -1, 'y': -1, 'maximized': False,
},
},
# Display list filter configuration
'episode_list_view_mode': 1,
'episode_list_columns': int('101', 2), # bitfield of visible columns
'podcast_list_view_mode': 1,
'podcast_list_hide_boring': False,
'toolbar': True,
'notifications': True,
'html_shownotes': True,
'new_episodes': 'show', # ignore, show, queue, download
# URLs to OPML files
'example_opml': 'http://gpodder.org/directory.opml',
'toplist_opml': 'http://gpodder.org/toplist.opml',
'podcast_list': {
'all_episodes': True,
'sections': True,
'view_mode': 1,
'hide_empty': False,
},
# YouTube
'youtube_preferred_fmt_id': 18,
'episode_list': {
'descriptions': True,
'view_mode': 1,
'columns': int('101', 2), # bitfield of visible columns
},
# Misc
'_paned_position': 200,
'rotation_mode': 0,
'mimetype_prefs': '',
'auto_cleanup_downloads': True,
'do_not_show_new_episodes_dialog': False,
'auto_download': 'never',
'download_list': {
'remove_finished': True,
},
},
},
'youtube': {
'preferred_fmt_id': 18,
},
}
# Helper function to add window-specific properties (position and size)
def window_props(config_prefix, x=-1, y=-1, width=700, height=500):
return {
config_prefix+'_x': x,
config_prefix+'_y': y,
config_prefix+'_width': width,
config_prefix+'_height': height,
config_prefix+'_maximized': False,
}
# Register window-specific properties
gPodderSettings.update(window_props('_main_window', width=700, height=500))
gPodderSettings.update(window_props('_episode_selector', width=600, height=400))
gPodderSettings.update(window_props('_episode_window', width=500, height=400))
# The sooner this goes away, the better
gPodderSettings_LegacySupport = {
'player': 'player.audio',
'videoplayer': 'player.video',
'limit_rate': 'limit.bandwidth.enabled',
'limit_rate_value': 'limit.bandwidth.kbps',
'max_downloads_enabled': 'limit.downloads.enabled',
'max_downloads': 'limit.downloads.concurrent',
'episode_old_age': 'auto.cleanup.days',
'auto_remove_played_episodes': 'auto.cleanup.played',
'auto_remove_unfinished_episodes': 'auto.cleanup.unfinished',
'auto_remove_unplayed_episodes': 'auto.cleanup.unplayed',
'max_episodes_per_feed': 'limit.episodes',
'show_toolbar': 'ui.gtk.toolbar',
'paned_position': 'ui.gtk.state.main_window.paned_position',
'enable_notifications': 'ui.gtk.notifications',
'episode_list_descriptions': 'ui.gtk.episode_list.descriptions',
'podcast_list_view_all': 'ui.gtk.podcast_list.all_episodes',
'podcast_list_sections': 'ui.gtk.podcast_list.sections',
'enable_html_shownotes': 'ui.gtk.html_shownotes',
'episode_list_view_mode': 'ui.gtk.episode_list.view_mode',
'podcast_list_view_mode': 'ui.gtk.podcast_list.view_mode',
'podcast_list_hide_boring': 'ui.gtk.podcast_list.hide_empty',
'youtube_preferred_fmt_id': 'youtube.preferred_fmt_id',
'episode_list_columns': 'ui.gtk.episode_list.columns',
'auto_cleanup_downloads': 'ui.gtk.download_list.remove_finished',
'auto_update_feeds': 'auto.update.enabled',
'auto_update_frequency': 'auto.update.frequency',
'auto_download': 'ui.gtk.new_episodes',
}
logger = logging.getLogger(__name__)
class Config(dict):
Settings = gPodderSettings
class JsonConfigSubtree(object):
def __init__(self, parent, name):
self._parent = parent
self._name = name
def __repr__(self):
return '<Subtree %r of %r>' % (self._name, self._parent)
def _attr(self, name):
return '.'.join((self._name, name))
def __getitem__(self, name):
return self._parent._lookup(self._name).__getitem__(name)
def __setitem__(self, name, value):
self._parent._lookup(self._name).__setitem__(name, value)
def __getattr__(self, name):
if name == 'keys':
# Kludge for using dict() on a JsonConfigSubtree
return getattr(self._parent._lookup(self._name), name)
return getattr(self._parent, self._attr(name))
def __setattr__(self, name, value):
if name.startswith('_'):
object.__setattr__(self, name, value)
else:
self._parent.__setattr__(self._attr(name), value)
class JsonConfig(object):
_DEFAULT = defaults
_INDENT = 2
def __init__(self, data=None, on_key_changed=None):
self._data = copy.deepcopy(self._DEFAULT)
self._on_key_changed = on_key_changed
if data is not None:
self._data = json.loads(data)
def _restore(self, backup):
self._data = json.loads(backup)
# Recurse into the data and add missing items from _DEFAULT
work_queue = [(self._data, self._DEFAULT)]
while work_queue:
data, default = work_queue.pop()
for key, value in default.iteritems():
if key not in data:
# Copy defaults for missing key
data[key] = copy.deepcopy(value)
elif isinstance(value, dict):
# Recurse into sub-dictionaries
work_queue.append((data[key], value))
def __repr__(self):
return json.dumps(self._data, indent=self._INDENT)
def _lookup(self, name):
return reduce(lambda d, k: d[k], name.split('.'), self._data)
def __getattr__(self, name):
try:
value = self._lookup(name)
if not isinstance(value, dict):
return value
except KeyError:
pass
return JsonConfigSubtree(self, name)
def __setattr__(self, name, value):
if name.startswith('_'):
object.__setattr__(self, name, value)
return
attrs = name.split('.')
target_dict = self._data
while attrs:
attr = attrs.pop(0)
if not attrs:
old_value = target_dict[attr]
if old_value != value:
target_dict[attr] = value
if self._on_key_changed is not None:
self._on_key_changed(name, old_value, value)
break
target = target_dict.get(attr, None)
if target is None or not isinstance(target, dict):
target_dict[attr] = target = {}
target_dict = target
class Config(object):
# Number of seconds after which settings are auto-saved
WRITE_TO_DISK_TIMEOUT = 60
def __init__(self, filename='gpodder.conf'):
dict.__init__(self)
def __init__(self, filename='gpodder.json'):
self.__json_config = JsonConfig(on_key_changed=self._on_key_changed)
self.__save_thread = None
self.__filename = filename
self.__section = 'gpodder-conf-1'
self.__observers = []
self.load()
@ -136,18 +284,12 @@ class Config(dict):
if not os.path.exists(self.__filename):
self.save()
atexit.register( self.__atexit)
def __getattr__(self, name):
if name in self.Settings:
return self[name]
else:
raise AttributeError('%s is not a setting' % name)
atexit.register(self.__atexit)
def add_observer(self, callback):
"""
Add a callback function as observer. This callback
will be called when a setting changes. It should
will be called when a setting changes. It should
have this signature:
observer(name, old_value, new_value)
@ -184,42 +326,20 @@ class Config(dict):
if self.__save_thread is not None:
self.save()
def get_backup(self):
"""Create a backup of the current settings
Returns a dictionary with the current settings which can
be used with "restore_backup" (see below) to restore the
state of the configuration object at a future point in time.
"""
return dict(self)
def restore_backup(self, backup):
"""Restore a previously-created backup
Restore a previously-created configuration backup (created
with "get_backup" above) and notify any observer about the
changed settings.
"""
for key, value in backup.iteritems():
setattr(self, key, value)
def save(self, filename=None):
if filename is None:
filename = self.__filename
logger.info('Flushing settings to disk')
parser = ConfigParser.RawConfigParser()
parser.add_section(self.__section)
for key, default in self.Settings.items():
fieldtype = type(default)
parser.set(self.__section, key, getattr(self, key, default))
try:
parser.write(open(filename, 'w'))
fp = open(filename+'.tmp', 'wb')
fp.write(repr(self.__json_config))
fp.close()
util.atomic_rename(filename+'.tmp', filename)
except:
logger.error('Cannot write settings to %s', filename)
util.delete_file(filename+'.tmp')
raise
self.__save_thread = None
@ -228,85 +348,47 @@ class Config(dict):
if filename is not None:
self.__filename = filename
parser = ConfigParser.RawConfigParser()
if os.path.exists(self.__filename):
try:
parser.read(self.__filename)
data = open(self.__filename, 'rb').read()
self.__json_config._restore(data)
except:
logger.warn('Cannot parse config file: %s',
self.__filename, exc_info=True)
for key, default in self.Settings.items():
fieldtype = type(default)
value = default
try:
if not parser.has_section(self.__section):
value = default
elif fieldtype == int:
value = parser.getint(self.__section, key)
elif fieldtype == float:
value = parser.getfloat(self.__section, key)
elif fieldtype == bool:
value = parser.getboolean(self.__section, key)
else:
value = fieldtype(parser.get(self.__section, key))
except ConfigParser.NoOptionError:
# Not (yet) set in the file, use the default value
value = default
except:
logger.warn('Invalid value in %s for %s: %s',
self.__filename, key, value, exc_info=True)
value = default
self[key] = value
def toggle_flag(self, name):
if name in self.Settings:
default = self.Settings[name]
fieldtype = type(default)
if fieldtype == bool:
setattr(self, name, not getattr(self, name))
else:
logger.warn('Cannot toggle value: %s (not boolean)', name)
else:
logger.warn('Invalid setting name: %s', name)
setattr(self, name, not getattr(self, name))
def update_field(self, name, new_value):
if name in self.Settings:
default = self.Settings[name]
fieldtype = type(default)
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:
new_value = fieldtype(new_value)
except:
logger.warn('Cannot convert %s to %s.', str(new_value),
fieldtype.__name__, exc_info=True)
return False
setattr(self, name, new_value)
return True
else:
logger.info('Ignoring invalid setting: %s', name)
return False
observer(name, old_value, value)
except Exception, exception:
logger.error('Error while calling observer %r: %s',
observer, exception, exc_info=True)
self.schedule_save()
def __getattr__(self, name):
if name in gPodderSettings_LegacySupport:
name = gPodderSettings_LegacySupport[name]
return getattr(self.__json_config, name)
def __setattr__(self, name, value):
if name in self.Settings:
default = self.Settings[name]
fieldtype = type(default)
try:
if self[name] != fieldtype(value):
old_value = self[name]
logger.info('Update %s: %s => %s', name, old_value, value)
self[name] = fieldtype(value)
for observer in self.__observers:
try:
# Notify observer about config change
observer(name, old_value, self[name])
except:
logger.error('Error while calling observer: %s',
repr(observer), exc_info=True)
self.schedule_save()
except:
raise ValueError('%s has to be of type %s' % (name, fieldtype.__name__))
else:
if name.startswith('_'):
object.__setattr__(self, name, value)
return
if name in gPodderSettings_LegacySupport:
name = gPodderSettings_LegacySupport[name]
setattr(self.__json_config, name, value)

View File

@ -50,7 +50,7 @@ class Core(object):
self.config = config_class(gpodder.config_file)
# Update the current device in the configuration
self.config.mygpo_device_type = util.detect_device_type()
self.config.mygpo.device.type = util.detect_device_type()
def shutdown(self):
# Close the database and store outstanding changes

View File

@ -97,7 +97,6 @@ class UIConfig(config.Config):
self.__ignore_window_events = False
def connect_gtk_editable(self, name, editable):
assert name in self.Settings
editable.delete_text(0, -1)
editable.insert_text(str(getattr(self, name)))
@ -106,7 +105,6 @@ class UIConfig(config.Config):
editable.connect('changed', _editable_changed)
def connect_gtk_spinbutton(self, name, spinbutton):
assert name in self.Settings
spinbutton.set_value(getattr(self, name))
def _spinbutton_changed(spinbutton):
@ -114,7 +112,6 @@ class UIConfig(config.Config):
spinbutton.connect('value-changed', _spinbutton_changed)
def connect_gtk_paned(self, name, paned):
assert name in self.Settings
paned.set_position(getattr(self, name))
paned_child = paned.get_child1()
@ -123,7 +120,6 @@ class UIConfig(config.Config):
paned_child.connect('size-allocate', _child_size_allocate)
def connect_gtk_togglebutton(self, name, togglebutton):
assert name in self.Settings
togglebutton.set_active(getattr(self, name))
def _togglebutton_toggled(togglebutton):
@ -131,46 +127,42 @@ class UIConfig(config.Config):
togglebutton.connect('toggled', _togglebutton_toggled)
def connect_gtk_window(self, window, config_prefix, show_window=False):
x, y, width, height, maximized = map(lambda x: config_prefix+'_'+x, \
('x', 'y', 'width', 'height', 'maximized'))
cfg = getattr(self.ui.gtk.state, config_prefix)
if set((x, y, width, height)).issubset(set(self.Settings)):
window.resize(getattr(self, width), getattr(self, height))
if getattr(self, x) == -1 or getattr(self, y) == -1:
window.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
else:
window.move(getattr(self, x), getattr(self, y))
window.resize(cfg.width, cfg.height)
if cfg.x == -1 or cfg.y == -1:
window.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
else:
window.move(cfg.x, cfg.y)
# Ignore events while we're connecting to the window
self.__ignore_window_events = True
# Ignore events while we're connecting to the window
self.__ignore_window_events = True
def _receive_configure_event(widget, event):
x_pos, y_pos = event.x, event.y
width_size, height_size = event.width, event.height
if not self.__ignore_window_events and not \
(hasattr(self, maximized) and getattr(self, maximized)):
setattr(self, x, x_pos)
setattr(self, y, y_pos)
setattr(self, width, width_size)
setattr(self, height, height_size)
def _receive_configure_event(widget, event):
x_pos, y_pos = event.x, event.y
width_size, height_size = event.width, event.height
if not self.__ignore_window_events and not cfg.maximized:
cfg.x = x_pos
cfg.y = y_pos
cfg.width = width_size
cfg.height = height_size
window.connect('configure-event', _receive_configure_event)
window.connect('configure-event', _receive_configure_event)
def _receive_window_state(widget, event):
new_value = bool(event.new_window_state & \
gtk.gdk.WINDOW_STATE_MAXIMIZED)
if hasattr(self, maximized):
setattr(self, maximized, new_value)
def _receive_window_state(widget, event):
new_value = bool(event.new_window_state &
gtk.gdk.WINDOW_STATE_MAXIMIZED)
cfg.maximized = new_value
window.connect('window-state-event', _receive_window_state)
window.connect('window-state-event', _receive_window_state)
# After the window has been set up, we enable events again
def _enable_window_events():
self.__ignore_window_events = False
util.idle_add(_enable_window_events)
# After the window has been set up, we enable events again
def _enable_window_events():
self.__ignore_window_events = False
util.idle_add(_enable_window_events)
if show_window:
window.show()
if getattr(self, maximized, False):
window.maximize()
if show_window:
window.show()
if cfg.maximized:
window.maximize()

View File

@ -93,7 +93,7 @@ class gPodderEpisodeSelector(BuilderWidget):
COLUMN_ADDITIONAL = 3
def new( self):
self._config.connect_gtk_window(self.gPodderEpisodeSelector, '_episode_selector', True)
self._config.connect_gtk_window(self.gPodderEpisodeSelector, 'episode_selector', True)
if not hasattr( self, 'callback'):
self.callback = None

View File

@ -30,6 +30,7 @@ _ = gpodder.gettext
from gpodder import util
from gpodder import opml
from gpodder import youtube
from gpodder import my
from gpodder.gtkui.opml import OpmlListModel
@ -106,7 +107,7 @@ class gPodderPodcastDirectory(BuilderWidget):
def thread_func(self, tab=0):
if tab == 1:
model = OpmlListModel(opml.Importer(self._config.toplist_opml))
model = OpmlListModel(opml.Importer(my.TOPLIST_OPML))
if len(model) == 0:
self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
elif tab == 2:

View File

@ -34,35 +34,26 @@ from gpodder.gtkui.interface.configeditor import gPodderConfigEditor
from gpodder.gtkui.desktopfile import PlayerListModel
class NewEpisodeActionList(gtk.ListStore):
C_CAPTION, C_AUTO_DOWNLOAD, C_HIDE_DIALOG = range(3)
C_CAPTION, C_AUTO_DOWNLOAD = range(2)
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = range(4)
def __init__(self, config):
gtk.ListStore.__init__(self, str, str, bool)
gtk.ListStore.__init__(self, str, str)
self._config = config
self.append((_('Do nothing'), 'never', True))
self.append((_('Show episode list'), 'never', False))
self.append((_('Add to download list'), 'queue', False))
self.append((_('Download if minimized'), 'minimized', False))
self.append((_('Download immediately'), 'always', False))
self.append((_('Do nothing'), 'ignore'))
self.append((_('Show episode list'), 'show'))
self.append((_('Add to download list'), 'queue'))
self.append((_('Download immediately'), 'download'))
def get_index(self):
if self._config.do_not_show_new_episodes_dialog:
return 0
else:
for index, row in enumerate(self):
if row[self.C_HIDE_DIALOG]:
continue
if self._config.auto_download == \
row[self.C_AUTO_DOWNLOAD]:
return index
for index, row in enumerate(self):
if self._config.auto_download == row[self.C_AUTO_DOWNLOAD]:
return index
return 1 # Some sane default
def set_index(self, index):
self._config.do_not_show_new_episodes_dialog = self[index][self.C_HIDE_DIALOG]
self._config.auto_download = self[index][self.C_AUTO_DOWNLOAD]
@ -128,20 +119,20 @@ class gPodderPreferences(BuilderWidget):
self._config.connect_gtk_togglebutton('auto_remove_unfinished_episodes', self.checkbutton_expiration_unfinished)
# Have to do this before calling set_active on checkbutton_enable
self._enable_mygpo = self._config.mygpo_enabled
self._enable_mygpo = self._config.mygpo.enabled
# Initialize the UI state with configuration settings
self.checkbutton_enable.set_active(self._config.mygpo_enabled)
self.entry_username.set_text(self._config.mygpo_username)
self.entry_password.set_text(self._config.mygpo_password)
self.entry_caption.set_text(self._config.mygpo_device_caption)
self.checkbutton_enable.set_active(self._config.mygpo.enabled)
self.entry_username.set_text(self._config.mygpo.username)
self.entry_password.set_text(self._config.mygpo.password)
self.entry_caption.set_text(self._config.mygpo.device.caption)
# Disable mygpo sync while the dialog is open
self._config.mygpo_enabled = False
self._config.mygpo.enabled = False
def on_dialog_destroy(self, widget):
# Re-enable mygpo sync if the user has selected it
self._config.mygpo_enabled = self._enable_mygpo
self._config.mygpo.enabled = self._enable_mygpo
# Make sure the device is successfully created/updated
self.mygpo_client.create_device()
# Flush settings for mygpo client now
@ -226,21 +217,21 @@ class gPodderPreferences(BuilderWidget):
self._enable_mygpo = widget.get_active()
def on_username_changed(self, widget):
self._config.mygpo_username = widget.get_text()
self._config.mygpo.username = widget.get_text()
def on_password_changed(self, widget):
self._config.mygpo_password = widget.get_text()
self._config.mygpo.password = widget.get_text()
def on_device_caption_changed(self, widget):
self._config.mygpo_device_caption = widget.get_text()
self._config.mygpo.device.caption = widget.get_text()
def on_button_overwrite_clicked(self, button):
title = _('Replace subscription list on server')
message = _('Remote podcasts that have not been added locally will be removed on the server. Continue?')
if self.show_confirmation(message, title):
def thread_proc():
self._config.mygpo_enabled = True
self._config.mygpo.enabled = True
self.on_send_full_subscriptions()
self._config.mygpo_enabled = False
self._config.mygpo.enabled = False
threading.Thread(target=thread_proc).start()

View File

@ -34,7 +34,7 @@ class gPodderShownotesBase(BuilderWidget):
setattr(self, 'task', None)
self._config.connect_gtk_window(self.main_window, \
'_episode_window', True)
'episode_window', True)
self.main_window.connect('delete-event', self._on_delete_event)
self.main_window.connect('key-press-event', self._on_key_press_event)
self.on_create_window()

View File

@ -111,14 +111,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.bluetooth_available = util.bluetooth_available()
self.config.connect_gtk_window(self.gPodder, '_main_window')
self.config.connect_gtk_window(self.main_window, 'main_window')
# Default/last paned position for sidebar toggling
self._last_paned_position = 200
self._last_paned_position_toggling = False
self.item_sidebar.set_active(self.config._paned_position > 0)
self.config.connect_gtk_paned('_paned_position', self.channelPaned)
self.config.connect_gtk_paned('paned_position', self.channelPaned)
self.main_window.show()
@ -314,24 +309,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
if not self.channels:
self.on_itemUpdate_activate()
def on_view_sidebar_toggled(self, menu_item):
self.channelPaned.child_set_property(self.vboxChannelNavigator, \
'shrink', not menu_item.get_active())
if self._last_paned_position_toggling:
return
active = menu_item.get_active()
if active:
if self._last_paned_position == 0:
self._last_paned_position = 200
self.channelPaned.set_position(self._last_paned_position)
else:
current_position = self.channelPaned.get_position()
if current_position > 0:
self._last_paned_position = current_position
self.channelPaned.set_position(0)
def episode_object_by_uri(self, uri):
"""Get an episode object given a local or remote URI
@ -1189,21 +1166,18 @@ class gPodder(BuilderWidget, dbus.service.Object):
util.idle_add(self._on_config_changed, *args)
def _on_config_changed(self, name, old_value, new_value):
if name == 'show_toolbar':
if name == 'ui.gtk.toolbar':
self.toolbar.set_property('visible', new_value)
elif name == 'episode_list_descriptions':
elif name == 'ui.gtk.episode_list.descriptions':
self.update_episode_list_model()
elif name in ('auto_update_feeds', 'auto_update_frequency'):
elif name in ('auto.update.enabled', 'auto.update.frequency'):
self.restart_auto_update_timer()
elif name in ('podcast_list_view_all', 'podcast_list_sections'):
elif name in ('ui.gtk.podcast_list.all_episodes',
'ui.gtk.podcast_list.sections'):
# Force a update of the podcast list model
self.update_podcast_list_model()
elif name == 'episode_list_columns':
elif name == 'ui.gtk.episode_list.columns':
self.update_episode_list_columns_visibility()
elif name == '_paned_position':
self._last_paned_position_toggling = True
self.item_sidebar.set_active(new_value > 0)
self._last_paned_position_toggling = False
def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
# With get_bin_window, we get the window that contains the rows without
@ -2222,8 +2196,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
# The URL is valid and does not exist already - subscribe!
channel = self.model.load_podcast(url=url, create=True, \
authentication_tokens=auth_tokens.get(url, None), \
max_episodes=self.config.max_episodes_per_feed, \
mimetype_prefs=self.config.mimetype_prefs)
max_episodes=self.config.max_episodes_per_feed)
try:
username, password = util.username_password_from_url(url)
@ -2348,8 +2321,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
break
try:
channel.update(max_episodes=self.config.max_episodes_per_feed,
mimetype_prefs=self.config.mimetype_prefs)
channel.update(max_episodes=self.config.max_episodes_per_feed)
self._update_cover(channel)
except Exception, e:
d = {'url': cgi.escape(channel.url), 'message': cgi.escape(str(e))}
@ -2409,27 +2381,25 @@ class gPodder(BuilderWidget, dbus.service.Object):
count = len(episodes)
# New episodes are available
self.pbFeedUpdate.set_fraction(1.0)
# Are we minimized and should we auto download?
if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
if self.config.auto_download == 'download':
self.download_episode_list(episodes)
title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
self.show_update_feeds_buttons()
elif self.config.auto_download == 'queue':
self.download_episode_list_paused(episodes)
title = N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count) % {'count':count}
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
self.show_update_feeds_buttons()
else:
self.show_update_feeds_buttons()
# New episodes are available and we are not minimized
if (not self.config.do_not_show_new_episodes_dialog
and show_new_episodes_dialog):
if (show_new_episodes_dialog and
self.config.auto_download == 'show'):
self.new_episodes_show(episodes, notification=True)
else:
else: # !show_new_episodes_dialog or auto_download == 'ignore'
message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
self.pbFeedUpdate.set_text(message)
self.show_update_feeds_buttons()
util.idle_add(update_feed_cache_finish_callback)
threading.Thread(target=update_feed_cache_proc).start()
@ -2854,12 +2824,12 @@ class gPodder(BuilderWidget, dbus.service.Object):
title = _('Login to gpodder.net')
message = _('Please login to download your subscriptions.')
success, (username, password) = self.show_login_dialog(title, message, \
self.config.mygpo_username, self.config.mygpo_password)
self.config.mygpo.username, self.config.mygpo.password)
if not success:
return
self.config.mygpo_username = username
self.config.mygpo_password = password
self.config.mygpo.username = username
self.config.mygpo.password = password
dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
custom_title=_('Subscriptions on gpodder.net'), \
@ -2868,10 +2838,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
# TODO: Refactor this into "gpodder.my" or mygpoclient, so that
# we do not have to hardcode the URL here
OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo.username
url = util.url_add_authentication(OPML_URL, \
self.config.mygpo_username, \
self.config.mygpo_password)
self.config.mygpo.username, \
self.config.mygpo.password)
dir.download_opml_file(url)
def on_itemAddChannel_activate(self, widget=None):
@ -3042,7 +3012,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
def on_itemImportChannels_activate(self, widget, *args):
dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
add_urls_callback=self.add_podcast_list)
util.idle_add(dir.download_opml_file, self.config.example_opml)
util.idle_add(dir.download_opml_file, my.EXAMPLES_OPML)
def on_homepage_activate(self, widget, *args):
util.open_website(gpodder.__url__)

View File

@ -181,7 +181,7 @@ class PodcastEpisode(PodcastModelObject):
youtube.is_video_link(self.link))
@classmethod
def from_feedparser_entry(cls, entry, channel, mimetype_prefs=''):
def from_feedparser_entry(cls, entry, channel):
episode = cls(channel)
episode.guid = entry.get('id', '')
@ -228,25 +228,14 @@ class PodcastEpisode(PodcastModelObject):
video_available = any(e.get('type', '').startswith('video/') \
for e in enclosures + media_rss_content)
# Create the list of preferred mime types
mimetype_prefs = mimetype_prefs.split(',')
def calculate_preference_value(enclosure):
"""Calculate preference value of an enclosure
This is based on mime types and allows users to prefer
certain mime types over others (e.g. MP3 over AAC, ...)
"""
mimetype = enclosure.get('type', None)
try:
# If the mime type is found, return its (zero-based) index
return mimetype_prefs.index(mimetype)
except ValueError:
# If it is not found, assume it comes after all listed items
return len(mimetype_prefs)
# XXX: Make it possible for hooks/extensions to override this by
# giving them a list of enclosures and the "self" object (podcast)
# and letting them sort and/or filter the list of enclosures to
# get the desired enclosure picked by the algorithm below.
filter_and_sort_enclosures = lambda x: x
# Enclosures
for e in sorted(enclosures, key=calculate_preference_value):
for e in filter_and_sort_enclosures(enclosures):
episode.mime_type = e.get('type', 'application/octet-stream')
if episode.mime_type == '':
# See Maemo bug 10036
@ -280,7 +269,7 @@ class PodcastEpisode(PodcastModelObject):
return episode
# Media RSS content
for m in sorted(media_rss_content, key=calculate_preference_value):
for m in filter_and_sort_enclosures(media_rss_content):
episode.mime_type = m.get('type', 'application/octet-stream')
if '/' not in episode.mime_type:
continue
@ -900,8 +889,7 @@ class PodcastChannel(PodcastModelObject):
@classmethod
def load(cls, model, url, create=True, authentication_tokens=None,\
max_episodes=0, \
mimetype_prefs=''):
max_episodes=0):
if isinstance(url, unicode):
url = url.encode('utf-8')
@ -922,7 +910,7 @@ class PodcastChannel(PodcastModelObject):
tmp.save()
try:
tmp.update(max_episodes, mimetype_prefs)
tmp.update(max_episodes)
except Exception, e:
logger.debug('Fetch failed. Removing buggy feed.')
tmp.remove_downloaded()
@ -986,7 +974,7 @@ class PodcastChannel(PodcastModelObject):
self.remove_unreachable_episodes(existing, seen_guids, max_episodes)
def _consume_updated_feed(self, feed, max_episodes=0, mimetype_prefs=''):
def _consume_updated_feed(self, feed, max_episodes=0):
#self.parse_error = feed.get('bozo_exception', None)
self._consume_updated_title(feed.feed.get('title', self.url))
@ -1047,7 +1035,7 @@ class PodcastChannel(PodcastModelObject):
# Search all entries for new episodes
for entry in entries:
try:
episode = self.EpisodeClass.from_feedparser_entry(entry, self, mimetype_prefs)
episode = self.EpisodeClass.from_feedparser_entry(entry, self)
if episode is not None:
if not episode.title:
logger.warn('Using filename as title for %s',
@ -1132,7 +1120,7 @@ class PodcastChannel(PodcastModelObject):
self.http_etag = feed.headers.get('etag', self.http_etag)
self.http_last_modified = feed.headers.get('last-modified', self.http_last_modified)
def update(self, max_episodes=0, mimetype_prefs=''):
def update(self, max_episodes=0):
try:
self.feed_fetcher.fetch_channel(self)
except CustomFeed, updated:
@ -1141,7 +1129,7 @@ class PodcastChannel(PodcastModelObject):
self.save()
except feedcore.UpdatedFeed, updated:
feed = updated.data
self._consume_updated_feed(feed, max_episodes, mimetype_prefs)
self._consume_updated_feed(feed, max_episodes)
self._update_etag_modified(feed)
self.save()
except feedcore.NewLocation, updated:
@ -1150,7 +1138,7 @@ class PodcastChannel(PodcastModelObject):
if feed.href in set(x.url for x in self.model.get_podcasts()):
raise Exception('Already subscribed to ' + feed.href)
self.url = feed.href
self._consume_updated_feed(feed, max_episodes, mimetype_prefs)
self._consume_updated_feed(feed, max_episodes)
self._update_etag_modified(feed)
self.save()
except feedcore.NotModified, updated:
@ -1376,10 +1364,10 @@ class Model(object):
return self.children
def load_podcast(self, url, create=True, authentication_tokens=None,
max_episodes=0, mimetype_prefs=''):
max_episodes=0):
return self.PodcastClass.load(self, url, create,
authentication_tokens,
max_episodes, mimetype_prefs)
max_episodes)
@classmethod
def podcast_sort_key(cls, podcast):

View File

@ -63,6 +63,8 @@ from mygpoclient import public
from mygpoclient import util as mygpoutil
EXAMPLES_OPML = 'http://gpodder.org/directory.opml'
TOPLIST_OPML = 'http://gpodder.org/toplist.opml'
# Database model classes
class SinceValue(object):
@ -197,8 +199,8 @@ class MygPoClient(object):
# Insert our new update action
action = UpdateDeviceAction(self.device_id, \
self._config.mygpo_device_caption, \
self._config.mygpo_device_type)
self._config.mygpo.device.caption, \
self._config.mygpo.device.type)
self._store.save(action)
def get_rewritten_urls(self):
@ -307,14 +309,14 @@ class MygPoClient(object):
@property
def host(self):
return self._config.mygpo_server
return self._config.mygpo.server
@property
def device_id(self):
return self._config.mygpo_device_uid
return self._config.mygpo.device.uid
def can_access_webservice(self):
return self._config.mygpo_enabled and self._config.mygpo_device_uid
return self._config.mygpo.enabled and self._config.mygpo.device.uid
def set_subscriptions(self, urls):
if self.can_access_webservice():
@ -442,12 +444,12 @@ class MygPoClient(object):
logger.debug('Flush requested, already waiting.')
def on_config_changed(self, name=None, old_value=None, new_value=None):
if name in ('mygpo_username', 'mygpo_password', 'mygpo_server') \
if name in ('mygpo.username', 'mygpo.password', 'mygpo.server') \
or self._client is None:
self._client = api.MygPodderClient(self._config.mygpo_username,
self._config.mygpo_password, self._config.mygpo_server)
self._client = api.MygPodderClient(self._config.mygpo.username,
self._config.mygpo.password, self._config.mygpo.server)
logger.info('Reloading settings.')
elif name.startswith('mygpo_device_'):
elif name.startswith('mygpo.device.'):
# Update or create the device
self.create_device()
@ -587,7 +589,7 @@ class MygPoClient(object):
return result
def open_website(self):
util.open_website('http://' + self._config.mygpo_server)
util.open_website('http://' + self._config.mygpo.server)
class Directory(object):

View File

@ -187,11 +187,7 @@ class Exporter(object):
fp = open(self.filename+'.tmp', 'w')
fp.write(data)
fp.close()
if gpodder.win32:
# Win32 does not support atomic rename with os.rename
shutil.move(self.filename+'.tmp', self.filename)
else:
os.rename(self.filename+'.tmp', self.filename)
util.atomic_rename(self.filename+'.tmp', self.filename)
except:
logger.error('Could not open file for writing: %s', self.filename,
exc_info=True)

View File

@ -75,13 +75,13 @@ class Controller(QObject):
def on_config_changed(self, name, old_value, new_value):
logger.info('Config changed: %s (%s -> %s)', name,
old_value, new_value)
if name == 'mygpo_enabled':
if name == 'mygpo.enabled':
self.myGpoEnabledChanged.emit()
elif name == 'mygpo_username':
elif name == 'mygpo.username':
self.myGpoUsernameChanged.emit()
elif name == 'mygpo_password':
elif name == 'mygpo.password':
self.myGpoPasswordChanged.emit()
elif name == 'mygpo_device_caption':
elif name == 'mygpo.device.caption':
self.myGpoDeviceCaptionChanged.emit()
episodeListTitleChanged = Signal()
@ -198,10 +198,10 @@ class Controller(QObject):
myGpoEnabledChanged = Signal()
def getMyGpoEnabled(self):
return self.root.config.mygpo_enabled
return self.root.config.mygpo.enabled
def setMyGpoEnabled(self, enabled):
self.root.config.mygpo_enabled = enabled
self.root.config.mygpo.enabled = enabled
myGpoEnabled = Property(bool, getMyGpoEnabled,
setMyGpoEnabled, notify=myGpoEnabledChanged)
@ -209,10 +209,10 @@ class Controller(QObject):
myGpoUsernameChanged = Signal()
def getMyGpoUsername(self):
return model.convert(self.root.config.mygpo_username)
return model.convert(self.root.config.mygpo.username)
def setMyGpoUsername(self, username):
self.root.config.mygpo_username = username
self.root.config.mygpo.username = username
myGpoUsername = Property(unicode, getMyGpoUsername,
setMyGpoUsername, notify=myGpoUsernameChanged)
@ -220,10 +220,10 @@ class Controller(QObject):
myGpoPasswordChanged = Signal()
def getMyGpoPassword(self):
return model.convert(self.root.config.mygpo_password)
return model.convert(self.root.config.mygpo.password)
def setMyGpoPassword(self, password):
self.root.config.mygpo_password = password
self.root.config.mygpo.password = password
myGpoPassword = Property(unicode, getMyGpoPassword,
setMyGpoPassword, notify=myGpoPasswordChanged)
@ -231,10 +231,10 @@ class Controller(QObject):
myGpoDeviceCaptionChanged = Signal()
def getMyGpoDeviceCaption(self):
return model.convert(self.root.config.mygpo_device_caption)
return model.convert(self.root.config.mygpo.device.caption)
def setMyGpoDeviceCaption(self, caption):
self.root.config.mygpo_device_caption = caption
self.root.config.mygpo.device.caption = caption
myGpoDeviceCaption = Property(unicode, getMyGpoDeviceCaption,
setMyGpoDeviceCaption, notify=myGpoDeviceCaptionChanged)
@ -484,8 +484,7 @@ class Controller(QObject):
self.root.start_progress(_('Adding podcasts...') + ' (%d/%d)' % (idx, len(urls)))
try:
podcast = self.root.model.load_podcast(url=url, create=True,
max_episodes=self.root.config.max_episodes_per_feed,
mimetype_prefs=self.root.config.mimetype_prefs)
max_episodes=self.root.config.max_episodes_per_feed)
podcast.save()
self.root.insert_podcast(model.QPodcast(podcast))
except Exception, e:

View File

@ -41,6 +41,7 @@ import platform
import glob
import stat
import shlex
import shutil
import socket
import sys
import string
@ -1480,3 +1481,17 @@ def is_known_redirecter(url):
return False
def atomic_rename(old_name, new_name):
"""Atomically rename/move a (temporary) file
This is usually used when updating a file safely by writing
the new contents into a temporary file and then moving the
temporary file over the original file to replace it.
"""
if gpodder.win32:
# Win32 does not support atomic rename with os.rename
shutil.move(old_name, new_name)
else:
os.rename(old_name, new_name)