merge from trunk

This commit is contained in:
Lee 2011-03-15 13:05:29 +08:00
commit 5d5adabe87
15 changed files with 172 additions and 68 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

View File

@ -1,8 +1,11 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
import re
class DrawAndCook(BasicNewsRecipe): class DrawAndCook(BasicNewsRecipe):
title = 'DrawAndCook' title = 'DrawAndCook'
__author__ = 'Starson17' __author__ = 'Starson17'
__version__ = 'v1.10'
__date__ = '13 March 2011'
description = 'Drawings of recipes!' description = 'Drawings of recipes!'
language = 'en' language = 'en'
publisher = 'Starson17' publisher = 'Starson17'
@ -13,6 +16,7 @@ class DrawAndCook(BasicNewsRecipe):
remove_javascript = True remove_javascript = True
remove_empty_feeds = True remove_empty_feeds = True
cover_url = 'http://farm5.static.flickr.com/4043/4471139063_4dafced67f_o.jpg' cover_url = 'http://farm5.static.flickr.com/4043/4471139063_4dafced67f_o.jpg'
INDEX = 'http://www.theydrawandcook.com'
max_articles_per_feed = 30 max_articles_per_feed = 30
remove_attributes = ['style', 'font'] remove_attributes = ['style', 'font']
@ -34,20 +38,21 @@ class DrawAndCook(BasicNewsRecipe):
date = '' date = ''
current_articles = [] current_articles = []
soup = self.index_to_soup(url) 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: for recipe in recipes:
title = recipe.h3.a.string page_url = self.INDEX + recipe.a['href']
page_url = recipe.h3.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}) current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':date})
return current_articles return current_articles
keep_only_tags = [dict(name='h1', attrs={'id':'page_title'})
keep_only_tags = [dict(name='h3', attrs={'class':'post-title entry-title'}) ,dict(name='section', attrs={'id':'artwork'})
,dict(name='div', attrs={'class':'post-body entry-content'})
] ]
remove_tags = [dict(name='div', attrs={'class':['separator']}) remove_tags = [dict(name='article', attrs={'id':['recipe_actions', 'metadata']})
,dict(name='div', attrs={'class':['post-share-buttons']})
] ]
extra_css = ''' extra_css = '''

View File

@ -1,24 +1,25 @@
# -*- coding: utf-8 -*-
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1286819935(BasicNewsRecipe): class RBC_ru(BasicNewsRecipe):
title = u'RBC.ru' title = u'RBC.ru'
__author__ = 'A. Chewi' __author__ = 'A. Chewi'
oldest_article = 7 description = u'Российское информационное агентство «РосБизнесКонсалтинг» (РБК) - ленты новостей политики, экономики и финансов, аналитические материалы, комментарии и прогнозы, тематические статьи'
max_articles_per_feed = 100 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 no_stylesheets = True
remove_javascript = True
use_embedded_content = False use_embedded_content = False
conversion_options = {'linearize_tables' : True} conversion_options = {'linearize_tables' : True}
remove_attributes = ['style']
language = 'ru' language = 'ru'
timefmt = ' [%a, %d %b, %Y]' 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'), 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/politics.rss'),
(u'Экономика', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/rbc.ru/economics.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'Происшествия', 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')] (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"}), remove_tags = [dict(name='div', attrs={'class': "video-frame"}),
dict(name='div', attrs={'class': "photo-container videoContainer videoSWFLinks videoPreviewSlideContainer notes"}), dict(name='div', attrs={'class': "photo-container videoContainer videoSWFLinks videoPreviewSlideContainer notes"}),

View File

@ -47,7 +47,7 @@ def get_connected_device():
for d in connected_devices: for d in connected_devices:
try: try:
d.open() d.open(None)
except: except:
continue continue
else: else:
@ -121,7 +121,7 @@ def debug(ioreg_to_tmp=False, buf=None):
out('Trying to open', dev.name, '...', end=' ') out('Trying to open', dev.name, '...', end=' ')
try: try:
dev.reset(detected_device=det) dev.reset(detected_device=det)
dev.open() dev.open(None)
out('OK') out('OK')
except: except:
import traceback import traceback

View File

@ -48,6 +48,7 @@ class ANDROID(USBMS):
0x04e8 : { 0x681d : [0x0222, 0x0223, 0x0224, 0x0400], 0x04e8 : { 0x681d : [0x0222, 0x0223, 0x0224, 0x0400],
0x681c : [0x0222, 0x0224, 0x0400], 0x681c : [0x0222, 0x0224, 0x0400],
0x6640 : [0x0100], 0x6640 : [0x0100],
0x6877 : [0x0400],
}, },
# Acer # Acer

View File

@ -221,7 +221,8 @@ class PRS505(USBMS):
os.path.splitext(os.path.basename(p))[0], os.path.splitext(os.path.basename(p))[0],
book, p) book, p)
except: except:
debug_print('FAILED to upload cover', p) debug_print('FAILED to upload cover',
prefix, book.lpath)
else: else:
debug_print('PRS505: NOT uploading covers in sync_booklists') debug_print('PRS505: NOT uploading covers in sync_booklists')

