diff --git a/resources/images/news/DrawAndCook.png b/resources/images/news/DrawAndCook.png
new file mode 100644
index 0000000000..8b40b75344
Binary files /dev/null and b/resources/images/news/DrawAndCook.png differ
diff --git a/resources/recipes/DrawAndCook.recipe b/resources/recipes/DrawAndCook.recipe
index 1c080b85db..8db4f71014 100644
--- a/resources/recipes/DrawAndCook.recipe
+++ b/resources/recipes/DrawAndCook.recipe
@@ -1,8 +1,11 @@
from calibre.web.feeds.news import BasicNewsRecipe
+import re
class DrawAndCook(BasicNewsRecipe):
title = 'DrawAndCook'
__author__ = 'Starson17'
+ __version__ = 'v1.10'
+ __date__ = '13 March 2011'
description = 'Drawings of recipes!'
language = 'en'
publisher = 'Starson17'
@@ -13,6 +16,7 @@ class DrawAndCook(BasicNewsRecipe):
remove_javascript = True
remove_empty_feeds = True
cover_url = 'http://farm5.static.flickr.com/4043/4471139063_4dafced67f_o.jpg'
+ INDEX = 'http://www.theydrawandcook.com'
max_articles_per_feed = 30
remove_attributes = ['style', 'font']
@@ -34,20 +38,21 @@ class DrawAndCook(BasicNewsRecipe):
date = ''
current_articles = []
soup = self.index_to_soup(url)
- recipes = soup.findAll('div', attrs={'class': 'date-outer'})
+ featured_major_slider = soup.find(name='div', attrs={'id':'featured_major_slider'})
+ recipes = featured_major_slider.findAll('li', attrs={'data-id': re.compile(r'artwork_entry_\d+', re.DOTALL)})
for recipe in recipes:
- title = recipe.h3.a.string
- page_url = recipe.h3.a['href']
+ page_url = self.INDEX + recipe.a['href']
+ print 'page_url is: ', page_url
+ title = recipe.find('strong').string
+ print 'title is: ', title
current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':date})
return current_articles
-
- keep_only_tags = [dict(name='h3', attrs={'class':'post-title entry-title'})
- ,dict(name='div', attrs={'class':'post-body entry-content'})
+ keep_only_tags = [dict(name='h1', attrs={'id':'page_title'})
+ ,dict(name='section', attrs={'id':'artwork'})
]
- remove_tags = [dict(name='div', attrs={'class':['separator']})
- ,dict(name='div', attrs={'class':['post-share-buttons']})
+ remove_tags = [dict(name='article', attrs={'id':['recipe_actions', 'metadata']})
]
extra_css = '''
diff --git a/resources/recipes/rbc_ru.recipe b/resources/recipes/rbc_ru.recipe
index 4c377a334b..2495a195dc 100644
--- a/resources/recipes/rbc_ru.recipe
+++ b/resources/recipes/rbc_ru.recipe
@@ -1,24 +1,25 @@
+# -*- coding: utf-8 -*-
+
from calibre.web.feeds.news import BasicNewsRecipe
-class AdvancedUserRecipe1286819935(BasicNewsRecipe):
+class RBC_ru(BasicNewsRecipe):
title = u'RBC.ru'
__author__ = 'A. Chewi'
- oldest_article = 7
- max_articles_per_feed = 100
+ description = u'Российское информационное агентство «РосБизнесКонсалтинг» (РБК) - ленты новостей политики, экономики и финансов, аналитические материалы, комментарии и прогнозы, тематические статьи'
+ needs_subscription = False
+ cover_url = 'http://pics.rbc.ru/img/fp_v4/skin/img/logo.gif'
+ cover_margins = (80, 160, '#ffffff')
+ oldest_article = 10
+ max_articles_per_feed = 50
+ summary_length = 200
+ remove_empty_feeds = True
no_stylesheets = True
+ remove_javascript = True
use_embedded_content = False
conversion_options = {'linearize_tables' : True}
- remove_attributes = ['style']
language = 'ru'
timefmt = ' [%a, %d %b, %Y]'
- keep_only_tags = [dict(name='h2', attrs={}),
- dict(name='div', attrs={'class': 'box _ga1_on_'}),
- dict(name='h1', attrs={'class': 'news_section'}),
- dict(name='div', attrs={'class': 'news_body dotted_border_bottom'}),
- dict(name='table', attrs={'class': 'newsBody'}),
- dict(name='h2', attrs={'class': 'black'})]
-
feeds = [(u'Главные новости', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/rbc.ru/mainnews.rss'),
(u'Политика', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/rbc.ru/politics.rss'),
(u'Экономика', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/rbc.ru/economics.rss'),
@@ -26,6 +27,12 @@ class AdvancedUserRecipe1286819935(BasicNewsRecipe):
(u'Происшествия', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/rbc.ru/incidents.rss'),
(u'Финансовые новости Quote.rbc.ru', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/quote.ru/mainnews.rss')]
+ keep_only_tags = [dict(name='h2', attrs={}),
+ dict(name='div', attrs={'class': 'box _ga1_on_'}),
+ dict(name='h1', attrs={'class': 'news_section'}),
+ dict(name='div', attrs={'class': 'news_body dotted_border_bottom'}),
+ dict(name='table', attrs={'class': 'newsBody'}),
+ dict(name='h2', attrs={'class': 'black'})]
remove_tags = [dict(name='div', attrs={'class': "video-frame"}),
dict(name='div', attrs={'class': "photo-container videoContainer videoSWFLinks videoPreviewSlideContainer notes"}),
diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py
index 1918a36cc8..63b0b89a17 100644
--- a/src/calibre/devices/__init__.py
+++ b/src/calibre/devices/__init__.py
@@ -47,7 +47,7 @@ def get_connected_device():
for d in connected_devices:
try:
- d.open()
+ d.open(None)
except:
continue
else:
@@ -121,7 +121,7 @@ def debug(ioreg_to_tmp=False, buf=None):
out('Trying to open', dev.name, '...', end=' ')
try:
dev.reset(detected_device=det)
- dev.open()
+ dev.open(None)
out('OK')
except:
import traceback
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index 81d35cc159..0491f34d78 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -48,6 +48,7 @@ class ANDROID(USBMS):
0x04e8 : { 0x681d : [0x0222, 0x0223, 0x0224, 0x0400],
0x681c : [0x0222, 0x0224, 0x0400],
0x6640 : [0x0100],
+ 0x6877 : [0x0400],
},
# Acer
diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py
index 9f17ea22a4..da1ef55786 100644
--- a/src/calibre/devices/prs505/driver.py
+++ b/src/calibre/devices/prs505/driver.py
@@ -221,7 +221,8 @@ class PRS505(USBMS):
os.path.splitext(os.path.basename(p))[0],
book, p)
except:
- debug_print('FAILED to upload cover', p)
+ debug_print('FAILED to upload cover',
+ prefix, book.lpath)
else:
debug_print('PRS505: NOT uploading covers in sync_booklists')
diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py
index a19df07abf..578c28b894 100644
--- a/src/calibre/devices/usbms/driver.py
+++ b/src/calibre/devices/usbms/driver.py
@@ -10,7 +10,7 @@ driver. It is intended to be subclassed with the relevant parts implemented
for a particular device.
'''
-import os, re, time, json, uuid
+import os, re, time, json, uuid, functools
from itertools import cycle
from calibre.constants import numeric_version
@@ -372,15 +372,21 @@ class USBMS(CLI, Device):
@classmethod
def build_template_regexp(cls):
- def replfunc(match):
- if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
- return '(?P<' + match.group(1) + '>.+?)'
- elif match.group(1) in ['authors', 'author_sort']:
- return '(?P.+?)'
- else:
- return '(.+?)'
+ def replfunc(match, seen=None):
+ v = match.group(1)
+ if v in ['title', 'series', 'series_index', 'isbn']:
+ if v not in seen:
+ seen |= set([v])
+ return '(?P<' + v + '>.+?)'
+ elif v in ['authors', 'author_sort']:
+ if v not in seen:
+ seen |= set([v])
+ return '(?P.+?)'
+ return '(.+?)'
+ s = set()
+ f = functools.partial(replfunc, seen=s)
template = cls.save_template().rpartition('/')[2]
- return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)')
+ return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)')
@classmethod
def path_to_unicode(cls, path):
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 41b913455a..c5a8a82db1 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -92,8 +92,6 @@ class Metadata(object):
def is_null(self, field):
null_val = NULL_VALUES.get(field, None)
val = getattr(self, field, None)
- if val is False or val in (0, 0.0):
- return True
return not val or val == null_val
def __getattribute__(self, field):
@@ -129,6 +127,8 @@ class Metadata(object):
field, val = self._clean_identifier(field, val)
_data['identifiers'].update({field: val})
elif field == 'identifiers':
+ if not val:
+ val = copy.copy(NULL_VALUES.get('identifiers', None))
self.set_identifiers(val)
elif field in STANDARD_METADATA_FIELDS:
if val is None:
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py
index d918991aad..9b25545252 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.py
+++ b/src/calibre/gui2/dialogs/metadata_bulk.py
@@ -783,6 +783,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
books_to_refresh = self.db.set_custom(id, val, label=dfm['label'],
extra=extra, commit=False,
allow_case_change=True)
+ elif dest.startswith('#') and dest.endswith('_index'):
+ label = self.db.field_metadata[dest[:-6]]['label']
+ series = self.db.get_custom(id, label=label, index_is_id=True)
+ books_to_refresh = self.db.set_custom(id, series, label=label,
+ extra=val, commit=False,
+ allow_case_change=True)
else:
if dest == 'comments':
setter = self.db.set_comment
diff --git a/src/calibre/gui2/dialogs/saved_search_editor.py b/src/calibre/gui2/dialogs/saved_search_editor.py
index 1143a6f06a..c9f843109a 100644
--- a/src/calibre/gui2/dialogs/saved_search_editor.py
+++ b/src/calibre/gui2/dialogs/saved_search_editor.py
@@ -9,12 +9,13 @@ from PyQt4.QtGui import QDialog
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
from calibre.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key
+from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
- def __init__(self, window, initial_search=None):
- QDialog.__init__(self, window)
+ def __init__(self, parent, initial_search=None):
+ QDialog.__init__(self, parent)
Ui_SavedSearchEditor.__init__(self)
self.setupUi(self)
@@ -22,12 +23,13 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'),
self.current_index_changed)
self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search)
+ self.rename_button.clicked.connect(self.rename_search)
self.current_search_name = None
self.searches = {}
- self.searches_to_delete = []
for name in saved_searches().names():
self.searches[name] = saved_searches().lookup(name)
+ self.search_names = set([icu_lower(n) for n in saved_searches().names()])
self.populate_search_list()
if initial_search is not None and initial_search in self.searches:
@@ -42,6 +44,11 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
search_name = unicode(self.input_box.text()).strip()
if search_name == '':
return False
+ if icu_lower(search_name) in self.search_names:
+ error_dialog(self, _('Saved search already exists'),
+ _('The saved search %s already exists, perhaps with '
+ 'different case')%search_name).exec_()
+ return False
if search_name not in self.searches:
self.searches[search_name] = ''
self.populate_search_list()
@@ -57,10 +64,25 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
+'
', 'saved_search_editor_delete', self):
return
del self.searches[self.current_search_name]
- self.searches_to_delete.append(self.current_search_name)
self.current_search_name = None
self.search_name_box.removeItem(self.search_name_box.currentIndex())
+ def rename_search(self):
+ new_search_name = unicode(self.input_box.text()).strip()
+ if new_search_name == '':
+ return False
+ if icu_lower(new_search_name) in self.search_names:
+ error_dialog(self, _('Saved search already exists'),
+ _('The saved search %s already exists, perhaps with '
+ 'different case')%new_search_name).exec_()
+ return False
+ if self.current_search_name in self.searches:
+ self.searches[new_search_name] = self.searches[self.current_search_name]
+ del self.searches[self.current_search_name]
+ self.populate_search_list()
+ self.select_search(new_search_name)
+ return True
+
def select_search(self, name):
self.search_name_box.setCurrentIndex(self.search_name_box.findText(name))
@@ -78,7 +100,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def accept(self):
if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
- for name in self.searches_to_delete:
+ for name in saved_searches().names():
saved_searches().delete(name)
for name in self.searches:
saved_searches().add(name, self.searches[name])
diff --git a/src/calibre/gui2/dialogs/saved_search_editor.ui b/src/calibre/gui2/dialogs/saved_search_editor.ui
index 3ba37bdf10..99672b5b8e 100644
--- a/src/calibre/gui2/dialogs/saved_search_editor.ui
+++ b/src/calibre/gui2/dialogs/saved_search_editor.ui
@@ -134,6 +134,20 @@
+ -
+
+
+ Rename the current search to what is in the box
+
+
+ ...
+
+
+
+ :/images/edit-undo.png:/images/edit-undo.png
+
+
+
-
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index 15d5666978..206f2b97fb 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -67,6 +67,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if db.field_metadata[k]['is_category'] and
db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']])
choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers'])
+ choices |= set(['search'])
self.opt_categories_using_hierarchy.update_items_cache(choices)
r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList,
choices=sorted(list(choices), key=sort_key))
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index c4871880a4..12a29a469c 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -533,7 +533,9 @@ class TagsView(QTreeView): # {{{
self.setModel(self._model)
except:
# The DB must be gone. Set the model to None and hope that someone
- # will call set_database later. I don't know if this in fact works
+ # will call set_database later. I don't know if this in fact works.
+ # But perhaps a Bad Thing Happened, so print the exception
+ traceback.print_exc()
self._model = None
self.setModel(None)
# }}}
@@ -678,7 +680,8 @@ class TagTreeItem(object): # {{{
break
elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\
self.tag.state == TAG_SEARCH_STATES['mark_minusminus']:
- if self.tag.is_hierarchical and len(self.children):
+ if self.tag.is_searchable and self.tag.is_hierarchical \
+ and len(self.children):
break
else:
break
@@ -1258,19 +1261,22 @@ class TagsModel(QAbstractItemModel): # {{{
if t.type != TagTreeItem.CATEGORY])
if (comp,tag.category) in child_map:
node_parent = child_map[(comp,tag.category)]
- node_parent.tag.is_hierarchical = True
+ node_parent.tag.is_hierarchical = key != 'search'
else:
if i < len(components)-1:
t = copy.copy(tag)
t.original_name = '.'.join(components[:i+1])
- # This 'manufactured' intermediate node can
- # be searched, but cannot be edited.
- t.is_editable = False
+ if key != 'search':
+ # This 'manufactured' intermediate node can
+ # be searched, but cannot be edited.
+ t.is_editable = False
+ else:
+ t.is_searchable = t.is_editable = False
else:
t = tag
if not in_uc:
t.original_name = t.name
- t.is_hierarchical = True
+ t.is_hierarchical = key != 'search'
t.name = comp
self.beginInsertRows(category_index, 999999, 1)
node_parent = TagTreeItem(parent=node_parent, data=t,
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index 97ddaeb51a..be996063d5 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -123,14 +123,22 @@ REGEXP_MATCH = 2
def _match(query, value, matchkind):
if query.startswith('..'):
query = query[1:]
- prefix_match_ok = False
+ sq = query[1:]
+ internal_match_ok = True
else:
- prefix_match_ok = True
+ internal_match_ok = False
for t in value:
t = icu_lower(t)
try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished
if (matchkind == EQUALS_MATCH):
- if prefix_match_ok and query[0] == '.':
+ if internal_match_ok:
+ if query == t:
+ return True
+ comps = [c.strip() for c in t.split('.') if c.strip()]
+ for comp in comps:
+ if sq == comp:
+ return True
+ elif query[0] == '.':
if t.startswith(query[1:]):
ql = len(query) - 1
if (len(t) == ql) or (t[ql:ql+1] == '.'):
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index e46f9b818d..e70a746b15 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -56,7 +56,7 @@ class Tag(object):
self.is_hierarchical = False
self.is_editable = is_editable
self.is_searchable = is_searchable
- self.id_set = id_set
+ self.id_set = id_set if id_set is not None else set([])
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
if self.avg_rating > 0:
@@ -1691,10 +1691,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return books_to_refresh
def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
- set_authors=True, commit=True, force_cover=False,
- force_tags=False):
+ set_authors=True, commit=True, force_changes=False):
'''
Set metadata for the book `id` from the `Metadata` object `mi`
+
+ Setting force_changes=True will force set_metadata to update fields even
+ if mi contains empty values. In this case, 'None' is distinguished from
+ 'empty'. If mi.XXX is None, the XXX is not replaced, otherwise it is.
+ The tags, identifiers, and cover attributes are special cases. Tags and
+ identifiers cannot be set to None so then will always be replaced if
+ force_changes is true. You must ensure that mi contains the values you
+ want the book to have. Covers are always changed if a new cover is
+ provided, but are never deleted. Also note that force_changes has no
+ effect on setting title or authors.
'''
if callable(getattr(mi, 'to_book_metadata', None)):
# Handle code passing in a OPF object instead of a Metadata object
@@ -1708,12 +1717,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc()
else:
raise
- # force_changes has no role to play in setting title or author
+
+ def should_replace_field(attr):
+ return (force_changes and (mi.get(attr, None) is not None)) or \
+ not mi.is_null(attr)
+
path_changed = False
- if set_title and not mi.is_null('title'):
+ if set_title and mi.title:
self._set_title(id, mi.title)
path_changed = True
- if set_authors and not mi.is_null('authors'):
+ if set_authors:
+ if not mi.authors:
+ mi.authors = [_('Unknown')]
authors = []
for a in mi.authors:
authors += string_to_authors(a)
@@ -1722,17 +1737,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if path_changed:
self.set_path(id, index_is_id=True)
- if not mi.is_null('author_sort'):
+ if should_replace_field('author_sort'):
doit(self.set_author_sort, id, mi.author_sort, notify=False,
commit=False)
- if not mi.is_null('publisher'):
+ if should_replace_field('publisher'):
doit(self.set_publisher, id, mi.publisher, notify=False,
commit=False)
- if not mi.is_null('rating'):
+
+ # Setting rating to zero is acceptable.
+ if mi.rating is not None:
doit(self.set_rating, id, mi.rating, notify=False, commit=False)
- if not mi.is_null('series'):
+ if should_replace_field('series'):
doit(self.set_series, id, mi.series, notify=False, commit=False)
+ # force_changes has no effect on cover manipulation
if mi.cover_data[1] is not None:
doit(self.set_cover, id, mi.cover_data[1], commit=False)
elif mi.cover is not None:
@@ -1741,36 +1759,45 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
raw = f.read()
if raw:
doit(self.set_cover, id, raw, commit=False)
- elif force_cover:
- doit(self.remove_cover, id, notify=False, commit=False)
- if force_tags or not mi.is_null('tags'):
+ # if force_changes is true, tags are always replaced because the
+ # attribute cannot be set to None.
+ if should_replace_field('tags'):
doit(self.set_tags, id, mi.tags, notify=False, commit=False)
- if not mi.is_null('comments'):
+
+ if should_replace_field('comments'):
doit(self.set_comment, id, mi.comments, notify=False, commit=False)
- if not mi.is_null('series_index'):
+
+ # Setting series_index to zero is acceptable
+ if mi.series_index is not None:
doit(self.set_series_index, id, mi.series_index, notify=False,
commit=False)
- if not mi.is_null('pubdate'):
+ if should_replace_field('pubdate'):
doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False)
if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False,
commit=False)
+ # identifiers will always be replaced if force_changes is True
mi_idents = mi.get_identifiers()
- if mi_idents:
+ if force_changes:
+ self.set_identifiers(id, mi_idents, notify=False, commit=False)
+ elif mi_idents:
identifiers = self.get_identifiers(id, index_is_id=True)
for key, val in mi_idents.iteritems():
- if val and val.strip():
+ if val and val.strip(): # Don't delete an existing identifier
identifiers[icu_lower(key)] = val
self.set_identifiers(id, identifiers, notify=False, commit=False)
+
user_mi = mi.get_all_user_metadata(make_copy=False)
for key in user_mi.iterkeys():
if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
- doit(self.set_custom, id, val=mi.get(key), commit=False,
- extra=mi.get_extra(key), label=user_mi[key]['label'])
+ val = mi.get(key, None)
+ if force_changes or val is not None:
+ doit(self.set_custom, id, val=val, extra=mi.get_extra(key),
+ label=user_mi[key]['label'], commit=False)
if commit:
self.conn.commit()
self.notify('metadata', [id])