my.gpodder.org Advanced API Support (bug 691)
This support is still very early, and some bits and pieces are missing, but it works for the most basic use cases. New hard dependency on "mygpoclient", which you can get from the following URL: http://thpinfo.com/2010/mygpoclient/
This commit is contained in:
parent
8cbbea9b8b
commit
b20357f832
1
README
1
README
|
@ -32,6 +32,7 @@
|
|||
* python (>= 2.5)
|
||||
* python-gtk2 (>= 2.12)
|
||||
* python-feedparser
|
||||
* python-mygpoclient (http://thpinfo.com/2010/mygpoclient/)
|
||||
* python-dbus (optional, but highly recommended)
|
||||
|
||||
Additional dependencies for iPod synchronization support:
|
||||
|
|
|
@ -161,17 +161,10 @@
|
|||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="itemUploadToMygpo">
|
||||
<property name="name">itemUploadToMygpo</property>
|
||||
<property name="label" translatable="yes">Upload to my.gpodder.org</property>
|
||||
<signal handler="on_upload_to_mygpo" name="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="itemDownloadFromMygpo">
|
||||
<property name="name">itemDownloadFromMygpo</property>
|
||||
<property name="label" translatable="yes">Download from my.gpodder.org</property>
|
||||
<signal handler="on_download_from_mygpo" name="activate"/>
|
||||
<object class="GtkAction" id="item_mygpo_settings">
|
||||
<property name="name">item_mygpo_settings</property>
|
||||
<property name="label" translatable="yes">my.gpodder.org Settings</property>
|
||||
<signal handler="on_mygpo_settings_activate" name="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -434,8 +427,7 @@
|
|||
<separator/>
|
||||
<menuitem action="itemMassUnsubscribe"/>
|
||||
<separator/>
|
||||
<menuitem action="itemUploadToMygpo"/>
|
||||
<menuitem action="itemDownloadFromMygpo"/>
|
||||
<menuitem action="item_mygpo_settings"/>
|
||||
<menuitem action="item_goto_mygpo"/>
|
||||
</menu>
|
||||
<menu action="menuChannels">
|
||||
|
|
|
@ -89,16 +89,6 @@
|
|||
<property name="label" translatable="yes">Export to OPML file</property>
|
||||
<signal handler="on_itemExportChannels_activate" name="activate"/>
|
||||
</object>
|
||||
<object class="GtkAction" id="itemUploadToMygpo">
|
||||
<property name="name">itemUploadToMygpo</property>
|
||||
<property name="label" translatable="yes">Upload to my.gpodder.org</property>
|
||||
<signal handler="on_upload_to_mygpo" name="activate"/>
|
||||
</object>
|
||||
<object class="GtkAction" id="itemDownloadFromMygpo">
|
||||
<property name="name">itemDownloadFromMygpo</property>
|
||||
<property name="label" translatable="yes">Download from my.gpodder.org</property>
|
||||
<signal handler="on_download_from_mygpo" name="activate"/>
|
||||
</object>
|
||||
<object class="GtkRadioAction" id="item_view_podcasts_all">
|
||||
<property name="name">item_view_podcasts_all</property>
|
||||
<property name="label" translatable="yes">All</property>
|
||||
|
|
|
@ -162,17 +162,16 @@
|
|||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="itemUploadToMygpo">
|
||||
<property name="name">itemUploadToMygpo</property>
|
||||
<property name="label" translatable="yes">Upload to my.gpodder.org</property>
|
||||
<signal handler="on_upload_to_mygpo" name="activate"/>
|
||||
<object class="GtkAction" id="item_mygpo_settings">
|
||||
<property name="name">item_mygpo_settings</property>
|
||||
<property name="label" translatable="yes">my.gpodder.org Settings</property>
|
||||
<signal handler="on_mygpo_settings_activate" name="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="itemDownloadFromMygpo">
|
||||
<property name="name">itemDownloadFromMygpo</property>
|
||||
<property name="label" translatable="yes">Download from my.gpodder.org</property>
|
||||
<signal handler="on_download_from_mygpo" name="activate"/>
|
||||
<object class="GtkAction" id="item_goto_mygpo">
|
||||
<property name="label" translatable="yes">Go to my.gpodder.org</property>
|
||||
<signal handler="on_goto_mygpo" name="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -391,8 +390,8 @@
|
|||
<menuitem action="item_import_from_file"/>
|
||||
<menuitem action="itemExportChannels"/>
|
||||
<separator/>
|
||||
<menuitem action="itemUploadToMygpo"/>
|
||||
<menuitem action="itemDownloadFromMygpo"/>
|
||||
<menuitem action="item_mygpo_settings"/>
|
||||
<menuitem action="item_goto_mygpo"/>
|
||||
</menu>
|
||||
<menu action="menuChannels">
|
||||
<menuitem action="itemPlaySelected"/>
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--*- mode: xml -*-->
|
||||
<interface>
|
||||
<object class="GtkDialog" id="MygPodderSettings">
|
||||
<property name="default_height">260</property>
|
||||
<property name="default_width">320</property>
|
||||
<property context="yes" name="title" translatable="yes">my.gPodder.org settings</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="visible">True</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkVBox" id="vbox">
|
||||
<property name="border_width">2</property>
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<object class="GtkTable" id="table">
|
||||
<property name="border_width">12</property>
|
||||
<property name="column_spacing">6</property>
|
||||
<property name="n_columns">3</property>
|
||||
<property name="n_rows">9</property>
|
||||
<property name="row_spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_general">
|
||||
<property context="yes" name="label" translatable="yes"><b>General</b></property>
|
||||
<property name="use_markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0.0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="y_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="checkbutton_enable">
|
||||
<property context="yes" name="label" translatable="yes">Enable synchronization of subscription list</property>
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_username">
|
||||
<property context="yes" name="label" translatable="yes">Username:</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">1.0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">3</property>
|
||||
<property name="top_attach">2</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_password">
|
||||
<property context="yes" name="label" translatable="yes">Password:</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">1.0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">4</property>
|
||||
<property name="top_attach">3</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHSeparator" id="hseparator">
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">5</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_device">
|
||||
<property context="yes" name="label" translatable="yes"><b>Device configuration</b></property>
|
||||
<property name="use_markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0.0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">6</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">5</property>
|
||||
<property name="y_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_uid">
|
||||
<property context="yes" name="label" translatable="yes">UID:</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">1.0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">7</property>
|
||||
<property name="top_attach">6</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_caption">
|
||||
<property context="yes" name="label" translatable="yes">Caption:</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">1.0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">8</property>
|
||||
<property name="top_attach">7</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_type">
|
||||
<property context="yes" name="label" translatable="yes">Type:</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">1.0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">9</property>
|
||||
<property name="top_attach">8</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="entry_username">
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">3</property>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="entry_password">
|
||||
<property name="visibility">False</property>
|
||||
<property name="is_focus">True</property>
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">4</property>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="entry_uid">
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">7</property>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="entry_caption">
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">8</property>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">7</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="combo_type">
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">9</property>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">8</property>
|
||||
<property name="y_options"></property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="button_list_uids">
|
||||
<property context="yes" name="label" translatable="yes">List UIDs</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<signal handler="on_button_list_uids_clicked" name="clicked"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">7</property>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">6</property>
|
||||
<property name="x_options">fill</property>
|
||||
<property name="y_options"></property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkHButtonBox" id="action_area">
|
||||
<property name="border_width">5</property>
|
||||
<property name="layout_style">end</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="button_cancel">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="visible">True</property>
|
||||
<signal handler="on_button_cancel_clicked" name="clicked"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="button_save">
|
||||
<property name="label">gtk-save</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="visible">True</property>
|
||||
<signal handler="on_button_save_clicked" name="clicked"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="pack_type">end</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
|
@ -41,6 +41,15 @@ except ImportError:
|
|||
sys.exit(1)
|
||||
del feedparser
|
||||
|
||||
try:
|
||||
import mygpoclient
|
||||
except ImportError:
|
||||
print """
|
||||
Error: Module "mygpoclient" not found. Please install "python-mygpoclient"
|
||||
or download it from http://thpinfo.com/2010/mygpoclient/
|
||||
"""
|
||||
sys.exit(1)
|
||||
del mygpoclient
|
||||
|
||||
# The User-Agent string for downloads
|
||||
user_agent = 'gPodder/%s (+%s)' % (__version__, __url__)
|
||||
|
|
|
@ -232,16 +232,23 @@ gPodderSettings = {
|
|||
'youtube_preferred_fmt_id': (int, 18,
|
||||
('The preferred video format that should be downloaded from YouTube.')),
|
||||
|
||||
# Settings for my.gpodder.org
|
||||
'my_gpodder_username': (str, '',
|
||||
# my.gpodder.org general settings
|
||||
'mygpo_username': (str, '',
|
||||
("The user's gPodder web services username.")),
|
||||
'my_gpodder_password': (str, '',
|
||||
'mygpo_password': (str, '',
|
||||
("The user's gPodder web services password.")),
|
||||
'my_gpodder_autoupload': (bool, False,
|
||||
("Upload the user's podcast list to the gPodder web services when "
|
||||
"gPodder is closed.")),
|
||||
'my_gpodder_service': (str, 'http://my.gpodder.org',
|
||||
('The base URL of the my.gpodder.org service.')),
|
||||
'mygpo_enabled': (bool, False,
|
||||
("Synchronize subscriptions with the web service.")),
|
||||
'mygpo_server': (str, 'my.gpodder.org',
|
||||
('The hostname of the mygpo server in use.')),
|
||||
|
||||
# my.gpodder.org device-specific settings
|
||||
'mygpo_device_uid': (str, '',
|
||||
("The UID that is assigned to this installation.")),
|
||||
'mygpo_device_caption': (str, '',
|
||||
("The human-readable name of this installation.")),
|
||||
'mygpo_device_type': (str, 'desktop',
|
||||
("The type of the device gPodder is running on.")),
|
||||
|
||||
# Paned position
|
||||
'paned_position': ( int, 200,
|
||||
|
|
|
@ -174,8 +174,8 @@ class gPodderPreferences(BuilderWidget):
|
|||
self._config.videoplayer = new_value
|
||||
|
||||
def update_button_mygpo(self):
|
||||
if self._config.my_gpodder_username:
|
||||
self.button_mygpo.set_value(self._config.my_gpodder_username)
|
||||
if self._config.mygpo_username:
|
||||
self.button_mygpo.set_value(self._config.mygpo_username)
|
||||
else:
|
||||
self.button_mygpo.set_value(_('Not logged in'))
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
# Copyright (c) 2005-2010 Thomas Perl and 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/>.
|
||||
#
|
||||
|
||||
# gpodder.gtkui.mygpodder- UI code for my.gpodder.org settings
|
||||
# Thomas Perl <thpinfo.com>; 2010-01-19
|
||||
|
||||
import gtk
|
||||
|
||||
import gpodder
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
from gpodder.gtkui.interface.common import BuilderWidget
|
||||
|
||||
class MygPodderSettings(BuilderWidget):
|
||||
# Valid types defined in mygpoclient.api.PodcastDevice
|
||||
VALID_TYPES = (
|
||||
('desktop', _('Desktop')),
|
||||
('laptop', _('Laptop')),
|
||||
('mobile', _('Mobile phone')),
|
||||
('server', _('Server')),
|
||||
('other', _('Other')),
|
||||
)
|
||||
|
||||
# Columns IDs for the combo box model
|
||||
C_ID, C_CAPTION = range(2)
|
||||
|
||||
def new(self):
|
||||
active_index = 0
|
||||
self._model = gtk.ListStore(str, str)
|
||||
for index, data in enumerate(self.VALID_TYPES):
|
||||
id, caption = data
|
||||
if id == self.config.mygpo_device_type:
|
||||
active_index = index
|
||||
self._model.append(data)
|
||||
self.combo_type.set_model(self._model)
|
||||
|
||||
cell = gtk.CellRendererText()
|
||||
self.combo_type.pack_start(cell, True)
|
||||
self.combo_type.add_attribute(cell, 'text', 1)
|
||||
|
||||
# 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_uid.set_text(self.config.mygpo_device_uid)
|
||||
self.entry_caption.set_text(self.config.mygpo_device_caption)
|
||||
self.combo_type.set_active(active_index)
|
||||
|
||||
def on_button_list_uids_clicked(self, button):
|
||||
# FIXME: Not implemented yet
|
||||
pass
|
||||
|
||||
def on_button_cancel_clicked(self, button):
|
||||
# Ignore changed settings and close
|
||||
self.main_window.destroy()
|
||||
|
||||
def on_button_save_clicked(self, button):
|
||||
model = self.combo_type.get_model()
|
||||
it = self.combo_type.get_active_iter()
|
||||
device_type = model.get_value(it, self.C_ID)
|
||||
|
||||
# Update configuration and close
|
||||
self.config.mygpo_enabled = self.checkbutton_enable.get_active()
|
||||
self.config.mygpo_username = self.entry_username.get_text()
|
||||
self.config.mygpo_password = self.entry_password.get_text()
|
||||
self.config.mygpo_device_uid = self.entry_uid.get_text()
|
||||
self.config.mygpo_device_caption = self.entry_caption.get_text()
|
||||
self.config.mygpo_device_type = device_type
|
||||
|
||||
self.main_window.destroy()
|
||||
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
#
|
||||
|
||||
import os
|
||||
import cgi
|
||||
import gtk
|
||||
import gtk.gdk
|
||||
import gobject
|
||||
|
@ -88,6 +89,7 @@ from gpodder.gtkui.draw import draw_text_box_centered
|
|||
from gpodder.gtkui.interface.common import BuilderWidget
|
||||
from gpodder.gtkui.interface.common import TreeViewHelper
|
||||
from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
|
||||
from gpodder.gtkui.mygpodder import MygPodderSettings
|
||||
|
||||
if gpodder.ui.desktop:
|
||||
from gpodder.gtkui.download import DownloadStatusModel
|
||||
|
@ -468,10 +470,88 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.main_window.set_title(_('gPodder'))
|
||||
hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
|
||||
|
||||
# Set up the first instance of MygPoClient
|
||||
self.mygpo_client = my.MygPoClient(self.config,
|
||||
on_add_remove_podcasts=self.on_add_remove_podcasts_mygpo,
|
||||
on_rewrite_url=self.on_rewrite_url_mygpo,
|
||||
on_send_full_subscriptions=self.on_send_full_subscriptions)
|
||||
util.idle_add(self.mygpo_client.schedule_podcast_sync)
|
||||
|
||||
# First-time users should be asked if they want to see the OPML
|
||||
if not self.channels and not gpodder.ui.fremantle:
|
||||
util.idle_add(self.on_itemUpdate_activate)
|
||||
|
||||
def on_add_remove_podcasts_mygpo(self, add_urls, remove_urls):
|
||||
existing_urls = [c.url for c in self.channels]
|
||||
|
||||
# Columns for the episode selector window - just one...
|
||||
columns = (
|
||||
('description', None, None, _('Action')),
|
||||
)
|
||||
|
||||
# A list of actions that have to be chosen from
|
||||
actions = []
|
||||
|
||||
for url in add_urls:
|
||||
if url not in existing_urls:
|
||||
actions.append(my.Change(url, my.Change.ADD))
|
||||
|
||||
for url in remove_urls:
|
||||
if url in existing_urls:
|
||||
podcast_object = None
|
||||
for podcast in self.channels:
|
||||
if podcast.url == url:
|
||||
podcast_object = podcast
|
||||
break
|
||||
actions.append(my.Change(url, my.Change.REMOVE, podcast))
|
||||
|
||||
def execute_podcast_actions(selected):
|
||||
subscribe_list = [a.url for a in selected if a.change == a.ADD]
|
||||
remove_list = [a.podcast for a in selected if a.change == a.REMOVE]
|
||||
# Apply the accepted changes locally
|
||||
self.add_podcast_list(subscribe_list)
|
||||
self.remove_podcast_list(remove_list, confirm=False)
|
||||
|
||||
unselected = [a for a in actions if a not in selected]
|
||||
add_urls = [a.url for a in unselected if a.change == a.REMOVE]
|
||||
remove_urls = [a.url for a in unselected if a.change == a.ADD]
|
||||
# Revert the declined changes on the server
|
||||
self.mygpo_client.on_subscribe(add_urls)
|
||||
self.mygpo_client.on_unsubscribe(remove_urls)
|
||||
|
||||
def ask():
|
||||
# We're abusing the Episode Selector again ;) -- thp
|
||||
gPodderEpisodeSelector(self.main_window, \
|
||||
title=_('Confirm changes from my.gpodder.org'), \
|
||||
instructions=_('Select the actions you want to carry out.'), \
|
||||
episodes=actions, \
|
||||
columns=columns, \
|
||||
size_attribute=None, \
|
||||
stock_ok_button=gtk.STOCK_APPLY, \
|
||||
callback=execute_podcast_actions, \
|
||||
_config=self.config)
|
||||
|
||||
if actions:
|
||||
util.idle_add(ask)
|
||||
|
||||
def on_rewrite_url_mygpo(self, old_url, new_url):
|
||||
# Called by the mygpo client if a local URL needs to be fixed
|
||||
if not new_url:
|
||||
return
|
||||
|
||||
for channel in self.channels:
|
||||
if channel.url == old_url:
|
||||
log('Updating URL of %s to %s', channel, new_url, sender=self)
|
||||
channel.url = new_url
|
||||
channel.save()
|
||||
self.channel_list_changed = True
|
||||
util.idle_add(self.update_episode_list_model)
|
||||
return
|
||||
|
||||
def on_send_full_subscriptions(self):
|
||||
# Send the full subscription list to the my.gpodder.org client
|
||||
self.mygpo_client.on_subscribe([c.url for c in self.channels])
|
||||
|
||||
def on_podcast_selected(self, treeview, path, column):
|
||||
# for Maemo 5's UI
|
||||
model = treeview.get_model()
|
||||
|
@ -2123,6 +2203,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
error_messages.get(url, _('Unknown')))) for url in failed)
|
||||
self.show_message(message, title, important=True)
|
||||
|
||||
# Upload subscription changes to my.gpodder.org
|
||||
self.mygpo_client.on_subscribe(worked)
|
||||
|
||||
# If at least one podcast has been added, save and update all
|
||||
if self.channel_list_changed:
|
||||
self.save_channels_opml()
|
||||
|
@ -2451,9 +2534,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
"""
|
||||
if self.channels:
|
||||
if self.save_channels_opml():
|
||||
if self.config.my_gpodder_autoupload:
|
||||
log('Uploading to my.gpodder.org on close', sender=self)
|
||||
util.idle_add(self.on_upload_to_mygpo, None)
|
||||
pass # FIXME: Add mygpo synchronization here
|
||||
else:
|
||||
self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
|
||||
|
||||
|
@ -2652,7 +2733,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if self.channels:
|
||||
self.update_feed_cache()
|
||||
else:
|
||||
gPodderWelcome(self.gPodder, center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
|
||||
gPodderWelcome(self.gPodder,
|
||||
center_on_widget=self.gPodder,
|
||||
show_example_podcasts_callback=self.on_itemImportChannels_activate,
|
||||
setup_my_gpodder_callback=self.on_mygpo_settings_activate)
|
||||
|
||||
def download_episode_list_paused(self, episodes):
|
||||
self.download_episode_list(episodes, True)
|
||||
|
@ -2847,76 +2931,16 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
gPodderPreferences(self.gPodder, _config=self.config, \
|
||||
callback_finished=self.properties_closed, \
|
||||
user_apps_reader=self.user_apps_reader, \
|
||||
mygpo_login=lambda: self.require_my_gpodder_authentication(force_dialog=True))
|
||||
mygpo_login=self.on_mygpo_settings_activate)
|
||||
|
||||
def on_itemDependencies_activate(self, widget):
|
||||
gPodderDependencyManager(self.gPodder)
|
||||
|
||||
def require_my_gpodder_authentication(self, force_dialog=False):
|
||||
if force_dialog or (not self.config.my_gpodder_username or not self.config.my_gpodder_password):
|
||||
success, authentication = self.show_login_dialog(_('Login to my.gpodder.org'), _('Please enter your e-mail address and your password.'), username=self.config.my_gpodder_username, password=self.config.my_gpodder_password, username_prompt=_('E-Mail Address'), register_callback=lambda: util.open_website('http://my.gpodder.org/register'))
|
||||
if success:
|
||||
self.config.my_gpodder_username, self.config.my_gpodder_password = authentication
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def on_goto_mygpo(self, widget):
|
||||
client = my.MygPodderClient(self.config.my_gpodder_service, \
|
||||
self.config.my_gpodder_username, \
|
||||
self.config.my_gpodder_password)
|
||||
client.open_website()
|
||||
self.mygpo_client.open_website()
|
||||
|
||||
def on_download_from_mygpo(self, widget=None):
|
||||
if self.require_my_gpodder_authentication():
|
||||
client = my.MygPodderClient(self.config.my_gpodder_service, \
|
||||
self.config.my_gpodder_username, self.config.my_gpodder_password)
|
||||
opml_data = client.download_subscriptions()
|
||||
if len(opml_data) > 0:
|
||||
fp = open(gpodder.subscription_file, 'w')
|
||||
fp.write(opml_data)
|
||||
fp.close()
|
||||
(added, skipped) = (0, 0)
|
||||
i = opml.Importer(gpodder.subscription_file)
|
||||
|
||||
existing = [c.url for c in self.channels]
|
||||
urls = [item['url'] for item in i.items if item['url'] not in existing]
|
||||
|
||||
skipped = len(i.items) - len(urls)
|
||||
added = len(urls)
|
||||
|
||||
self.add_podcast_list(urls)
|
||||
if added > 0:
|
||||
d = {'added': added, 'skipped': skipped}
|
||||
message = _('Added %(added)d new and skipped %(skipped)d existing subscriptions.')
|
||||
self.show_message(message % d, _('Result of subscription download'), widget=self.treeChannels)
|
||||
elif widget is not None:
|
||||
self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'), widget=self.treeChannels)
|
||||
else:
|
||||
self.config.my_gpodder_password = ''
|
||||
self.on_download_from_mygpo(widget)
|
||||
else:
|
||||
self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
|
||||
|
||||
def on_upload_to_mygpo(self, widget):
|
||||
if self.require_my_gpodder_authentication():
|
||||
client = my.MygPodderClient(self.config.my_gpodder_service, \
|
||||
self.config.my_gpodder_username, self.config.my_gpodder_password)
|
||||
self.save_channels_opml()
|
||||
success, messages = client.upload_subscriptions(gpodder.subscription_file)
|
||||
if widget is not None:
|
||||
if not success:
|
||||
self.show_message('\n'.join(messages), _('Results of upload'), important=True)
|
||||
self.config.my_gpodder_password = ''
|
||||
self.on_upload_to_mygpo(widget)
|
||||
else:
|
||||
self.show_message('\n'.join(messages), _('Results of upload'), widget=self.treeChannels)
|
||||
elif not success:
|
||||
log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
|
||||
elif widget is not None:
|
||||
self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
|
||||
def on_mygpo_settings_activate(self, action=None):
|
||||
settings = MygPodderSettings(self.main_window, config=self.config)
|
||||
|
||||
def on_itemAddChannel_activate(self, widget=None):
|
||||
gPodderAddPodcast(self.gPodder, \
|
||||
|
@ -2972,6 +2996,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
progress = ProgressIndicator(title, info, parent=self.main_window)
|
||||
|
||||
def finish_deletion(select_url):
|
||||
# Upload subscription list changes to the web service
|
||||
self.mygpo_client.on_unsubscribe([c.url for c in channels])
|
||||
|
||||
# Re-load the channels and select the desired new channel
|
||||
self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
|
||||
progress.on_finished()
|
||||
|
@ -2995,7 +3022,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
if len(channels) == 1:
|
||||
# get the URL of the podcast we want to select next
|
||||
position = self.channels.index(channel)
|
||||
if channel in self.channels:
|
||||
position = self.channels.index(channel)
|
||||
else:
|
||||
position = -1
|
||||
|
||||
if position == len(self.channels)-1:
|
||||
# this is the last podcast, so select the URL
|
||||
# of the item before this one (i.e. the "new last")
|
||||
|
@ -3095,7 +3126,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.config.opml_url, \
|
||||
self.add_podcast_list, \
|
||||
self.on_itemAddChannel_activate, \
|
||||
self.on_download_from_mygpo, \
|
||||
self.on_mygpo_settings_activate, \
|
||||
self.show_text_edit_dialog)
|
||||
else:
|
||||
dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
|
||||
|
|
|
@ -20,113 +20,260 @@
|
|||
|
||||
|
||||
#
|
||||
# my.py -- "my gPodder" service client
|
||||
# Thomas Perl <thp@gpodder.org> 2008-12-08
|
||||
# my.py -- mygpo Client Abstraction for gPodder
|
||||
# Thomas Perl <thp@gpodder.org>; 2010-01-19
|
||||
#
|
||||
|
||||
import gpodder
|
||||
_ = gpodder.gettext
|
||||
|
||||
import atexit
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from gpodder.liblogger import log
|
||||
|
||||
from gpodder import util
|
||||
|
||||
########################################################################
|
||||
# Based on upload_test.py
|
||||
# Copyright Michael Foord, 2004 & 2005.
|
||||
# Released subject to the BSD License
|
||||
# Please see http://www.voidspace.org.uk/documents/BSD-LICENSE.txt
|
||||
# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
|
||||
# E-mail fuzzyman@voidspace.org.uk
|
||||
########################################################################
|
||||
from mygpoclient import api
|
||||
|
||||
import urllib2
|
||||
import mimetypes
|
||||
import mimetools
|
||||
import webbrowser
|
||||
|
||||
def encode_multipart_formdata(fields, files, BOUNDARY = '-----'+mimetools.choose_boundary()+'-----'):
|
||||
""" Encodes fields and files for uploading.
|
||||
fields is a sequence of (name, value) elements for regular form fields - or a dictionary.
|
||||
files is a sequence of (name, filename, value) elements for data to be uploaded as files.
|
||||
Return (content_type, body) ready for urllib2.Request instance
|
||||
You can optionally pass in a boundary string to use or we'll let mimetools provide one.
|
||||
"""
|
||||
CRLF = '\r\n'
|
||||
L = []
|
||||
if isinstance(fields, dict):
|
||||
fields = fields.items()
|
||||
for (key, value) in fields:
|
||||
L.append('--' + BOUNDARY)
|
||||
L.append('Content-Disposition: form-data; name="%s"' % key)
|
||||
L.append('')
|
||||
L.append(value)
|
||||
for (key, filename, value) in files:
|
||||
filetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||
L.append('--' + BOUNDARY)
|
||||
L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
|
||||
L.append('Content-Type: %s' % filetype)
|
||||
L.append('')
|
||||
L.append(value)
|
||||
L.append('--' + BOUNDARY + '--')
|
||||
L.append('')
|
||||
body = CRLF.join(L)
|
||||
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||
return content_type, body
|
||||
|
||||
def build_request(theurl, fields, files, txheaders=None):
|
||||
"""Given the fields to set and the files to encode it returns a fully formed urllib2.Request object.
|
||||
You can optionally pass in additional headers to encode into the opject. (Content-type and Content-length will be overridden if they are set).
|
||||
fields is a sequence of (name, value) elements for regular form fields - or a dictionary.
|
||||
files is a sequence of (name, filename, value) elements for data to be uploaded as files.
|
||||
"""
|
||||
content_type, body = encode_multipart_formdata(fields, files)
|
||||
if not txheaders: txheaders = {}
|
||||
txheaders['Content-type'] = content_type
|
||||
txheaders['Content-length'] = str(len(body))
|
||||
txheaders['User-agent'] = gpodder.user_agent
|
||||
return urllib2.Request(theurl, body, txheaders)
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
|
||||
class MygPodderClient(object):
|
||||
def __init__(self, service_uri, username, password):
|
||||
self.service_uri = service_uri
|
||||
self.username = username
|
||||
self.password = password
|
||||
class Change(object):
|
||||
ADD, REMOVE = range(2)
|
||||
|
||||
def __init__(self, url, change, podcast=None):
|
||||
self.url = url
|
||||
self.change = change
|
||||
self.podcast = podcast
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
if self.change == self.ADD:
|
||||
return _('Add %s') % self.url
|
||||
else:
|
||||
return _('Remove %s') % self.podcast.title
|
||||
|
||||
|
||||
class Actions(object):
|
||||
NONE = 0
|
||||
|
||||
SYNC_PODCASTS, \
|
||||
UPLOAD_EPISODES, \
|
||||
UPDATE_DEVICE = (1<<x for x in range(3))
|
||||
|
||||
class MygPoClient(object):
|
||||
CACHE_FILE = 'mygpo.queue.json'
|
||||
FLUSH_TIMEOUT = 10
|
||||
FLUSH_RETRIES = 3
|
||||
|
||||
def __init__(self, config,
|
||||
on_rewrite_url=lambda old_url, new_url: None,
|
||||
on_add_remove_podcasts=lambda add_urls, remove_urls: None,
|
||||
on_send_full_subscriptions=lambda: None):
|
||||
self._cache = {'actions': Actions.NONE,
|
||||
'add_podcasts': [],
|
||||
'remove_podcasts': [],
|
||||
'episodes': []}
|
||||
|
||||
self._config = config
|
||||
self._client = None
|
||||
|
||||
# Callback for actions that need to be handled by the UI frontend
|
||||
self._on_rewrite_url = on_rewrite_url
|
||||
self._on_add_remove_podcasts = on_add_remove_podcasts
|
||||
self._on_send_full_subscriptions = on_send_full_subscriptions
|
||||
|
||||
# Initialize the _client attribute and register with config
|
||||
self.on_config_changed('mygpo_username')
|
||||
assert self._client is not None
|
||||
self._config.add_observer(self.on_config_changed)
|
||||
|
||||
# Initialize and load the local queue
|
||||
self._cache_file = os.path.join(gpodder.home, self.CACHE_FILE)
|
||||
try:
|
||||
self._cache = json.loads(open(self._cache_file).read())
|
||||
except Exception, e:
|
||||
log('Cannot read cache file: %s', str(e), sender=self)
|
||||
|
||||
self._worker_thread = None
|
||||
atexit.register(self._at_exit)
|
||||
|
||||
# Do the initial flush (in case any actions are queued)
|
||||
self.flush()
|
||||
|
||||
def can_access_webservice(self):
|
||||
return self._config.mygpo_enabled and self._config.mygpo_device_uid
|
||||
|
||||
def schedule_podcast_sync(self):
|
||||
log('Scheduling podcast list sync', sender=self)
|
||||
self.schedule(Actions.SYNC_PODCASTS)
|
||||
|
||||
def request_podcast_lists_in_cache(self):
|
||||
if 'add_podcasts' not in self._cache:
|
||||
self._cache['add_podcasts'] = []
|
||||
if 'remove_podcasts' not in self._cache:
|
||||
self._cache['remove_podcasts'] = []
|
||||
|
||||
def on_subscribe(self, urls):
|
||||
self.request_podcast_lists_in_cache()
|
||||
self._cache['add_podcasts'].extend(urls)
|
||||
for url in urls:
|
||||
if url in self._cache['remove_podcasts']:
|
||||
self._cache['remove_podcasts'].remove(url)
|
||||
self.schedule(Actions.SYNC_PODCASTS)
|
||||
self.flush()
|
||||
|
||||
def on_unsubscribe(self, urls):
|
||||
self.request_podcast_lists_in_cache()
|
||||
self._cache['remove_podcasts'].extend(urls)
|
||||
for url in urls:
|
||||
if url in self._cache['add_podcasts']:
|
||||
self._cache['add_podcasts'].remove(url)
|
||||
self.schedule(Actions.SYNC_PODCASTS)
|
||||
self.flush()
|
||||
|
||||
@property
|
||||
def actions(self):
|
||||
return self._cache.get('actions', Actions.NONE)
|
||||
|
||||
def _at_exit(self):
|
||||
self._worker_proc(forced=True)
|
||||
|
||||
def _worker_proc(self, forced=False):
|
||||
if not forced:
|
||||
log('Worker thread waiting for timeout', sender=self)
|
||||
time.sleep(self.FLUSH_TIMEOUT)
|
||||
|
||||
# Only work when enabled, UID set and allowed to work
|
||||
if self.can_access_webservice() and \
|
||||
(self._worker_thread is not None or forced):
|
||||
self._worker_thread = None
|
||||
log('Worker thread starting to work...', sender=self)
|
||||
for retry in range(self.FLUSH_RETRIES):
|
||||
if retry:
|
||||
log('Retrying flush queue...', sender=self)
|
||||
|
||||
# Update the device first, so it can be created if new
|
||||
if self.actions & Actions.UPDATE_DEVICE:
|
||||
self.update_device()
|
||||
|
||||
if self.actions & Actions.SYNC_PODCASTS:
|
||||
self.synchronize_subscriptions()
|
||||
|
||||
if self.actions & Actions.UPLOAD_EPISODES:
|
||||
# TODO: Upload episode actions
|
||||
pass
|
||||
|
||||
if not self.actions:
|
||||
# No more pending actions. Ready to quit.
|
||||
break
|
||||
|
||||
log('Flush completed (result: %d)', self.actions, sender=self)
|
||||
self._dump_cache_to_file()
|
||||
|
||||
def _dump_cache_to_file(self):
|
||||
try:
|
||||
fp = open(self._cache_file, 'w')
|
||||
fp.write(json.dumps(self._cache))
|
||||
fp.close()
|
||||
# FIXME: Atomic file write would be nice ;)
|
||||
except Exception, e:
|
||||
log('Cannot dump cache to file: %s', str(e), sender=self)
|
||||
|
||||
def flush(self):
|
||||
if not self.actions:
|
||||
return
|
||||
|
||||
if self._worker_thread is None:
|
||||
self._worker_thread = threading.Thread(target=self._worker_proc)
|
||||
self._worker_thread.setDaemon(True)
|
||||
self._worker_thread.start()
|
||||
else:
|
||||
log('Flush already queued', sender=self)
|
||||
|
||||
def schedule(self, action):
|
||||
if 'actions' not in self._cache:
|
||||
self._cache['actions'] = 0
|
||||
|
||||
self._cache['actions'] |= action
|
||||
self.flush()
|
||||
|
||||
def done(self, action):
|
||||
if 'actions' not in self._cache:
|
||||
self._cache['actions'] = 0
|
||||
|
||||
if action == Actions.SYNC_PODCASTS:
|
||||
self._cache['add_podcasts'] = []
|
||||
self._cache['remove_podcasts'] = []
|
||||
|
||||
self._cache['actions'] &= ~action
|
||||
|
||||
def on_config_changed(self, name=None, old_value=None, new_value=None):
|
||||
if name in ('mygpo_username', 'mygpo_password', 'mygpo_server'):
|
||||
self._client = api.MygPodderClient(self._config.mygpo_username,
|
||||
self._config.mygpo_password, self._config.mygpo_server)
|
||||
log('Reloading settings.', sender=self)
|
||||
elif name.startswith('mygpo_device_'):
|
||||
self.schedule(Actions.UPDATE_DEVICE)
|
||||
if name == 'mygpo_device_uid':
|
||||
# Reset everything because we have a new device ID
|
||||
self._on_send_full_subscriptions()
|
||||
self._cache['podcasts_since'] = 0
|
||||
|
||||
def synchronize_subscriptions(self):
|
||||
try:
|
||||
device_id = self._config.mygpo_device_uid
|
||||
since = self._cache.get('podcasts_since', 0)
|
||||
|
||||
# Step 1: Pull updates from the server and notify the frontend
|
||||
result = self._client.pull_subscriptions(device_id, since)
|
||||
self._cache['podcasts_since'] = result.since
|
||||
if result.add or result.remove:
|
||||
log('Changes from server: add %d, remove %d', \
|
||||
len(result.add), \
|
||||
len(result.remove), \
|
||||
sender=self)
|
||||
self._on_add_remove_podcasts(result.add, result.remove)
|
||||
|
||||
# Step 2: Push updates to the server and rewrite URLs (if any)
|
||||
add = list(set(self._cache.get('add_podcasts', [])))
|
||||
remove = list(set(self._cache.get('remove_podcasts', [])))
|
||||
if add or remove:
|
||||
# Only do a push request if something has changed
|
||||
result = self._client.update_subscriptions(device_id, add, remove)
|
||||
self._cache['podcasts_since'] = result.since
|
||||
|
||||
for old_url, new_url in result.update_urls:
|
||||
if new_url:
|
||||
log('URL %s rewritten: %s', old_url, new_url, sender=self)
|
||||
self._on_rewrite_url(old_url, new_url)
|
||||
|
||||
self.done(Actions.SYNC_PODCASTS)
|
||||
return True
|
||||
except Exception, e:
|
||||
log('Cannot upload subscriptions: %s', str(e), sender=self, traceback=True)
|
||||
return False
|
||||
|
||||
def update_device(self):
|
||||
try:
|
||||
log('Uploading device settings...', sender=self)
|
||||
uid = self._config.mygpo_device_uid
|
||||
caption = self._config.mygpo_device_caption
|
||||
device_type = self._config.mygpo_device_type
|
||||
self._client.update_device_settings(uid, caption, device_type)
|
||||
log('Device settings uploaded.', sender=self)
|
||||
self.done(Actions.UPDATE_DEVICE)
|
||||
return True
|
||||
except Exception, e:
|
||||
log('Cannot update device %s: %s', uid, str(e), sender=self, traceback=True)
|
||||
return False
|
||||
|
||||
def open_website(self):
|
||||
webbrowser.open(self.service_uri, new=1)
|
||||
|
||||
def download_subscriptions(self):
|
||||
theurl = self.service_uri+"/getlist"
|
||||
args = {'username': self.username, 'password': self.password}
|
||||
args = '&'.join(('%s=%s' % a for a in args.items()))
|
||||
url = theurl + '?' + args
|
||||
opml_data = util.urlopen(url).read()
|
||||
return opml_data
|
||||
|
||||
def upload_subscriptions(self, filename):
|
||||
theurl = self.service_uri+'/upload'
|
||||
action = 'update-subscriptions'
|
||||
fields = {'username': self.username, 'password': self.password, 'action': 'update-subscriptions', 'protocol': '0'}
|
||||
opml_file = ('opml', 'subscriptions.opml', open(filename).read())
|
||||
|
||||
result = urllib2.urlopen(build_request(theurl, fields, [opml_file])).read()
|
||||
messages = []
|
||||
|
||||
success = False
|
||||
|
||||
if '@GOTOMYGPODDER' in result:
|
||||
self.open_website()
|
||||
messages.append(_('Please have a look at the website for more information.'))
|
||||
|
||||
if '@SUCCESS' in result:
|
||||
messages.append(_('Subscriptions uploaded.'))
|
||||
success = True
|
||||
elif '@AUTHFAIL' in result:
|
||||
messages.append(_('Authentication failed.'))
|
||||
elif '@PROTOERROR' in result:
|
||||
messages.append(_('Protocol error.'))
|
||||
else:
|
||||
messages.append(_('Unknown response.'))
|
||||
|
||||
return success, messages
|
||||
util.open_website('http://' + self._config.mygpo_server)
|
||||
|
||||
|
|
Loading…
Reference in New Issue