Re: [RFC]: Patch to include epilicious in epiphany-extensions



On Mon, Jul 17, 2006 at 21:37:08 +0200, Christian Persch wrote:
>Le dimanche 16 juillet 2006 à 23:19 +0100, Magnus Therning a écrit :
>> I've been looking into putting epilicious in epiphany-extensions (from
>> CVS). I haven't quite gotten so far as to trying it all out (building
>> stuff with jhbuild at the moment) but I thought that if I can get early
>> feedback on my attempt so far it would probably help a bit.
>> 
>> Anything obvious I've missed?
>> 
>
>Looks ok to me, except maybe this:
>
>epiliciousdir = $(EXTENSIONS_DIR)
>epiclicious_PYTHON = \
>                     libepilicious/pydelicious.py \
>[...]
>
>Why don't you place those files in the same source directory, and just
>install into the right place with
>
>epiliciousdir = $(EXTENSIONS_DIR)/libepilicious 
>
>?

No good reason not to do it :-)

Here's the full patch. Still untested :-(

diff -NuPr epiphany-extensions_orig/configure.ac epiphany-extensions/configure.ac
--- epiphany-extensions_orig/configure.ac	2006-07-12 22:16:38.000000000 +0100
+++ epiphany-extensions/configure.ac	2006-07-16 22:51:28.000000000 +0100
@@ -156,7 +156,7 @@
 USEFUL_EXTENSIONS="actions auto-reload auto-scroller certificates error-viewer extensions-manager-ui gestures java-console page-info push-scroller select-stylesheet sidebar smart-bookmarks tab-groups tab-states"
 DEFAULT_EXTENSIONS="actions auto-scroller certificates error-viewer extensions-manager-ui gestures java-console page-info push-scroller select-stylesheet sidebar smart-bookmarks tab-groups tab-states"
 
-PYTHON_ALL_EXTENSIONS="python-console sample-python favicon"
+PYTHON_ALL_EXTENSIONS="python-console sample-python epilicious favicon"
 PYTHON_USEFUL_EXTENSIONS="python-console favicon"
 PYTHON_DEFAULT_EXTENSIONS="python-console favicon"
 
@@ -342,6 +342,7 @@
 extensions/auto-scroller/Makefile
 extensions/certificates/Makefile
 extensions/dashboard/Makefile
+extensions/epilicious/Makefile
 extensions/gestures/Makefile
 extensions/error-viewer/Makefile
 extensions/error-viewer/mozilla/Makefile