View File

@ -10,7 +10,7 @@ driver. It is intended to be subclassed with the relevant parts implemented
for a particular device. for a particular device.
''' '''
import os, re, time, json, uuid import os, re, time, json, uuid, functools
from itertools import cycle from itertools import cycle
from calibre.constants import numeric_version from calibre.constants import numeric_version
@ -372,15 +372,21 @@ class USBMS(CLI, Device):
@classmethod @classmethod
def build_template_regexp(cls): def build_template_regexp(cls):
def replfunc(match): def replfunc(match, seen=None):
if match.group(1) in ['title', 'series', 'series_index', 'isbn']: v = match.group(1)
return '(?P<' + match.group(1) + '>.+?)' if v in ['title', 'series', 'series_index', 'isbn']:
elif match.group(1) in ['authors', 'author_sort']: if v not in seen:
return '(?P<author>.+?)' seen |= set([v])
else: return '(?P<' + v + '>.+?)'
return '(.+?)' elif v in ['authors', 'author_sort']:
if v not in seen:
seen |= set([v])
return '(?P<author>.+?)'
return '(.+?)'
s = set()
f = functools.partial(replfunc, seen=s)
template = cls.save_template().rpartition('/')[2] template = cls.save_template().rpartition('/')[2]
return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)') return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)')
@classmethod @classmethod
def path_to_unicode(cls, path): def path_to_unicode(cls, path):

View File

@ -92,8 +92,6 @@ class Metadata(object):
def is_null(self, field): def is_null(self, field):
null_val = NULL_VALUES.get(field, None) null_val = NULL_VALUES.get(field, None)
val = getattr(self, 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 return not val or val == null_val
def __getattribute__(self, field): def __getattribute__(self, field):
@ -129,6 +127,8 @@ class Metadata(object):
field, val = self._clean_identifier(field, val) field, val = self._clean_identifier(field, val)
_data['identifiers'].update({field: val}) _data['identifiers'].update({field: val})
elif field == 'identifiers': elif field == 'identifiers':
if not val:
val = copy.copy(NULL_VALUES.get('identifiers', None))
self.set_identifiers(val) self.set_identifiers(val)
elif field in STANDARD_METADATA_FIELDS: elif field in STANDARD_METADATA_FIELDS:
if val is None: if val is None:

View File

@ -783,6 +783,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
books_to_refresh = self.db.set_custom(id, val, label=dfm['label'], books_to_refresh = self.db.set_custom(id, val, label=dfm['label'],
extra=extra, commit=False, extra=extra, commit=False,
allow_case_change=True) 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: else:
if dest == 'comments': if dest == 'comments':
setter = self.db.set_comment setter = self.db.set_comment

View File

@ -9,12 +9,13 @@ from PyQt4.QtGui import QDialog
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def __init__(self, window, initial_search=None): def __init__(self, parent, initial_search=None):
QDialog.__init__(self, window) QDialog.__init__(self, parent)
Ui_SavedSearchEditor.__init__(self) Ui_SavedSearchEditor.__init__(self)
self.setupUi(self) self.setupUi(self)
@ -22,12 +23,13 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'), self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'),
self.current_index_changed) self.current_index_changed)
self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search) 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.current_search_name = None
self.searches = {} self.searches = {}
self.searches_to_delete = []
for name in saved_searches().names(): for name in saved_searches().names():
self.searches[name] = saved_searches().lookup(name) self.searches[name] = saved_searches().lookup(name)
self.search_names = set([icu_lower(n) for n in saved_searches().names()])
self.populate_search_list() self.populate_search_list()
if initial_search is not None and initial_search in self.searches: 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() search_name = unicode(self.input_box.text()).strip()
if search_name == '': if search_name == '':
return False 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: if search_name not in self.searches:
self.searches[search_name] = '' self.searches[search_name] = ''
self.populate_search_list() self.populate_search_list()
@ -57,10 +64,25 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
+'</p>', 'saved_search_editor_delete', self): +'</p>', 'saved_search_editor_delete', self):
return return
del self.searches[self.current_search_name] del self.searches[self.current_search_name]
self.searches_to_delete.append(self.current_search_name)
self.current_search_name = None self.current_search_name = None
self.search_name_box.removeItem(self.search_name_box.currentIndex()) 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): def select_search(self, name):
self.search_name_box.setCurrentIndex(self.search_name_box.findText(name)) self.search_name_box.setCurrentIndex(self.search_name_box.findText(name))
@ -78,7 +100,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def accept(self): def accept(self):
if self.current_search_name: if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText()) 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) saved_searches().delete(name)
for name in self.searches: for name in self.searches:
saved_searches().add(name, self.searches[name]) saved_searches().add(name, self.searches[name])

View File

@ -134,6 +134,20 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="6">
<widget class="QToolButton" name="rename_button">
<property name="toolTip">
<string>Rename the current search to what is in the box</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/edit-undo.png</normaloff>:/images/edit-undo.png</iconset>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">

View File

@ -67,6 +67,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if db.field_metadata[k]['is_category'] and if db.field_metadata[k]['is_category'] and
db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']]) db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']])
choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers']) choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers'])
choices |= set(['search'])
self.opt_categories_using_hierarchy.update_items_cache(choices) self.opt_categories_using_hierarchy.update_items_cache(choices)
r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList, r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList,
choices=sorted(list(choices), key=sort_key)) choices=sorted(list(choices), key=sort_key))

View File

@ -533,7 +533,9 @@ class TagsView(QTreeView): # {{{
self.setModel(self._model) self.setModel(self._model)
except: except:
# The DB must be gone. Set the model to None and hope that someone # 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._model = None
self.setModel(None) self.setModel(None)
# }}} # }}}
@ -678,7 +680,8 @@ class TagTreeItem(object): # {{{
break break
elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\ elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\
self.tag.state == TAG_SEARCH_STATES['mark_minusminus']: 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 break
else: else:
break break
@ -1258,19 +1261,22 @@ class TagsModel(QAbstractItemModel): # {{{
if t.type != TagTreeItem.CATEGORY]) if t.type != TagTreeItem.CATEGORY])
if (comp,tag.category) in child_map: if (comp,tag.category) in child_map:
node_parent = child_map[(comp,tag.category)] node_parent = child_map[(comp,tag.category)]
node_parent.tag.is_hierarchical = True node_parent.tag.is_hierarchical = key != 'search'
else: else:
if i < len(components)-1: if i < len(components)-1:
t = copy.copy(tag) t = copy.copy(tag)
t.original_name = '.'.join(components[:i+1]) t.original_name = '.'.join(components[:i+1])
# This 'manufactured' intermediate node can if key != 'search':
# be searched, but cannot be edited. # This 'manufactured' intermediate node can
t.is_editable = False # be searched, but cannot be edited.
t.is_editable = False
else:
t.is_searchable = t.is_editable = False
else: else:
t = tag t = tag
if not in_uc: if not in_uc:
t.original_name = t.name t.original_name = t.name
t.is_hierarchical = True t.is_hierarchical = key != 'search'
t.name = comp t.name = comp
self.beginInsertRows(category_index, 999999, 1) self.beginInsertRows(category_index, 999999, 1)
node_parent = TagTreeItem(parent=node_parent, data=t, node_parent = TagTreeItem(parent=node_parent, data=t,

View File

@ -123,14 +123,22 @@ REGEXP_MATCH = 2
def _match(query, value, matchkind): def _match(query, value, matchkind):
if query.startswith('..'): if query.startswith('..'):
query = query[1:] query = query[1:]
prefix_match_ok = False sq = query[1:]
internal_match_ok = True
else: else:
prefix_match_ok = True internal_match_ok = False
for t in value: for t in value:
t = icu_lower(t) t = icu_lower(t)
try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished
if (matchkind == EQUALS_MATCH): 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:]): if t.startswith(query[1:]):
ql = len(query) - 1 ql = len(query) - 1
if (len(t) == ql) or (t[ql:ql+1] == '.'): if (len(t) == ql) or (t[ql:ql+1] == '.'):