diff -NuPr epiphany-extensions_orig/extensions/epilicious/BaseStore.py epiphany-extensions/extensions/epilicious/BaseStore.py
--- epiphany-extensions_orig/extensions/epilicious/BaseStore.py	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/BaseStore.py	2006-07-05 22:39:30.000000000 +0100
@@ -0,0 +1,49 @@
+# Copyright (C) 2005 by Magnus Therning
+
+# This program 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 2 of the License, or
+# (at your option) any later version.
+
+# This program 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+class BaseStore:
+    '''The base of all stores. Never to be instantiated.'''
+
+    def get_snapshot(self):
+        '''Calculates a snapshot of the store's bookmarks.
+
+        The format of the snapshot is::
+         { <url> : [ <description>, [tag, tag, ...]],
+           <url> : [ <description>, [<tags>*]],
+           ... }
+
+        @return: the snapshot
+        '''
+        pass
+
+
+    def url_delete(self, url):
+        '''Deletes a URL form the store.
+
+        @type url: string
+        @param url: URL to delete
+        '''
+        pass
+
+
+    def url_sync(self, url, desc, to_del, to_add):
+        '''Synchronises a URL's tags. The URL is added if it doesn't exist.
+
+        @param url: The URL
+        @param to_del: Set of tags to delete
+        @param to_add: Set of tags to add
+        '''
+        pass
diff -NuPr epiphany-extensions_orig/extensions/epilicious/DeliciousStore.py epiphany-extensions/extensions/epilicious/DeliciousStore.py
--- epiphany-extensions_orig/extensions/epilicious/DeliciousStore.py	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/DeliciousStore.py	2006-07-05 22:39:30.000000000 +0100
@@ -0,0 +1,96 @@
+# Copyright (C) 2005 by Magnus Therning
+
+# This program 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 2 of the License, or
+# (at your option) any later version.
+
+# This program 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from urllib2 import URLError
+from sets import Set
+import pydelicious as delicious
+import libepilicious
+from libepilicious.BaseStore import BaseStore
+
+
+class DeliciousStore(BaseStore):
+    '''The class representing the storage of Delicious bookmarks.'''
+
+    def __init__(self, user, pwd, space_repl):
+        '''Constructor.
+
+        @param user: Delicious username
+        @param pwd: Delicious password
+        @param space_repl: Character that replaces space
+        '''
+        self.__un = user
+        self.__pwd = pwd
+        self.__sr = space_repl
+        self.__d = delicious.apiNew(user, pwd)
+        self.__snap_utd = 0
+
+    def get_snapshot(self):
+        '''Calculates a snapshot of the del.icio.us bookmarks.
+
+        @note: L{BaseStore.get_snapshot} documents the format of the return
+        value.
+        '''
+        if self.__snap_utd:
+            return self.__snap
+        all = self.__d.posts_all()
+        res = {}
+        for p in all:
+            # Delicious prepends "dangerous" links
+            if p['href'][:33] == 'http://del.icio.us/doc/dangerous#':
+                url = p['href'][33:]
+            else:
+                url = p['href']
+            res[url] = [p['description'], \
+                    [t.replace(self.__sr, ' ') for t in p['tags'].split(' ')]]
+
+        self.__snap = res
+        self.__snap_utd = 1
+        return res
+
+    def url_delete(self, url):
+        '''Deletes a URL from the storage.
+
+        @param url: The URL to delete
+        '''
+        # Delicious prepends "dangerous" links
+        if url[:7] == 'file://':
+            url = 'http://del.icio.us/doc/dangerous#' + url
+        try:
+            self.__d.posts_delete(url)
+            self.__snap_utd = 0
+        except:
+            libepilicious.get_logger().exception('Failed to delete URL %s' % url)
+
+    def url_sync(self, url, desc, to_del, to_add):
+        '''Synchronises a URL's tags. The URL is added if it doesn't exist.
+
+        @param url: The URL
+        @param to_del: Set of tags to delete
+        @param to_add: Set of tags to add
+        '''
+        if to_del or to_add:
+            snap = self.get_snapshot()
+            if snap.has_key(url):
+                tags = (Set(snap[url][1]) | to_add) - to_del
+            else:
+                tags = to_add
+            tag_str = ' '.join([t.replace(' ', self.__sr) for t in tags])
+            try:
+                self.__d.posts_add(url, description=desc, \
+                        tags=tag_str, replace='yes')
+                self.__snap_utd = 0
+            except:
+                libepilicious.get_logger().exception('Failed to synchronise URL %s' % url)
diff -NuPr epiphany-extensions_orig/extensions/epilicious/epilicious.ephy-extension epiphany-extensions/extensions/epilicious/epilicious.ephy-extension
--- epiphany-extensions_orig/extensions/epilicious/epilicious.ephy-extension	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/epilicious.ephy-extension	2006-07-06 23:39:56.000000000 +0100
@@ -0,0 +1,9 @@
+[Epiphany Extension]
+Version=1
+Name=Epilicious
+Authors=Magnus Therning <magnus therning org>;
+URL=http://therning.org/magnus/epilicious
+
+[Loader]
+Type=python
+Module=epilicious
diff -NuPr epiphany-extensions_orig/extensions/epilicious/epilicious.py.in epiphany-extensions/extensions/epilicious/epilicious.py.in
--- epiphany-extensions_orig/extensions/epilicious/epilicious.py.in	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/epilicious.py.in	2006-07-16 23:01:10.000000000 +0100
@@ -0,0 +1,212 @@
+# Copyright (C) 2005 by Magnus Therning
+
+# This program 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 2 of the License, or
+# (at your option) any later version.
+
+# This program 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import gtk
+import gconf
+import gobject
+import sys, os, os.path
+
+sys.path.append('%EXTENSION_DIR%')
+
+import libepilicious
+libepilicious.LOCATION = '%EXTENSION_DIR%'
+
+# localization
+import gettext
+try:
+    t = gettext.translation('epilicious')
+    _ = t.ugettext
+except Exception, e:
+    _ = lambda x : x
+
+## Globals ##
+username = ''
+password = ''
+keyword = ''
+exclude = False
+space_repl = ''
+
+## Configuration ##
+_gconf_dir = '/apps/epiphany/extensions/epilicious'
+_gconf_un = '/apps/epiphany/extensions/epilicious/username'
+_gconf_pwd = '/apps/epiphany/extensions/epilicious/password'
+_gconf_kw = '/apps/epiphany/extensions/epilicious/keyword'
+_gconf_excl = '/apps/epiphany/extensions/epilicious/exclude'
+
+def _new_un(client, *args, **kwargs):
+    '''Callback to handle the username is modified in GConf.'''
+    global username
+    username = client.get_string(_gconf_un)
+
+def _new_pwd(client, *args, **kwargs):
+    '''Callback to handle the password is modified in GConf.'''
+    global password
+    password = client.get_string(_gconf_pwd)
+
+def _new_keyword(client, *args, **kwargs):
+    '''Callback to handle the keyword is modified in GConf.'''
+    global keyword
+    keyword = client.get_string(_gconf_kw)
+
+def _new_exclude(client, *args, **kwargs):
+    '''Callback to handle the exclude is modified in GConf.'''
+    global exclude
+    exclude = client.get_bool(_gconf_excl)
+
+def _gconf_register():
+    '''Sets up the GConf callbacks.'''
+    global keyword, exclude, space_repl
+
+    # hard coded for now
+    space_repl = '#'
+
+    client = gconf.client_get_default()
+    client.add_dir(_gconf_dir, gconf.CLIENT_PRELOAD_NONE)
+    client.notify_add(_gconf_un, _new_un)
+    client.notify_add(_gconf_pwd, _new_pwd)
+    client.notify_add(_gconf_kw, _new_keyword)
+    client.notify_add(_gconf_excl, _new_exclude)
+    _new_un(client)
+    _new_pwd(client)
+    _new_keyword(client)
+    _new_exclude(client)
+
+_gconf_register()
+
+## Synchronisation ##
+def _CB_Sync(action, window):
+    # Using gobject.idle_add() together with a generator is much easier than
+    # getting threading to work properly. It doesn't make the GUI very
+    # responsive during del.icio.us calls, but it's better than nothing.
+    libepilicious.get_logger().info('Starting sync')
+    sync_gen = _do_sync()
+    gobject.idle_add(sync_gen.next)
+    libepilicious.get_logger().info('Sync done')
+
+def _do_sync():
+    '''Perform the synchronisation.
+
+    This is the method called from the menu item added in epiphany. The
+    algorithm is as follows:
+
+    1. Read the base snapshot (L{libepilicious.get_old})
+    2. Create remote storage representation (L{libepilicious.DeliciousStore})
+    3. Create local storage representation (L{libepilicious.EpiphanyStore})
+    4. Remove URLs (L{libepilicous.remove_urls})
+    5. Add new URLs and synchronise tags (L{libepilicous.sync_tags_on_urls})
+    6. Save a local snapshot as a base for the next synchronisation
+       (L{libepilicious.save_snapshot})
+
+    Any errors that occur are logged.
+    '''
+    try:
+        from libepilicious import *
+        from libepilicious.progress import ProgressBar
+        from libepilicious.DeliciousStore import DeliciousStore
+        from libepilicious.EpiphanyStore import EpiphanyStore
+
+        pbar = ProgressBar()
+        pbar.show()
+        stepper = pbar.step()
+
+        remote_store = DeliciousStore(user = username, pwd = password, \
+                space_repl = space_repl)
+        local_store = EpiphanyStore(keyword = keyword, exclude = exclude)
+        stepper.next()
+        old = get_old()
+        yield True
+        stepper.next()
+        remote = remote_store.get_snapshot()
+        yield True
+        stepper.next()
+        local = local_store.get_snapshot()
+        yield True
+        stepper.next()
+
+        # Synchronize URLs
+        remove_urls(old = old,
+                remote = remote,
+                local = local,
+                rem_store = remote_store,
+                loc_store = local_store)
+        yield True
+        stepper.next()
+
+        purls = calculate_pertinent_urls(old = old,
+                remote = remote, local = local)
+        sync_tags_on_urls(purls = purls,
+                old = old,
+                remote = remote,
+                local = local,
+                rem_store = remote_store,
+                loc_store = local_store)
+        yield True
+        stepper.next()
+
+        # Save the current state for future sync
+        save_snapshot(local_store.get_snapshot())
+        yield True
+        stepper.next()
+        yield False
+    except StopIteration, e:
+        libepilicious.get_logger().exception('Too many calls to stepper.next()')
+    except:
+        libepilicious.get_logger().exception('Failed sync')
+
+
+## Epiphany integration ##
+
+_menu_ui = '''
+<ui>
+  <menubar name="menubar">
+    <menu name="BookmarksMenu" action="Bookmarks">
+      <separator />
+      <menuitem name="%s" action="EpiliciousSync" />
+      <separator />
+    </menu>
+  </menubar>
+</ui>
+''' % (_('Epilicious Synchronize'))
+
+_actions = [ \
+        ('EpiliciousSync', None, _('Epilicious Synchronize'),
+                None, None, _CB_Sync), \
+        ]
+
+# Epiphany extension interface
+def attach_window(window):
+    try:
+        ui_manager = window.get_ui_manager()
+        group = gtk.ActionGroup('My Menu')
+        group.add_actions(_actions, window)
+        ui_manager.insert_action_group(group, 0)
+        ui_id = ui_manager.add_ui_from_string(_menu_ui)
+
+        window._my_menu_data = (group, ui_id)
+    except Exception, e:
+        libepilicious.get_logger().exception('Failed attach')
+
+def detach_window(window):
+    try:
+        group, ui_id = window._my_menu_data
+        del window._my_menu_data
+
+        ui_manager = window.get_ui_manager()
+        ui_manager.remove_ui(ui_id)
+        ui_manager.remove_action_group(group)
+        ui_manager.ensure_update()
+    except Exception, e:
+        libepilicious.get_logger().exception('Failed detach')
diff -NuPr epiphany-extensions_orig/extensions/epilicious/epilicious.schemas.in epiphany-extensions/extensions/epilicious/epilicious.schemas.in
--- epiphany-extensions_orig/extensions/epilicious/epilicious.schemas.in	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/epilicious.schemas.in	2006-07-05 22:39:30.000000000 +0100
@@ -0,0 +1,60 @@
+<gconfschemafile>
+  <schemalist>
+
+    <schema>
+      <key>/schemas/apps/epiphany/extensions/epilicious/username</key>
+      <applyto>/apps/epiphany/extensions/epilicious/username</applyto>
+      <owner>epiphany</owner>
+      <type>string</type>
+      <default> </default>
+      <locale name="C">
+        <short>del.icio.us username</short>
+        <long>The del.icio.us username that epilicious will use for
+          synchronizing bookmarks.</long>
+      </locale>
+    </schema>
+
+    <schema>
+      <key>/schemas/apps/epiphany/extensions/epilicious/password</key>
+      <applyto>/apps/epiphany/extensions/epilicious/password</applyto>
+      <owner>epiphany</owner>
+      <type>string</type>
+      <default> </default>
+      <locale name="C">
+        <short>del.icio.us password</short>
+        <long>The del.icio.us password that epilicious will use for
+          synchronizing bookmarks.</long>
+      </locale>
+    </schema>
+
+    <schema>
+      <key>/schemas/apps/epiphany/extensions/epilicious/keyword</key>
+      <applyto>/apps/epiphany/extensions/epilicious/keyword</applyto>
+      <owner>epiphany</owner>
+      <type>string</type>
+      <default>EpiliciousShare</default>
+      <locale name="C">
+        <short>Shared topic</short>
+        <long>Bookmarks with this topic will be synchronized to
+          del.icio.us.</long>
+      </locale>
+    </schema>
+
+    <schema>
+      <key>/schemas/apps/epiphany/extensions/epilicious/exclude</key>
+      <applyto>/apps/epiphany/extensions/epilicious/exclude</applyto>
+      <owner>epiphany</owner>
+      <type>bool</type>
+      <default>false</default>
+      <locale name="C">
+        <short>Exclude topic</short>
+        <long>Controls the function of the topic. If this is unset then all
+          bookmarks with the topic will be synched with del.icio.us. If this
+          is set then all bookmarks without the topic will be synched with
+          del.icio.us.</long>
+      </locale>
+    </schema>
+
+  </schemalist>
+</gconfschemafile>
+<!-- vim: set ft=xml: -->
diff -NuPr epiphany-extensions_orig/extensions/epilicious/EpiphanyStore.py epiphany-extensions/extensions/epilicious/EpiphanyStore.py
--- epiphany-extensions_orig/extensions/epilicious/EpiphanyStore.py	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/EpiphanyStore.py	2006-07-05 22:39:30.000000000 +0100
@@ -0,0 +1,130 @@
+# Copyright (C) 2005 by Magnus Therning
+
+# This program 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 2 of the License, or
+# (at your option) any later version.
+
+# This program 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from sets import Set
+import epiphany
+import libepilicious
+from libepilicious.BaseStore import BaseStore
+
+
+class EpiphanyStore(BaseStore):
+
+    def __init__(self, keyword = None, exclude = False):
+        # TODO: add a check for the priority of the keyword and set it to 0, at
+        # the moment it's not possible since there's no method for it
+        # (ephy_node_set_property() isn't in the Python API)
+        self.__kw = keyword
+        self.__excl = exclude
+
+        self.__bms = epiphany.ephy_shell_get_default().get_bookmarks()
+        if not self.__bms.find_keyword(self.__kw, False):
+            self.__bms.add_keyword(self.__kw)
+
+    def __get_all_shared_bookmarks(self):
+        # Return all shared bookmarks
+        # Two possibilities:
+        #  1. Take all bookmarks we have and remove the ones in __kw
+        #  2. All bookmarks are in __kw
+        key = self.__bms.find_keyword(self.__kw, False)
+        if self.__excl:
+            tmp = self.__bms.get_bookmarks().get_children()
+            res = []
+            for b in tmp:
+                if not self.__bms.has_keyword(key, b):
+                    res.append(b)
+            return res
+        else:
+            return key.get_children()
+
+    def __get_all_keywords(self):
+        # Return a list containing all interesting keywords. All special
+        # keywords (priority==1) and 'All' (id==0) are uninteresting.
+        kw_node = self.__bms.find_keyword(self.__kw, False)
+        return [kw for kw in self.__bms.get_keywords().get_children() \
+                if kw.get_id() != 0 and \
+                kw.get_property_int(epiphany.NODE_KEYWORD_PROP_PRIORITY) != 1 and \
+                kw != kw_node]
+
+    def get_snapshot(self):
+        '''Calculates a snapshot of the Epiphany bookmarks.
+
+        @note: L{BaseStore.get_snapshot} documents the format of the return
+        value.
+        '''
+        all_shared_bms = self.__get_all_shared_bookmarks()
+        all_keywords = self.__get_all_keywords()
+
+        res = {}
+        for bm in all_shared_bms:
+            res[bm.get_property_string(epiphany.NODE_BMK_PROP_LOCATION)] = \
+                    [bm.get_property_string(epiphany.NODE_BMK_PROP_TITLE)]
+            keywords = []
+            for key in all_keywords:
+                if self.__bms.has_keyword(key, bm):
+                    keywords.append(key.get_property_string(epiphany.NODE_KEYWORD_PROP_NAME))
+            res[bm.get_property_string(epiphany.NODE_BMK_PROP_LOCATION)].append(keywords)
+
+        return res
+
+    def url_delete(self, url):
+        '''Delete a bookmark.
+
+        :type url: string
+        :param url: The URL of the bookmark
+        '''
+        # This is brute force. It seems the easiest way of removing a bookmark
+        # is by removing every keyword there is from it.
+        bm = self.__bms.find_bookmark(url)
+        if bm:
+            for kw in self.__bms.get_keywords().get_children():
+                self.__bms.unset_keyword(kw, bm)
+
+    def url_sync(self, url, desc, to_del, to_add):
+        '''Synchronise a bookmark.
+
+        The bookmark with the given URL is created if needed and its keywords
+        are adjusted.
+
+        :type url: string
+        :param url: The URL of the bookmark
+        :type desc: string
+        :param desc: One-line description of the bookmark
+        :type to_del: list
+        :param to_del: The keywords (as strings) to delete from the bookmark
+        :type to_add: list
+        :param to_add: The keywords (as strings) to add to the bookmark
+        '''
+        bm = self.__bms.find_bookmark(url)
+        sharekw = self.__bms.find_keyword(self.__kw, False)
+        if not bm:
+            bm = self.__bms.add(desc, url)
+
+        if self.__excl:
+            if self.__bms.has_keyword(sharekw, bm):
+                self.__bms.unset_keyword(sharekw, bm)
+        else:
+            if not self.__bms.has_keyword(sharekw, bm):
+                self.__bms.set_keyword(sharekw, bm)
+
+        for k in to_del:
+            kw = self.__bms.find_keyword(k, False)
+            self.__bms.unset_keyword(kw, bm)
+
+        for k in to_add:
+            kw = self.__bms.find_keyword(k, False)
+            if not kw:
+                kw = self.__bms.add_keyword(k)
+            self.__bms.set_keyword(kw, bm)
diff -NuPr epiphany-extensions_orig/extensions/epilicious/__init__.py epiphany-extensions/extensions/epilicious/__init__.py
--- epiphany-extensions_orig/extensions/epilicious/__init__.py	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/__init__.py	2006-07-06 23:12:07.000000000 +0100
@@ -0,0 +1,156 @@
+# Copyright (C) 2005 by Magnus Therning
+
+# This program 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 2 of the License, or
+# (at your option) any later version.
+
+# This program 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from sets import Set
+import pickle
+import os, os.path
+import time
+
+# Nice to have for debugging
+logger = None
+def get_logger():
+    '''Create and return a file logger.
+
+    The file used for logging is ~/.epilicious.log.
+
+    @return: a file logger
+    '''
+    import logging
+    global logger
+
+    if logger:
+        return logger
+
+    logger = logging.getLogger('epilicious')
+    hdlr = logging.FileHandler(os.path.join(os.environ['HOME'], '.epilicious.log'))
+    formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+    hdlr.setFormatter(formatter)
+    logger.addHandler(hdlr)
+    logger.setLevel(logging.ERROR)
+    return logger
+
+_file_name = os.path.join(os.environ['HOME'], '.gnome2', \
+        'epiphany', 'epilicious_old')
+
+def get_old():
+    '''Read the base snapshot.
+
+    The base snapshot is read from ~/.gnome2/epiphany/epilicoius_old.
+
+    @return: the base snapshot (an empty dictionary if none exists)
+    '''
+    res = {}
+    try:
+        f = open(_file_name, 'r')
+        res = pickle.load(f)
+        f.close()
+    except IOError, e:
+        res = {}
+    return res
+
+def save_snapshot(snap):
+    '''Save a snapshot for future synchronisations.
+
+    The base snapshot is written to ~/.gnome2/epiphany/epilicoius_old.
+
+    @param snap: The snapshot
+    '''
+    f = open(_file_name, 'w+')
+    pickle.dump(snap, f)
+    f.close()
+
+def remove_urls(old, remote, local, rem_store, loc_store):
+    '''Remove URL from local and remote storage.
+
+    @param old: The base snapshot
+    @param remote: The remote snapshot
+    @param local: The local snapshot
+    @param rem_store: The remote storage (L{DeliciousStore})
+    @param loc_store: The local storage (L{EpiphanyStore})
+    '''
+    o = Set(old.keys())
+    r = Set(remote.keys())
+    l = Set(local.keys())
+    for url in o - l:
+        rem_store.url_delete(url)
+    for url in o - r:
+        loc_store.url_delete(url)
+
+def calculate_pertinent_urls(old, remote, local):
+    '''Calculate the interesting set of URLs.
+
+    The URLs returned are local URLs minus the URLs that were removed remotely
+    plus the URLs that were added remotely.
+
+    >>> o = {2:'',4:'',6:'',8:'',0:''}
+    >>> r = {2:'',4:'',6:'',8:'',1:''}
+    >>> l = {2:'',3:'',6:'',8:'',0:''}
+    >>> res = calculate_pertinent_urls(o, r, l); res.sort(); res
+    [1, 2, 3, 6, 8]
+
+    @param old: The base snapshot
+    @param remote: The remote snapshot
+    @param local: The local snapshot
+    @return: a list of URLs
+    '''
+    o = Set(old.keys())
+    r = Set(remote.keys())
+    l = Set(local.keys())
+
+    return list((l - (o - r)) | (r - o))
+
+def _get_tags(url, old, rem, loc):
+    '''Get the tags and the description for a URL in all three locations.
+
+    @param url: The URL to get tags for
+    @param old: The base snapshot
+    @param rem: The remote (Del.icio.us) snapshot
+    @param loc: The local (Epiphany) snapshot
+    @return: a tuple of description and lists of tags found in C{old}, C{rem},
+        and C{loc}.
+    '''
+    try:
+        otags = Set(old[url][1])
+        desc = old[url][0]
+    except:
+        otags = Set()
+    try:
+        rtags = Set(rem[url][1])
+        desc = rem[url][0]
+    except:
+        rtags = Set()
+    try:
+        ltags = Set(loc[url][1])
+        desc = loc[url][0]
+    except:
+        ltags = Set()
+    return desc, otags, rtags, ltags
+
+def sync_tags_on_urls(purls, old, remote, \
+        local, rem_store, loc_store):
+    '''Synchronise tags on URLs, and add URLs that don't exist.
+
+    @param purls: The list of URLs to synchronise
+    @param old: The base snapshot
+    @param remote: The remote snapshot
+    @param local: The local snapshot
+    @param rem_store: The remote storage (L{DeliciousStore})
+    @param loc_store: The local storage (L{EpiphanyStore})
+    '''
+    for url in purls:
+        desc, otags, rtags, ltags = _get_tags(url, old, remote, local)
+        rem_store.url_sync(url, desc, otags - ltags, ltags - otags)
+        loc_store.url_sync(url, desc, otags - rtags, rtags - otags)
diff -NuPr epiphany-extensions_orig/extensions/epilicious/Makefile.am epiphany-extensions/extensions/epilicious/Makefile.am
--- epiphany-extensions_orig/extensions/epilicious/Makefile.am	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/Makefile.am	2006-07-17 21:41:32.000000000 +0100
@@ -0,0 +1,43 @@
+epiliciousdir = $(EXTENSIONS_DIR)
+epiliciouslibdir = $(EXTENSIONS_DIR)/libepilicious
+epiclicious_PYTHON = \
+		     epilicious.py
+epiliciouslib_PYTHON = \
+		     pydelicious.py \
+		     progress.py \
+		     __init__.py \
+		     EpiphanyStore.py \
+		     DeliciousStore.py \
+		     BaseStore.py
+
+gladedir = $(pkgdatadir)/glade
+glade_DATA = \
+	     progress.glade
+
+schemadir = $(GCONF_SCHEMA_FILE_DIR)
+schema_DATA = epilicious.schemas
+
+extensioninidir = $(extensiondir)
+extensionini_DATA = epilicious.ephy-extension
+
+epilicious.py : epilicious.py.in
+	sed -e "s|%EXTENSION_DIR%|$(extensiondir)|" $< > $@
+
+# ??? @INTLTOOL_SCHEMAS_RULE@
+ EPIPHANY_EXTENSION_RULE@
+
+install-data-local: $(schema_DATA)
+if GCONF_SCHEMAS_INSTALL
+	if test -z "$(DESTDIR)" ; then \
+	for p in $^ ; do \
+		GCONF_CONFIG_SOURCE=$(GCONF_SCHEMA_CONFIG_SOURCE) \
+		$(GCONFTOOL) --makefile-install-rule $$p >&1 > /dev/null; \
+	done \
+	fi
+endif
+
+CLEANFILES  =
+DISTCLEANFILES = epilicious.py
+
+EXTRA_DIST = $(glade_DATA) $(schema_DATA) $(extensionini_DATA) \
+	epilicious.py.in epilicious.schemas.in
Binary files epiphany-extensions_orig/extensions/epilicious/.Makefile.am.swp and epiphany-extensions/extensions/epilicious/.Makefile.am.swp differ
diff -NuPr epiphany-extensions_orig/extensions/epilicious/progress.glade epiphany-extensions/extensions/epilicious/progress.glade
--- epiphany-extensions_orig/extensions/epilicious/progress.glade	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/progress.glade	2006-07-05 22:39:30.000000000 +0100
@@ -0,0 +1,399 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd";>
+
+<glade-interface>
+
+<widget class="GtkDialog" id="dlgProgress">
+  <property name="visible">True</property>
+  <property name="title" translatable="yes">Synchronizing...</property>
+  <property name="type">GTK_WINDOW_TOPLEVEL</property>
+  <property name="window_position">GTK_WIN_POS_NONE</property>
+  <property name="modal">False</property>
+  <property name="resizable">False</property>
+  <property name="destroy_with_parent">False</property>
+  <property name="decorated">True</property>
+  <property name="skip_taskbar_hint">False</property>
+  <property name="skip_pager_hint">False</property>
+  <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+  <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
+  <property name="focus_on_map">True</property>
+  <property name="urgency_hint">False</property>
+  <property name="has_separator">True</property>
+
+  <child internal-child="vbox">
+    <widget class="GtkVBox" id="dialog-vbox3">
+      <property name="visible">True</property>
+      <property name="homogeneous">False</property>
+      <property name="spacing">0</property>
+
+      <child internal-child="action_area">
+	<widget class="GtkHButtonBox" id="dialog-action_area3">
+	  <property name="visible">True</property>
+	  <property name="layout_style">GTK_BUTTONBOX_END</property>
+
+	  <child>
+	    <widget class="GtkButton" id="btnClose">
+	      <property name="visible">True</property>
+	      <property name="sensitive">False</property>
+	      <property name="can_default">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label">gtk-close</property>
+	      <property name="use_stock">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="response_id">-7</property>
+	      <signal name="clicked" handler="on_btnClose_clicked" last_modification_time="Mon, 03 Jul 2006 22:55:04 GMT"/>
+	    </widget>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">True</property>
+	  <property name="pack_type">GTK_PACK_END</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkVBox" id="vbox2">
+	  <property name="visible">True</property>
+	  <property name="homogeneous">False</property>
+	  <property name="spacing">0</property>
+
+	  <child>
+	    <widget class="GtkLabel" id="label2">
+	      <property name="visible">True</property>
+	      <property name="label" translatable="yes">&lt;b&gt;Synchronizing with del.icio.us&lt;/b&gt;</property>
+	      <property name="use_underline">False</property>
+	      <property name="use_markup">True</property>
+	      <property name="justify">GTK_JUSTIFY_LEFT</property>
+	      <property name="wrap">False</property>
+	      <property name="selectable">False</property>
+	      <property name="xalign">0.5</property>
+	      <property name="yalign">0.5</property>
+	      <property name="xpad">3</property>
+	      <property name="ypad">3</property>
+	      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+	      <property name="width_chars">-1</property>
+	      <property name="single_line_mode">False</property>
+	      <property name="angle">0</property>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkTable" id="table1">
+	      <property name="visible">True</property>
+	      <property name="n_rows">6</property>
+	      <property name="n_columns">2</property>
+	      <property name="homogeneous">False</property>
+	      <property name="row_spacing">0</property>
+	      <property name="column_spacing">0</property>
+
+	      <child>
+		<widget class="GtkLabel" id="label3">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Retrieving previous synch point</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">1</property>
+		  <property name="right_attach">2</property>
+		  <property name="top_attach">0</property>
+		  <property name="bottom_attach">1</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options"></property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkLabel" id="label4">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Retrieving bookmarks from del.icio.us</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">1</property>
+		  <property name="right_attach">2</property>
+		  <property name="top_attach">1</property>
+		  <property name="bottom_attach">2</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options"></property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkLabel" id="label5">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Retrieving bookmarks from epiphany</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">1</property>
+		  <property name="right_attach">2</property>
+		  <property name="top_attach">2</property>
+		  <property name="bottom_attach">3</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options"></property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkLabel" id="label6">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Removing deleted bookmarks</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">1</property>
+		  <property name="right_attach">2</property>
+		  <property name="top_attach">3</property>
+		  <property name="bottom_attach">4</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options"></property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkLabel" id="label7">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Adding new bookmarks and synching topics</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">1</property>
+		  <property name="right_attach">2</property>
+		  <property name="top_attach">4</property>
+		  <property name="bottom_attach">5</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options"></property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkLabel" id="label8">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Saving new synch point</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">1</property>
+		  <property name="right_attach">2</property>
+		  <property name="top_attach">5</property>
+		  <property name="bottom_attach">6</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options"></property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkImage" id="imgStep1">
+		  <property name="icon_size">4</property>
+		  <property name="icon_name">gtk-execute</property>
+		  <property name="xalign">0.5</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">0</property>
+		  <property name="right_attach">1</property>
+		  <property name="top_attach">0</property>
+		  <property name="bottom_attach">1</property>
+		  <property name="y_options">fill</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkImage" id="imgStep2">
+		  <property name="icon_size">4</property>
+		  <property name="icon_name">gtk-execute</property>
+		  <property name="xalign">0.5</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">0</property>
+		  <property name="right_attach">1</property>
+		  <property name="top_attach">1</property>
+		  <property name="bottom_attach">2</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options">fill</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkImage" id="imgStep3">
+		  <property name="icon_size">4</property>
+		  <property name="icon_name">gtk-execute</property>
+		  <property name="xalign">0.5</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">0</property>
+		  <property name="right_attach">1</property>
+		  <property name="top_attach">2</property>
+		  <property name="bottom_attach">3</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options">fill</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkImage" id="imgStep4">
+		  <property name="icon_size">4</property>
+		  <property name="icon_name">gtk-execute</property>
+		  <property name="xalign">0.5</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">0</property>
+		  <property name="right_attach">1</property>
+		  <property name="top_attach">3</property>
+		  <property name="bottom_attach">4</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options">fill</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkImage" id="imgStep5">
+		  <property name="icon_size">4</property>
+		  <property name="icon_name">gtk-execute</property>
+		  <property name="xalign">0.5</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">0</property>
+		  <property name="right_attach">1</property>
+		  <property name="top_attach">4</property>
+		  <property name="bottom_attach">5</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options">fill</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkImage" id="imgStep6">
+		  <property name="icon_size">1</property>
+		  <property name="icon_name">gtk-execute</property>
+		  <property name="xalign">0.5</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">3</property>
+		  <property name="ypad">3</property>
+		</widget>
+		<packing>
+		  <property name="left_attach">0</property>
+		  <property name="right_attach">1</property>
+		  <property name="top_attach">5</property>
+		  <property name="bottom_attach">6</property>
+		  <property name="x_options">fill</property>
+		  <property name="y_options">fill</property>
+		</packing>
+	      </child>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">True</property>
+	      <property name="fill">True</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <placeholder/>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">True</property>
+	  <property name="fill">True</property>
+	</packing>
+      </child>
+    </widget>
+  </child>
+</widget>
+
+</glade-interface>
diff -NuPr epiphany-extensions_orig/extensions/epilicious/progress.py epiphany-extensions/extensions/epilicious/progress.py
--- epiphany-extensions_orig/extensions/epilicious/progress.py	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/progress.py	2006-07-05 22:39:30.000000000 +0100
@@ -0,0 +1,53 @@
+# Copyright (C) 2006 by Magnus Therning
+
+# This program 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 2 of the License, or
+# (at your option) any later version.
+
+# This program 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import libepilicious
+import gtk, gtk.glade
+import os.path
+
+class ProgressBar:
+
+    def __init__(self):
+        self.gui = gtk.glade.XML(os.path.join(libepilicious.LOCATION, \
+                'libepilicious', 'progress.glade'))
+        self.gui.signal_autoconnect(self)
+
+        self.dlg = self.gui.get_widget('dlgProgress')
+
+    def show(self):
+        self.dlg.show()
+
+    def hide(self):
+        self.dlg.hide()
+
+    def step(self):
+        images = ['imgStep1', 'imgStep2', 'imgStep3', 'imgStep4', 'imgStep5', 'imgStep6',]
+        for i in images:
+            img = self.gui.get_widget(i)
+            img.show()
+            while gtk.events_pending():
+                gtk.main_iteration()
+            yield True
+            img.set_from_icon_name('gtk-apply', gtk.ICON_SIZE_MENU)
+        self.gui.get_widget('btnClose').set_sensitive(True)
+        while gtk.events_pending():
+            gtk.main_iteration()
+        yield True
+
+    ### signal handlers
+    def on_btnClose_clicked(self, widget):
+        self.hide()
+        del(self)
diff -NuPr epiphany-extensions_orig/extensions/epilicious/pydelicious-license.txt epiphany-extensions/extensions/epilicious/pydelicious-license.txt
--- epiphany-extensions_orig/extensions/epilicious/pydelicious-license.txt	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/pydelicious-license.txt	2006-07-05 22:39:30.000000000 +0100
@@ -0,0 +1,10 @@
+Copyright (c) 2006, Frank Timmermann
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+    * Neither the name Frank Timmermann, developer team of pydelicious nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff -NuPr epiphany-extensions_orig/extensions/epilicious/pydelicious.py epiphany-extensions/extensions/epilicious/pydelicious.py
--- epiphany-extensions_orig/extensions/epilicious/pydelicious.py	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/pydelicious.py	2006-07-06 23:11:30.000000000 +0100
@@ -0,0 +1,507 @@
+#!/usr/bin/env python
+# -*- coding: iso-8859-1 -*-
+"""Library to access del.icio.us via python.
+
+This module helps you to ...
+
+get()
+a = apiNew()
+a.tags.get
+a.tags.rename
+a.posts.get
+"""
+
+
+##
+# License: pydelicious is released under the bsd license.
+# See 'license.txt' for more informations.
+#
+
+##
+# TODO fuer pydelicious.py
+#  * die dokumentation aller docs muss noch geschehen
+#  * dokumentation
+#  * welche lizense benutze ich
+#  * lizense einbinden und auch via setup.py verteilen
+#  * readme auch schreiben und via setup.py verteilen
+#  * mehr tests
+#  * auch die funktion von setup.py testen?
+#  * auch auf anderen systemen testen (linux -> uni)
+#  * automatisch releases bauen lassen, richtig benennen und in das
+#    richtige verzeichnis verschieben.
+#  * was können die anderen librarys denn noch so? (ruby, java, perl, etc)
+#  * was wollen die, die es benutzen?
+#  * wofür könnte ich es benutzen?
+#  * entschlacken?
+#
+# realy?
+#  * date object von del.icio.us muss ich noch implementieren
+#
+# done!
+#  * stimmt das so? muss eher noch täg str2utf8 konvertieren
+#    >>> pydelicious.getrss(tag="täg")
+#    url: http://del.icio.us/rss/tag/täg
+#  * requester muss eine sekunde warten
+#  * __init__.py gibt die funktionen weiter
+#  * html parser funktioniert noch nicht, gar nicht
+#  * alte funktionen fehlen, get_posts_by_url, etc.
+#  * post funktion erstellen, die auch die fehlenden attribs addiert.
+#  * die api muss ich noch weiter machen
+#  * requester muss die 503er abfangen
+#  * rss parser muss auf viele möglichkeiten angepasst werden
+
+
+import re, md5, httplib
+import urllib, urllib2, time
+# import datetime,
+import StringIO
+
+# this is new                                                           #
+# this relays on an external library, will probably be kept             #
+import sys
+import os
+
+# !!! zweiter Versuch funzt nur auf linux rechner in der uni -
+# ersteres auch auf win32, doof.
+# [MT] this is how it works on Debian Sid!
+from elementtree.ElementTree import parse
+import feedparser
+
+# Taken from FeedParser.py
+# timeoutsocket allows feedparser to time out rather than hang forever on ultra-slow servers.
+# Python 2.3 now has this functionality available in the standard socket library, so under
+# 2.3 you don't need to install anything.  But you probably should anyway, because the socket
+# module is buggy and timeoutsocket is better.
+try:
+    import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py
+    timeoutsocket.setDefaultSocketTimeout(20)
+except ImportError:
+    import socket
+    if hasattr(socket, 'setdefaulttimeout'): socket.setdefaulttimeout(20)
+
+# some basic settings
+VERSION = '0.3.2'
+AUTHOR = 'Frank Timmermann'
+AUTHOR_EMAIL = 'regenkind_at_gmx_dot_de'
+PROJECT_URL = 'http://deliciouspython.python-hosting.com/'
+# das folgende ist mit python 2.2.3 nicht erlaubt. :(
+CONTACT = '%(URL)s or %(email)s'%dict(URL = PROJECT_URL, email = AUTHOR_EMAIL)
+DESCRIPTION = '''pydelicious.py allows you to access the web service of del.icio.us via it's API through python.'''
+LONG_DESCRIPTION = '''the goal is to design an easy to use and fully functional python interface to
+del.icio.us. '''
+DWS_HOSTNAME = 'http://del.icio.us/'
+DWS_HOSTNAME_RSS = 'http://del.icio.us/rss/'
+DWS_REALM = 'del.icio.us API'
+DWS_API = 'http://del.icio.us/api/'
+USER_AGENT = 'pydelicious.py/%(version)s %(contact)s' % dict(version = VERSION, contact = CONTACT)
+
+LAST_CALL = 0
+DEBUG = 0
+
+delicious_date_pattern  = re.compile("[1,2][0-9]{3}-[0-2][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]Z")
+
+
+# Helper Funktion
+def str2uni(s):
+    # type(in) str or unicode
+    # type(out) unicode
+    return ("".join([unichr(ord(i)) for i in s]))
+
+
+def str2utf8(s):
+    # type(in) str or unicode
+    # type(out) str
+    return ("".join([unichr(ord(i)).encode("utf-8") for i in s]))
+
+
+def str2quote(s):
+    return urllib.quote_plus("".join([unichr(ord(i)).encode("utf-8") for i in s]))
+
+
+def dict0(d):
+    # {'a':'a', 'b':'', 'c': 'c'} => {'a': 'a', 'c': 'c'}
+    dd = dict()
+    for i in d:
+            if d[i] != "": dd[i] = d[i]
+    return dd
+
+
+class Waiter:
+
+    def __init__(self, t = 0, sleeptime = 1):
+        self.t=t
+	self.sleeptime = sleeptime
+	self.waitcommand = 0
+	self.waited = 0
+
+    def wait(self, t=None):
+        self.waitcommand += 1
+        if t == None: t = time.time()
+        if DEBUG: print "Waiter:",t-self.t
+        if t-self.t<self.sleeptime:
+	    time.sleep(self.sleeptime-t+self.t)
+	    self.waited += 1
+	self.t = time.time()
+
+
+Waiter = Waiter()
+
+
+# Fehlerbehandlung
+comment='''Fehlerbehandlungszeug,
+kopiert aus delicious025.py, damit der reqester funktioniert. brauche ich das alles so?
+'''
+class DeliciousException(Exception):
+    '''Std. Error Function'''
+    pass
+
+
+class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler):
+
+    '''Handles HTTP Error, currently only 503
+    Behandelt die HTTP Fehler, behandelt nur 503 Fehler'''
+    def http_error_503(self, req, fp, code, msg, headers):
+        raise urllib2.HTTPError(req, code, throttled_message, headers, fp)
+
+
+# ! #
+class post(dict):
+
+    # a post object contains of this:
+    #  href
+    #  description
+    #  hash
+    #  dt
+    #  tags
+    #  extended
+    #  user
+    #  count
+    def __init__(self, href = "", description = "", hash = "", time = "", tag = "", extended = "", user = "", count = "",
+                 tags = "", url = "", dt = ""): # tags or tag?
+        self["href"] = href
+        if url != "": self["href"] = url
+        self["description"] = description
+        self["hash"] = hash
+        self["dt"] = dt
+        if time != "": self["dt"] = time
+        self["tags"] = tags
+        if tag != "":  self["tags"] = tag     # tag or tags? # !! tags
+        self["extended"] = extended
+        self["user"] = user
+        self["count"] = count
+
+    def __getattr__(self, name):
+        try: return self[name]
+        except: object.__getattribute__(self, name)
+
+
+class posts(list):
+
+    def __init__(self, *args):
+        for i in args: self.append(i)
+
+    def __getattr__(self, attr):
+        try: return [p[attr] for p in self]
+        except: object.__getattribute__(self, attr)
+
+
+# handle all the RSS stuff
+comment='''rss sollte nun wieder funktionieren, aber diese try, except scheisse ist so nicht schoen
+
+rss wird unterschiedlich zusammengesetzt. ich kann noch keinen einheitlichen zusammenhang
+zwischen daten (url, desc, ext, usw) und dem feed erkennen. warum können die das nicht einheitlich machen?
+'''
+def _readRSS(tag = "", popular = 0, user = "", url = ''):
+    '''handle a rss request to del.icio.us'''
+    tag = str2quote(tag)
+    user = str2quote(user)
+    if url != '':
+        # http://del.icio.us/rss/url/efbfb246d886393d48065551434dab54
+        url = DWS_HOSTNAME_RSS + '''url/%s'''%md5.new(url).hexdigest()
+    elif user != '' and tag != '':
+        url = DWS_HOSTNAME_RSS + '''%(user)s/%(tag)s'''%dict(user=user, tag=tag)
+    elif user != '' and tag == '':
+        # http://del.icio.us/rss/delpy
+        url = DWS_HOSTNAME_RSS + '''%s'''%user
+    elif popular == 0 and tag == '':
+        url = DWS_HOSTNAME_RSS
+    elif popular == 0 and tag != '':
+        # http://del.icio.us/rss/tag/apple
+        # http://del.icio.us/rss/tag/web2.0
+        url = DWS_HOSTNAME_RSS + "tag/%s"%tag
+    elif popular == 1 and tag == '':
+        url = DWS_HOSTNAME_RSS + '''popular/'''
+    elif popular == 1 and tag != '':
+        url = DWS_HOSTNAME_RSS + '''popular/%s'''%tag
+    rss = _request(url, useUrlAsIs = 1)
+    rss = feedparser.parse(rss)
+    # print rss
+#     for e in rss.entries: print e;print
+    l = posts()
+    for e in rss.entries:
+        if e.has_key("links") and e["links"]!=[] and e["links"][0].has_key("href"):
+            url = e["links"][0]["href"]
+        elif e.has_key("link"):
+            url = e["link"]
+        elif e.has_key("id"):
+            url = e["id"]
+        else:
+            url = ""
+        if e.has_key("title"):
+            description = e['title']
+        elif e.has_key("title_detail") and e["title_detail"].has_key("title"):
+            description = e["title_detail"]['value']
+        else:
+            description = ''
+        try: tags = e['categories'][0][1]
+        except:
+            try: tags = e["category"]
+            except: tags = ""
+        if e.has_key("modified"):
+            dt = e['modified']
+        else:
+            dt = ""
+        if e.has_key("summary"):
+            extended = e['summary']
+        elif e.has_key("summary_detail"):
+            e['summary_detail']["value"]
+        else:
+            extended = ""
+        if e.has_key("author"):
+            user = e['author']
+        else:
+            user = ""
+# time = dt ist weist auf ein problem hin
+# die benennung der variablen ist nicht einheitlich
+#  api senden und
+#  xml bekommen sind zwei verschiedene schuhe :(
+        l.append(post(url = url, description = description, tags = tags, dt = dt, extended = extended, user = user))
+    return l
+
+
+# HTML Parser, deprecated
+comment='''paring html gibt mehr infos als die parsing von html, aber
+ * eine aenderung des html's macht die funktion kaput
+ * fuer rss gibt es einen guten funktionierenden parser, muss ich denn trotzdem html wirklich parsen
+ * get_posts_by_url funktioniert nur mit dem parsen von html ==> stimmt das noch?
+'''
+def _readHTML(tar = "", popular = 0, user = ""):
+    pass
+    # construct url
+    # get data
+    # data 2 posts
+    # return posts
+
+
+# Requester
+comment='''stimmt der requester schon mit den vorgaben von del.icio.us ueberein, nein ...
+ * sollte ich die daten von del.icio.us auf nur text untersuchen?
+
+Done
+* eine sekunde pause zwischen jedem request klappt.
+* ich fange noch nicht den 503 code ab, klappt aber in der alten version
+   muss ich auch nicht, denn das läuft über die Exception DefaultErrorHandler.
+ '''
+def _request(url, params = '', useUrlAsIs = 0, user = '', passwd = '', ):
+    if DEBUG: httplib.HTTPConnection.debuglevel = 1
+    # Please wait AT LEAST ONE SECOND between queries, or you are likely to get automatically throttled.
+    # If you are releasing a library to access the API, you MUST do this.
+    # Everything is in the Caller, don't use this at home!
+    Waiter.wait()
+    # params come as a dict => dict0 => urlencode
+    params = urllib.urlencode(dict0(params))
+    authinfo = urllib2.HTTPBasicAuthHandler()
+    authinfo.add_password(DWS_REALM, DWS_HOSTNAME, user, passwd)
+    opener = urllib2.build_opener(authinfo, DefaultErrorHandler())
+    request = urllib2.Request(DWS_API + url + params)
+    if useUrlAsIs: request = urllib2.Request(url)
+    request.add_header('User-Agent', USER_AGENT)
+    if DEBUG: print "url:", request.get_full_url()
+    try:
+        o = opener.open(request)
+        return o.read()
+    except DefaultErrorHandler:
+        if DEBUG: return opener.open(request).read()
+        return ""
+
+# XML Parser
+comment='''ist vollständig,
+ * test fehlt nocht
+'''
+def _handleXML(data):
+    if DEBUG: print data
+    x = parse(StringIO.StringIO(data))
+    mode = x.getroot().tag
+    if mode == 'tags':
+        l = [dict(count = t.attrib["count"], tag = t.attrib["tag"]) for t in x.findall("tag")]
+    elif mode == "result":
+        if (x.getroot().attrib.has_key("code") and x.getroot().attrib["code"] == 'done') or x.getroot().text in ['done', 'ok']:
+            l = True
+        else :
+            l = False
+    elif mode == 'update':
+        l = x.getroot().attrib['time']
+    elif mode == 'dates':
+        l = [dict(count = t.attrib["count"], date = t.attrib["date"]) for t in x.findall("date")]
+    elif mode == 'bundles':
+        l = [dict(name = t.attrib["name"], tags = t.attrib["tags"]) for t in x.findall("bundle")]
+    elif mode == 'posts':
+        l = posts()
+        for t in x.findall("post"):
+            href, description, hash = '', '', ''
+            tag,time, extended      = '', '', ''
+            count = ''
+            if t.attrib.has_key("href"): href = t.attrib["href"]
+            if t.attrib.has_key("description"): description = t.attrib["description"]
+            if t.attrib.has_key("hash"): hash = t.attrib["hash"]
+            if t.attrib.has_key("tag"): tag = t.attrib["tag"]
+            if t.attrib.has_key("time"): time = t.attrib["time"]
+            if t.attrib.has_key("extended"): extended = t.attrib["extended"]
+            if t.attrib.has_key("count"): count = t.attrib["count"]
+            p = post(href=href, description=description,hash=hash,
+                     tag=tag, time=time, extended=extended,
+                     count=count)
+            l.append(p)
+    return l
+
+'''brauche ich das?'''
+def _validatePost(post): pass
+
+
+# del.icio.us api
+comment='''herzstueck
+
+Done
+ * was passiert mit nicht ascii daten in der verschickung als parameter?
+ * noch sehr unvollstaendig
+ * aufbau der api ist so, glaube ich, nicht mehr sinnvoll, vielleicht doch lieber nach dem alten schema, also
+        api.tags_get(...) anstatt api.tags.get(...)
+'''
+class _DeliciousAPI:
+# def _request(url, params = '', useUrlAsIs = 0, user = '', passwd = '', ):
+
+    def __init__(self, user, passwd):
+        self.user = user
+        self.passwd = passwd
+
+    def _main(self, url, params = ''):
+        x = _request(url = url, params = params, user = self.user, passwd = self.passwd)
+	self.xml = x
+        return _handleXML(x)
+
+    def tags_get(self):
+        return self._main(url = "tags/get?")
+
+    def tags_rename(self, old, new):
+        return self._main("tags/rename?", (dict(old = str2utf8(old),
+                                                new = str2utf8(new))))
+
+    def posts_update(self):
+        return self._main("posts/update")
+
+    def posts_dates(self, tag = ""):
+        return self._main("posts/dates?", (dict(tag = str2utf8(tag))))
+
+    def posts_get(self, tag="", dt="", url=""):
+        return self._main("posts/get?", (dict(tag = str2utf8(tag),
+                                              dt = str2utf8(dt),
+                                              url = str2utf8(url))))
+
+    def posts_recent(self, tag="", count=""):
+        return self._main("posts/recent?", (dict(tag = str2utf8(tag),
+                                                 count = str2utf8(count))))
+
+    def posts_all(self, tag=""):
+        return self._main("posts/all?", (dict(tag = str2utf8(tag))))
+
+    def posts_add(self, url, description="", extended="", tags="", dt="", replace="no"):
+        '''add an post to del.icio.us
+
+        url - the url of the page you like to add
+        description - a description of the page, often the title of the page
+        extended (opt) - an extended description, could be some kind of comment
+        tags - tags to sort your posts
+        dt (opt) - current date in format ...., if no date is given, the current
+                   date will be used
+        '''
+        return self._main("posts/add?", (dict(url = str2utf8(url),
+                                              description = str2utf8(description),
+                                              extended = str2utf8(extended),
+                                              tags = str2utf8(tags),
+                                              dt = str2utf8(dt),
+                                              replace = str2utf8(replace))))
+
+    def posts_delete(self, url):
+        return self._main("posts/delete?", (dict(url = str2utf8(url))))
+
+    def bundles_all(self):
+        return self._main(url = "tags/bundles/all")
+
+    def bundles_set(self, bundle, tags):
+        return self._main(url = "tags/bundles/set?",
+                          params = (dict(bundle = str2utf8(bundle),
+                                         tags = str2utf8(tags))))
+
+    def bundles_delete(self, bundle):
+        return self._main(url = "tags/bundles/delete?",
+                          params = (dict(bundle = str2utf8(bundle))))
+
+comment='''kick this, brauche das doch nicht, s.o,'''
+def apiNew(user, passwd):
+    return _DeliciousAPI(user=user, passwd= passwd)
+
+comment=''' holt die Daten via rss, entspricht _readRSS
+Done
+* basiert auch auf rss, html und api
+* braucht deshalb noch, bis es voll funzt. '''
+def getrss(tag = "", popular = 0, url = '', user = ""):
+    '''get posts from del.icio.us via parsing Rss or Html
+
+    tag (opt) sort by tag
+    popular (opt) look for the popular stuff
+    user (opt) get the posts by a user, this striks popular
+    url (opt) get the posts by url '''
+    return _readRSS(tag=tag, popular=popular, user=user, url=url)
+
+
+comment = '''api funktionen, damit die funktionen aus 0.2.5 bestehen bleiben'''
+def add(user, passwd, url, description, tags = "", extended = "", dt = "", replace="no"):
+    return apiNew(user=user, passwd=passwd).posts_add(url=url, description=description, extended=extended, tags=tags, dt=dt, replace=replace)
+
+
+def get(user, passwd, tag="", dt="",  count = 0):
+    posts = apiNew(user=user, passwd=passwd).posts_get(tag=tag,dt=dt)
+    if count != 0: posts = posts[0:count]
+    return posts
+
+
+def get_all(user, passwd, tag = ""):
+    return apiNew(user=user, passwd=passwd).posts_all(tag=tag)
+
+
+def delete(user, passwd, url):
+    return apiNew(user=user, passwd=passwd).posts_delete(url=url)
+
+
+def rename_tag(user, passwd, oldtag, newtag):
+    return apiNew(user=user, passwd=passwd).tags_rename(old=oldtag, new=newtag)
+
+
+def get_tags(user, passwd):
+    return apiNew(user=user, passwd=passwd).tags_get()
+
+
+def get_userposts(user):
+    return getrss(user = user)
+
+
+def get_tagposts(tag):
+    return getrss(tag = tag)
+
+
+def get_urlposts(url):
+    return getrss(url = url)
+
+
+def get_popular(tag = ""):
+    return getrss(tag = tag, popular = 1)
diff -NuPr epiphany-extensions_orig/extensions/epilicious/THOUGHTS epiphany-extensions/extensions/epilicious/THOUGHTS
--- epiphany-extensions_orig/extensions/epilicious/THOUGHTS	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/THOUGHTS	2006-07-06 23:12:26.000000000 +0100
@@ -0,0 +1,63 @@
+Either I mark the ones that shouldn't be synched (e.g. with keyword 'Skip')
+and all other bookmarks are synched.
+ Only one del.icio.us account can be used
+
+I mark all the bookmarks that should be synched.
+ More marking needed. Ability to have several del.icio.us accounts if each
+ account is associated with a specific keyword. When updating the del.icio.us
+ keywords have to be treated differently than other keywords.
+
+To update two sets when knowing the origin:
+
+  my_new = (my - (old - your)) | (your - old)
+  your_new = (your - (old - my)) | (my - old)
+
+ After this my_new == your_new.
+
+ Algortihm A:
+
+  URL with keywords are represented as
+   URL,kw1,kw2,...,kwn
+
+  old = Set of URL with keywords from last sync time
+  new = Set of URL with keywords retrieved from remote site
+  mine = Set of URL with keywords locally
+
+  to_add = new - old
+  to_remove = old - new
+
+  new_mine = (mine + to_add) - to_remove
+
+  1. Remove all bookmarks marked for share
+  2. Add bookmarks in new_mine
+  3. (Optional) remove empty tags
+
+  Do step 1-3 for remote site as well.
+
+The problem with descriptions.
+ Currently all bookmarks dealings are based on the URL. A change in description
+ on either side won't be reflected on the other side after a synch. Only if
+ there is a change in the tags will a change in description be propagated as
+ well, but then there is no way of knowing which site's description to use. At
+ the moment, if there's a change in description epilicious choses to keep the
+ local description (i.e. if locations A and B are synched with Delicious, and
+ A's and B's descriptions for a specific site differ then the name in Delicious
+ will bounce with every synch that changes tags for the site).
+
+Excluding instead of including bookmarks in synch
+ Two ways to do it:
+
+  1. All bookmarks in _share_kw_ are shared, except if they are in the
+     _block_kw_. If _share_kw_ isn't set, then all bookmarks are considered.
+  2. There is only one keyword, then there's a switch which controls whether
+     it is used to mark shared or non-shared bookmarks.
+
+ The former is the way proposed in the patch by Tom Coleman. The drawback here
+ is that setting the values must be explained to the user (at least one must
+ be set).
+
+ The latter is simpler, but it restricts the user somewhat. I don't think that
+ restriction is relevant though (what becomes impossible is to have something
+ marked both for inclusion and exclusion).
+
+vim: set ft=text:
diff -NuPr epiphany-extensions_orig/extensions/epilicious/TODO epiphany-extensions/extensions/epilicious/TODO
--- epiphany-extensions_orig/extensions/epilicious/TODO	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/TODO	2006-07-05 22:39:30.000000000 +0100
@@ -0,0 +1,44 @@
+- epilicious items
+  (added Fri May 19 17:14:45 2006, incomplete, priority medium)
+
+  - remove the cache in EpiphanyStore
+    (added Fri May 19 17:16:09 2006, completed on Sun May 21 23:57:55 2006, priority medium)
+
+  - use boolean to determine whether keyword is sharing or blocking
+    (added Fri May 19 17:19:01 2006, completed on Sun May 21 23:58:24 2006, priority medium)
+
+  - BUG: marking of bookmarks added to epiphany is wrong, they all get marked
+    with the keyword, that should depend on whether the keyword is used for
+    inclusion or exclusion
+    (added Mon Jun  5 09:08:16 2006, completed on Mon Jun  5 22:20:55 2006, priority medium)
+
+  - change priority on keyword so it doesn't show in bookmark menu
+    (added Fri May 19 17:17:40 2006, incomplete, priority medium)
+
+  - pop up dialogue if username/password is missing in gconf
+    (added Fri May 19 17:20:35 2006, incomplete, priority medium)
+
+  - configuration dialogue
+    (added Fri May 19 17:21:00 2006, incomplete, priority medium)
+
+  - ability to run in a mode where exceptions are caught and displayed
+    (added Fri May 19 17:21:32 2006, incomplete, priority medium)
+
+  - use epiphany's password store for password
+    (added Fri May 19 17:21:50 2006, incomplete, priority medium)
+
+  - deal better with descriptions of bookmarks
+    (added Fri May 19 17:22:11 2006, incomplete, priority medium)
+
+  - add support for scuttle
+    (added Fri May 19 17:22:41 2006, incomplete, priority medium)
+
+    - support for private bookmarks
+      (added Fri May 19 17:22:56 2006, incomplete, priority medium)
+
+    - changing URL to server
+      (added Fri May 19 17:23:10 2006, incomplete, priority medium)
+
+  - BUG: non-US characters are not treated right
+    (added Mon Jun  5 09:10:04 2006, incomplete, priority medium)
+
diff -NuPr epiphany-extensions_orig/extensions/epilicious/utils/delbackup epiphany-extensions/extensions/epilicious/utils/delbackup
--- epiphany-extensions_orig/extensions/epilicious/utils/delbackup	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/utils/delbackup	2006-07-06 23:11:49.000000000 +0100
@@ -0,0 +1,62 @@
+#! /usr/bin/env python
+
+
+import sys
+from optparse import OptionParser, make_option
+import cPickle as pickle
+from ProgressBar import ProgressBar
+try:
+    import pydelicious as delicious
+except:
+    print >> sys.stderr, "Make sure that 'libepilicious' is in $PYTHONPATH"
+    sys.exit(1)
+
+
+_OPTIONS = [ \
+        make_option('-u', '--username', \
+            help='del.icio.us username [REQUIRED]'), \
+        make_option('-p', '--password', \
+            help='del.icio.us password [REQUIRED]'), \
+        make_option('-o', '--filename', \
+            help='filename to save bookmarks to'), \
+        make_option('-l', '--noclear', action='store_true', default=False, \
+            help='do not clear del.icio.us after backing up'), \
+        ]
+
+
+def _clear_delicious(d):
+    dbms = d.posts_all()
+    pb = ProgressBar('Deleting from del.icio.us')
+    num_items = len(dbms)
+    i = 1
+    for b in dbms:
+        pb.update(i, num_items)
+        d.posts_delete(b['href'])
+        i += 1
+    pb.clear()
+
+
+def main():
+    parser = OptionParser(option_list=_OPTIONS)
+    options, args = parser.parse_args()
+    if not options.username or not options.password:
+        print >> sys.stderr, 'Both username and password are required!\n'
+        sys.exit(1)
+    outfile = sys.stdout
+    if options.filename:
+        try:
+            outfile = file(options.filename, 'w+')
+        except IOError, e:
+            print >> sys.stderr, 'Could not open file.'
+            sys.exit(1)
+
+    d = delicious.apiNew(options.username, options.password)
+    dbms = d.posts_all()
+    pickle.dump(dbms, outfile)
+
+    if not options.noclear:
+        _clear_delicious(d)
+
+
+if __name__ == '__main__':
+    main()
diff -NuPr epiphany-extensions_orig/extensions/epilicious/utils/delrestore epiphany-extensions/extensions/epilicious/utils/delrestore
--- epiphany-extensions_orig/extensions/epilicious/utils/delrestore	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/utils/delrestore	2006-07-05 22:39:30.000000000 +0100
@@ -0,0 +1,75 @@
+#! /usr/bin/env python
+
+from optparse import OptionParser, make_option
+import cPickle as pickle
+import sys
+from ProgressBar import ProgressBar
+try:
+    import pydelicious as delicious
+except:
+    print >> sys.stderr, "Make sure that 'libepilicious' is in $PYTHONPATH"
+    sys.exit(1)
+
+
+_OPTIONS = [ \
+        make_option('-u', '--username', \
+            help='del.icio.us username [REQUIRED]'), \
+        make_option('-p', '--password', \
+            help='del.icio.us password [REQUIRED]'), \
+        make_option('-f', '--filename', \
+            help='filename to read bookmarks from'), \
+        make_option('-l', '--noclear', action='store_true', default=False, \
+            help='do not clear del.icio.us before restoring'), \
+        ]
+
+
+def _clear_delicious(d):
+    dbms = d.posts_all()
+    pb = ProgressBar('Deleting from del.icio.us')
+    num_items = len(dbms)
+    i = 1
+    for b in dbms:
+        pb.update(i, num_items)
+        d.posts_delete(b['href'])
+        i += 1
+    pb.clear()
+
+
+def _restore_delicious(d, bms):
+    pb = ProgressBar('Restoring to del.icio.us')
+    num_items = len(bms)
+    i = 1
+    for b in bms:
+        pb.update(i, num_items)
+        d.posts_add(b['href'], \
+                description=b['description'], \
+                extended=b['extended'], \
+                tags=b['tags'], \
+                dt=b['dt'])
+        i += 1
+    pb.clear()
+
+
+def main():
+    parser = OptionParser(option_list=_OPTIONS)
+    options, args = parser.parse_args()
+    if not options.username or not options.password:
+        print >> sys.stderr, 'Both username and password are required!\n'
+        sys.exit(1)
+    infile = sys.stdout
+    if options.filename:
+        try:
+            infile = file(options.filename, 'r')
+        except IOError, e:
+            print >> sys.stderr, 'Could not open file.'
+            sys.exit(1)
+
+    lbms = pickle.load(infile)
+    d = delicious.apiNew(options.username, options.password)
+    if not options.noclear:
+        _clear_delicious(d)
+    _restore_delicious(d, lbms)
+
+
+if __name__ == '__main__':
+    main()
diff -NuPr epiphany-extensions_orig/extensions/epilicious/utils/ProgressBar.py epiphany-extensions/extensions/epilicious/utils/ProgressBar.py
--- epiphany-extensions_orig/extensions/epilicious/utils/ProgressBar.py	1970-01-01 01:00:00.000000000 +0100
+++ epiphany-extensions/extensions/epilicious/utils/ProgressBar.py	2006-07-06 23:10:13.000000000 +0100
@@ -0,0 +1,192 @@
+# Created by Edward Loper, version 1.2
+# Licensed under GPL (as well as released to the public domain)
+# Shamelessly copied from
+# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/475116
+
+import sys, re
+
+class _TerminalController:
+    """
+    A class that can be used to portably generate formatted output to
+    a terminal.
+
+    `_TerminalController` defines a set of instance variables whose
+    values are initialized to the control sequence necessary to
+    perform a given action.  These can be simply included in normal
+    output to the terminal:
+
+        >>> term = _TerminalController()
+        >>> print 'This is '+term.GREEN+'green'+term.NORMAL
+
+    Alternatively, the `render()` method can used, which replaces
+    '${action}' with the string required to perform 'action':
+
+        >>> term = _TerminalController()
+        >>> print term.render('This is ${GREEN}green${NORMAL}')
+
+    If the terminal doesn't support a given action, then the value of
+    the corresponding instance variable will be set to ''.  As a
+    result, the above code will still work on terminals that do not
+    support color, except that their output will not be colored.
+    Also, this means that you can test whether the terminal supports a
+    given action by simply testing the truth value of the
+    corresponding instance variable:
+
+        >>> term = _TerminalController()
+        >>> if term.CLEAR_SCREEN:
+        ...     print 'This terminal supports clearning the screen.'
+
+    Finally, if the width and height of the terminal are known, then
+    they will be stored in the `COLS` and `LINES` attributes.
+    """
+    # Cursor movement:
+    BOL = ''             #: Move the cursor to the beginning of the line
+    UP = ''              #: Move the cursor up one line
+    DOWN = ''            #: Move the cursor down one line
+    LEFT = ''            #: Move the cursor left one char
+    RIGHT = ''           #: Move the cursor right one char
+
+    # Deletion:
+    CLEAR_SCREEN = ''    #: Clear the screen and move to home position
+    CLEAR_EOL = ''       #: Clear to the end of the line.
+    CLEAR_BOL = ''       #: Clear to the beginning of the line.
+    CLEAR_EOS = ''       #: Clear to the end of the screen
+
+    # Output modes:
+    BOLD = ''            #: Turn on bold mode
+    BLINK = ''           #: Turn on blink mode
+    DIM = ''             #: Turn on half-bright mode
+    REVERSE = ''         #: Turn on reverse-video mode
+    NORMAL = ''          #: Turn off all modes
+
+    # Cursor display:
+    HIDE_CURSOR = ''     #: Make the cursor invisible
+    SHOW_CURSOR = ''     #: Make the cursor visible
+
+    # Terminal size:
+    COLS = None          #: Width of the terminal (None for unknown)
+    LINES = None         #: Height of the terminal (None for unknown)
+
+    # Foreground colors:
+    BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
+
+    # Background colors:
+    BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
+    BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
+
+    _STRING_CAPABILITIES = """
+    BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
+    CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
+    BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
+    HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
+    _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
+    _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
+
+    def __init__(self, term_stream=sys.stdout):
+        """
+        Create a `_TerminalController` and initialize its attributes
+        with appropriate values for the current terminal.
+        `term_stream` is the stream that will be used for terminal
+        output; if this stream is not a tty, then the terminal is
+        assumed to be a dumb terminal (i.e., have no capabilities).
+        """
+        # Curses isn't available on all platforms
+        try: import curses
+        except: return
+
+        # If the stream isn't a tty, then assume it has no capabilities.
+        if not term_stream.isatty(): return
+
+        # Check the terminal type.  If we fail, then assume that the
+        # terminal has no capabilities.
+        try: curses.setupterm()
+        except: return
+
+        # Look up numeric capabilities.
+        self.COLS = curses.tigetnum('cols')
+        self.LINES = curses.tigetnum('lines')
+
+        # Look up string capabilities.
+        for capability in self._STRING_CAPABILITIES:
+            (attrib, cap_name) = capability.split('=')
+            setattr(self, attrib, self._tigetstr(cap_name) or '')
+
+        # Colors
+        set_fg = self._tigetstr('setf')
+        if set_fg:
+            for i,color in zip(range(len(self._COLORS)), self._COLORS):
+                setattr(self, color, curses.tparm(set_fg, i) or '')
+        set_fg_ansi = self._tigetstr('setaf')
+        if set_fg_ansi:
+            for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
+                setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
+        set_bg = self._tigetstr('setb')
+        if set_bg:
+            for i,color in zip(range(len(self._COLORS)), self._COLORS):
+                setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
+        set_bg_ansi = self._tigetstr('setab')
+        if set_bg_ansi:
+            for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
+                setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
+
+    def _tigetstr(self, cap_name):
+        # String capabilities can include "delays" of the form "$<2>".
+        # For any modern terminal, we should be able to just ignore
+        # these, so strip them out.
+        import curses
+        cap = curses.tigetstr(cap_name) or ''
+        return re.sub(r'\$<\d+>[/*]?', '', cap)
+
+    def render(self, template):
+        """
+        Replace each $-substitutions in the given template string with
+        the corresponding terminal control string (if it's defined) or
+        '' (if it's not).
+        """
+        return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
+
+    def _render_sub(self, match):
+        s = match.group()
+        if s == '$$': return s
+        else: return getattr(self, s[2:-1])
+
+
+class ProgressBar:
+    """
+    A 2-line progress bar, which looks like::
+
+                                Header
+        20% [===========----------------------------------]
+
+    The progress bar adjusts to the width of the terminal.
+    """
+    HEADER = '${BOLD}%s${NORMAL}\n\n'
+    BAR = '${BOLD}[%s%s] %d/%d${NORMAL}\n'
+
+    def __init__(self, header, term_stream=sys.stdout):
+        self.term = _TerminalController(term_stream)
+        if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
+            raise ValueError("Terminal isn't capable enough -- you "
+                             "should use a simpler progress dispaly.")
+        self.width = self.term.COLS or 75
+        self.bar = self.term.render(self.BAR)
+        self.header = self.term.render(self.HEADER % header.center(self.width))
+        self.cleared = 1 #: true if we haven't drawn the bar yet.
+        self.update(0, 1)
+
+    def update(self, done, total):
+        if self.cleared:
+            sys.stdout.write(self.header)
+            self.cleared = 0
+        percent = float(done) / float(total)
+        n = int((self.width-10)*percent)
+        sys.stdout.write(
+            self.term.BOL + self.term.UP + self.term.CLEAR_EOL +
+            (self.bar % ('='*n, '-'*(self.width-10-n), done, total)))
+
+    def clear(self):
+        if not self.cleared:
+            sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL +
+                             self.term.UP + self.term.CLEAR_EOL +
+                             self.term.UP + self.term.CLEAR_EOL)
+            self.cleared = 1
diff -NuPr epiphany-extensions_orig/po/POTFILES.in epiphany-extensions/po/POTFILES.in
--- epiphany-extensions_orig/po/POTFILES.in	2006-04-24 11:27:02.000000000 +0100
+++ epiphany-extensions/po/POTFILES.in	2006-07-17 21:39:11.000000000 +0100
@@ -8,6 +8,9 @@
 extensions/actions/ephy-actions-extension-properties-dialog.c
 extensions/auto-reload/ephy-auto-reload-extension.c
 extensions/certificates/ephy-certificates-extension.c
+extensions/epilicious/epilicious.py.in
+extensions/epilicious/epilicious.schemas.in
+extensions/epilicious/progress.glade
 extensions/error-viewer/ephy-error-viewer-extension.c
 extensions/error-viewer/error-viewer.glade
 extensions/error-viewer/link-checker.c
@@ -15,9 +18,9 @@
 extensions/error-viewer/mozilla/ErrorViewerURICheckerObserver.cpp
 extensions/error-viewer/opensp/validate.cpp
 extensions/error-viewer/sgml-validator.c
+extensions/extensions-manager-ui/ephy-extensions-manager-ui-extension.c
 extensions/extensions-manager-ui/extensions-manager-ui.c
 extensions/extensions-manager-ui/extensions-manager-ui.glade
-extensions/extensions-manager-ui/ephy-extensions-manager-ui-extension.c
 extensions/greasemonkey/ephy-greasemonkey-extension.c
 extensions/java-console/ephy-java-console-extension.c
 extensions/page-info/ephy-page-info-extension.c

-- 
Magnus Therning                             (OpenPGP: 0xAB4DFBA4)
magnus therning org             Jabber: magnus therning gmail com
http://therning.org/magnus

Software is not manufactured, it is something you write and publish.
Keep Europe free from software patents, we do not want censorship
by patent law on written works.

Good powers of observation are frequently called "cynicism" by those
that don't have them.

Attachment: pgpJL8dtZWw9B.pgp
Description: PGP signature



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]