View File

@ -56,7 +56,7 @@ class Tag(object):
self.is_hierarchical = False self.is_hierarchical = False
self.is_editable = is_editable self.is_editable = is_editable
self.is_searchable = is_searchable 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.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort self.sort = sort
if self.avg_rating > 0: if self.avg_rating > 0:
@ -1691,10 +1691,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return books_to_refresh return books_to_refresh
def set_metadata(self, id, mi, ignore_errors=False, set_title=True, def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
set_authors=True, commit=True, force_cover=False, set_authors=True, commit=True, force_changes=False):
force_tags=False):
''' '''
Set metadata for the book `id` from the `Metadata` object `mi` 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)): if callable(getattr(mi, 'to_book_metadata', None)):
# Handle code passing in a OPF object instead of a Metadata object # Handle code passing in a OPF object instead of a Metadata object
@ -1708,12 +1717,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc() traceback.print_exc()
else: else:
raise 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 path_changed = False
if set_title and not mi.is_null('title'): if set_title and mi.title:
self._set_title(id, mi.title) self._set_title(id, mi.title)
path_changed = True path_changed = True
if set_authors and not mi.is_null('authors'): if set_authors:
if not mi.authors:
mi.authors = [_('Unknown')]
authors = [] authors = []
for a in mi.authors: for a in mi.authors:
authors += string_to_authors(a) authors += string_to_authors(a)
@ -1722,17 +1737,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if path_changed: if path_changed:
self.set_path(id, index_is_id=True) 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, doit(self.set_author_sort, id, mi.author_sort, notify=False,
commit=False) commit=False)
if not mi.is_null('publisher'): if should_replace_field('publisher'):
doit(self.set_publisher, id, mi.publisher, notify=False, doit(self.set_publisher, id, mi.publisher, notify=False,
commit=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) 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) 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: if mi.cover_data[1] is not None:
doit(self.set_cover, id, mi.cover_data[1], commit=False) doit(self.set_cover, id, mi.cover_data[1], commit=False)
elif mi.cover is not None: elif mi.cover is not None:
@ -1741,36 +1759,45 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
raw = f.read() raw = f.read()
if raw: if raw:
doit(self.set_cover, id, raw, commit=False) 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) 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) 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, doit(self.set_series_index, id, mi.series_index, notify=False,
commit=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) doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False)
if getattr(mi, 'timestamp', None) is not None: if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False, doit(self.set_timestamp, id, mi.timestamp, notify=False,
commit=False) commit=False)
# identifiers will always be replaced if force_changes is True
mi_idents = mi.get_identifiers() 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) identifiers = self.get_identifiers(id, index_is_id=True)
for key, val in mi_idents.iteritems(): 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 identifiers[icu_lower(key)] = val
self.set_identifiers(id, identifiers, notify=False, commit=False) self.set_identifiers(id, identifiers, notify=False, commit=False)
user_mi = mi.get_all_user_metadata(make_copy=False) user_mi = mi.get_all_user_metadata(make_copy=False)
for key in user_mi.iterkeys(): for key in user_mi.iterkeys():
if key in self.field_metadata and \ if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']: user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
doit(self.set_custom, id, val=mi.get(key), commit=False, val = mi.get(key, None)
extra=mi.get_extra(key), label=user_mi[key]['label']) 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: if commit:
self.conn.commit() self.conn.commit()
self.notify('metadata', [id]) self.notify('metadata', [id])