From 034e289ef397a76e5062864e700cc4ac7983cfa3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 24 Jun 2011 17:57:25 +0100 Subject: [PATCH 01/47] Author link first try --- src/calibre/ebooks/metadata/book/__init__.py | 2 ++ src/calibre/ebooks/metadata/book/base.py | 1 + src/calibre/ebooks/metadata/opf2.py | 7 +++-- src/calibre/gui2/book_details.py | 10 +++++++ .../gui2/dialogs/edit_authors_dialog.py | 22 +++++++++----- src/calibre/gui2/library/models.py | 8 ++++- src/calibre/gui2/tag_view.py | 4 ++- src/calibre/library/database2.py | 30 +++++++++++++------ src/calibre/library/schema_upgrades.py | 10 +++++++ src/calibre/library/sqlite.py | 6 ++-- 10 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py index fae858aabd..50e7b916ee 100644 --- a/src/calibre/ebooks/metadata/book/__init__.py +++ b/src/calibre/ebooks/metadata/book/__init__.py @@ -86,6 +86,8 @@ CALIBRE_METADATA_FIELDS = frozenset([ # a dict of user category names, where the value is a list of item names # from the book that are in that category 'user_categories', + # a dict of author to an associated hyperlink + 'author_link_map', ] ) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 382cb6c5a2..c28c65f7c9 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -34,6 +34,7 @@ NULL_VALUES = { 'authors' : [_('Unknown')], 'title' : _('Unknown'), 'user_categories' : {}, + 'author_link_map' : {}, 'language' : 'und' } diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 80fb84633b..c1cd2a739f 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -538,7 +538,8 @@ class OPF(object): # {{{ user_categories = MetadataField('user_categories', is_dc=False, formatter=json.loads, renderer=dump_user_categories) - + author_link_map = MetadataField('author_link_map', is_dc=False, + formatter=json.loads) def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True, populate_spine=True): @@ -1039,7 +1040,7 @@ class OPF(object): # {{{ for attr in ('title', 'authors', 'author_sort', 'title_sort', 'publisher', 'series', 'series_index', 'rating', 'isbn', 'tags', 'category', 'comments', - 'pubdate', 'user_categories'): + 'pubdate', 'user_categories', 'author_link_map'): val = getattr(mi, attr, None) if val is not None and val != [] and val != (None, None): setattr(self, attr, val) @@ -1336,6 +1337,8 @@ def metadata_to_opf(mi, as_string=True): for tag in mi.tags: factory(DC('subject'), tag) meta = lambda n, c: factory('meta', name='calibre:'+n, content=c) + if getattr(mi, 'author_link_map', None) is not None: + meta('author_link_map', json.dumps(mi.author_link_map)) if mi.series: meta('series', mi.series) if mi.series_index is not None: diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index f94e179166..ef21773ae4 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -121,6 +121,16 @@ def render_data(mi, use_roman_numbers=True, all_fields=False): if links: ans.append((field, u'
'+_('Changing the authors for several books can ' - 'take a while. Are you sure?') - +'
', 'tag_browser_drop_authors', self.tags_view): - return - elif len(ids) > 15: - if not confirm(''+_('Changing the metadata for that many books ' - 'can take a while. Are you sure?') - +'
', 'tag_browser_many_changes', self.tags_view): - return - - fm = self.db.metadata_for_field(key) - is_multiple = fm['is_multiple'] - val = on_node.tag.original_name - for id in ids: - mi = self.db.get_metadata(id, index_is_id=True) - - # Prepare to ignore the author, unless it is changed. Title is - # always ignored -- see the call to set_metadata - set_authors = False - - # Author_sort cannot change explicitly. Changing the author might - # change it. - mi.author_sort = None # Never will change by itself. - - if key == 'authors': - mi.authors = [val] - set_authors=True - elif fm['datatype'] == 'rating': - mi.set(key, len(val) * 2) - elif fm['is_custom'] and fm['datatype'] == 'series': - mi.set(key, val, extra=1.0) - elif is_multiple: - new_val = mi.get(key, []) - if val in new_val: - # Fortunately, only one field can change, so the continue - # won't break anything - continue - new_val.append(val) - mi.set(key, new_val) - else: - mi.set(key, val) - self.db.set_metadata(id, mi, set_title=False, - set_authors=set_authors, commit=False) - self.db.commit() - self.drag_drop_finished.emit(ids) - # }}} - - def set_search_restriction(self, s): - self.search_restriction = s - - def _get_category_nodes(self, sort): - ''' - Called by __init__. Do not directly call this method. - ''' - self.row_map = [] - self.categories = {} - - # Get the categories - if self.search_restriction: - try: - data = self.db.get_categories(sort=sort, - icon_map=self.category_icon_map, - ids=self.db.search('', return_matches=True)) - except: - data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) - self.tags_view.restriction_error.emit() - else: - data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) - - # Reconstruct the user categories, putting them into metadata - self.db.field_metadata.remove_dynamic_categories() - tb_cats = self.db.field_metadata - for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys(), - key=sort_key): - cat_name = '@' + user_cat # add the '@' to avoid name collision - while True: - try: - tb_cats.add_user_category(label=cat_name, name=user_cat) - dot = cat_name.rfind('.') - if dot < 0: - break - cat_name = cat_name[:dot] - except ValueError: - break - - for cat in sorted(self.db.prefs.get('grouped_search_terms', {}).keys(), - key=sort_key): - if (u'@' + cat) in data: - try: - tb_cats.add_user_category(label=u'@' + cat, name=cat) - except ValueError: - traceback.print_exc() - self.db.data.change_search_locations(self.db.field_metadata.get_search_terms()) - - if len(saved_searches().names()): - tb_cats.add_search_category(label='search', name=_('Searches')) - - if self.filter_categories_by: - for category in data.keys(): - data[category] = [t for t in data[category] - if lower(t.name).find(self.filter_categories_by) >= 0] - - tb_categories = self.db.field_metadata - for category in tb_categories: - if category in data: # The search category can come and go - self.row_map.append(category) - self.categories[category] = tb_categories[category]['name'] - return data - - def refresh(self, data=None): - ''' - Here to trap usages of refresh in the old architecture. Can eventually - be removed. - ''' - print ('TagsModel: refresh called!') - traceback.print_stack() - return False - - def create_node(self, *args, **kwargs): - node = TagTreeItem(*args, **kwargs) - self.node_map[id(node)] = node - return node - - def get_node(self, idx): - ans = self.node_map.get(idx.internalId(), self.root_item) - return ans - - def createIndex(self, row, column, internal_pointer=None): - idx = QAbstractItemModel.createIndex(self, row, column, - id(internal_pointer)) - return idx - def _create_node_tree(self, data, state_map): - ''' - Called by __init__. Do not directly call this method. - ''' sort_by = config['sort_tags_by'] if data is None: @@ -826,16 +503,338 @@ class TagsModel(QAbstractItemModel): # {{{ for category in self.category_nodes: process_one_node(category, state_map.get(category.py_name, {})) - def get_state(self): - state_map = {} - expanded_categories = [] - for row, category in enumerate(self.category_nodes): - if self.tags_view.isExpanded(self.index(row, 0, QModelIndex())): - expanded_categories.append(category.py_name) - states = [c.tag.state for c in category.child_tags()] - names = [(c.tag.name, c.tag.category) for c in category.child_tags()] - state_map[category.py_name] = dict(izip(names, states)) - return expanded_categories, state_map + # Drag'n Drop {{{ + def mimeTypes(self): + return ["application/calibre+from_library", + 'application/calibre+from_tag_browser'] + + def mimeData(self, indexes): + data = [] + for idx in indexes: + if idx.isValid(): + # get some useful serializable data + node = self.get_node(idx) + path = self.path_for_index(idx) + if node.type == TagTreeItem.CATEGORY: + d = (node.type, node.py_name, node.category_key) + else: + t = node.tag + p = node + while p.type != TagTreeItem.CATEGORY: + p = p.parent + d = (node.type, p.category_key, p.is_gst, t.original_name, + t.category, path) + data.append(d) + else: + data.append(None) + raw = bytearray(cPickle.dumps(data, -1)) + ans = QMimeData() + ans.setData('application/calibre+from_tag_browser', raw) + return ans + + def dropMimeData(self, md, action, row, column, parent): + fmts = set([unicode(x) for x in md.formats()]) + if not fmts.intersection(set(self.mimeTypes())): + return False + if "application/calibre+from_library" in fmts: + if action != Qt.CopyAction: + return False + return self.do_drop_from_library(md, action, row, column, parent) + elif 'application/calibre+from_tag_browser' in fmts: + return self.do_drop_from_tag_browser(md, action, row, column, parent) + + def do_drop_from_tag_browser(self, md, action, row, column, parent): + if not parent.isValid(): + return False + dest = self.get_node(parent) + if dest.type != TagTreeItem.CATEGORY: + return False + if not md.hasFormat('application/calibre+from_tag_browser'): + return False + data = str(md.data('application/calibre+from_tag_browser')) + src = cPickle.loads(data) + for s in src: + if s[0] != TagTreeItem.TAG: + return False + return self.move_or_copy_item_to_user_category(src, dest, action) + + def move_or_copy_item_to_user_category(self, src, dest, action): + ''' + src is a list of tuples representing items to copy. The tuple is + (type, containing category key, category key is global search term, + full name, category key, path to node) + The type must be TagTreeItem.TAG + dest is the TagTreeItem node to receive the items + action is Qt.CopyAction or Qt.MoveAction + ''' + def process_source_node(user_cats, src_parent, src_parent_is_gst, + is_uc, dest_key, node): + ''' + Copy/move an item and all its children to the destination + ''' + copied = False + src_name = node.tag.original_name + src_cat = node.tag.category + # delete the item if the source is a user category and action is move + if is_uc and not src_parent_is_gst and src_parent in user_cats and \ + action == Qt.MoveAction: + new_cat = [] + for tup in user_cats[src_parent]: + if src_name == tup[0] and src_cat == tup[1]: + continue + new_cat.append(list(tup)) + user_cats[src_parent] = new_cat + else: + copied = True + + # Now add the item to the destination user category + add_it = True + if not is_uc and src_cat == 'news': + src_cat = 'tags' + for tup in user_cats[dest_key]: + if src_name == tup[0] and src_cat == tup[1]: + add_it = False + if add_it: + user_cats[dest_key].append([src_name, src_cat, 0]) + + for c in node.children: + copied = process_source_node(user_cats, src_parent, src_parent_is_gst, + is_uc, dest_key, c) + return copied + + user_cats = self.db.prefs.get('user_categories', {}) + path = None + for s in src: + src_parent, src_parent_is_gst = s[1:3] + path = s[5] + + if src_parent.startswith('@'): + is_uc = True + src_parent = src_parent[1:] + else: + is_uc = False + dest_key = dest.category_key[1:] + + if dest_key not in user_cats: + continue + + node = self.index_for_path(path) + if node: + process_source_node(user_cats, src_parent, src_parent_is_gst, + is_uc, dest_key, + self.get_node(node)) + + self.db.prefs.set('user_categories', user_cats) + self.refresh_required.emit() + + return True + + def do_drop_from_library(self, md, action, row, column, parent): + idx = parent + if idx.isValid(): + node = self.data(idx, Qt.UserRole) + if node.type == TagTreeItem.TAG: + fm = self.db.metadata_for_field(node.tag.category) + if node.tag.category in \ + ('tags', 'series', 'authors', 'rating', 'publisher') or \ + (fm['is_custom'] and ( + fm['datatype'] in ['text', 'rating', 'series', + 'enumeration'] or + (fm['datatype'] == 'composite' and + fm['display'].get('make_category', False)))): + mime = 'application/calibre+from_library' + ids = list(map(int, str(md.data(mime)).split())) + self.handle_drop(node, ids) + return True + elif node.type == TagTreeItem.CATEGORY: + fm_dest = self.db.metadata_for_field(node.category_key) + if fm_dest['kind'] == 'user': + fm_src = self.db.metadata_for_field(md.column_name) + if md.column_name in ['authors', 'publisher', 'series'] or \ + (fm_src['is_custom'] and ( + (fm_src['datatype'] in ['series', 'text', 'enumeration'] and + not fm_src['is_multiple']))or + (fm_src['datatype'] == 'composite' and + fm_src['display'].get('make_category', False))): + mime = 'application/calibre+from_library' + ids = list(map(int, str(md.data(mime)).split())) + self.handle_user_category_drop(node, ids, md.column_name) + return True + return False + + def handle_user_category_drop(self, on_node, ids, column): + categories = self.db.prefs.get('user_categories', {}) + category = categories.get(on_node.category_key[1:], None) + if category is None: + return + fm_src = self.db.metadata_for_field(column) + for id in ids: + label = fm_src['label'] + if not fm_src['is_custom']: + if label == 'authors': + items = self.db.get_authors_with_ids() + items = [(i[0], i[1].replace('|', ',')) for i in items] + value = self.db.authors(id, index_is_id=True) + value = [v.replace('|', ',') for v in value.split(',')] + elif label == 'publisher': + items = self.db.get_publishers_with_ids() + value = self.db.publisher(id, index_is_id=True) + elif label == 'series': + items = self.db.get_series_with_ids() + value = self.db.series(id, index_is_id=True) + else: + items = self.db.get_custom_items_with_ids(label=label) + if fm_src['datatype'] != 'composite': + value = self.db.get_custom(id, label=label, index_is_id=True) + else: + value = self.db.get_property(id, loc=fm_src['rec_index'], + index_is_id=True) + if value is None: + return + if not isinstance(value, list): + value = [value] + for val in value: + for (v, c, id) in category: + if v == val and c == column: + break + else: + category.append([val, column, 0]) + categories[on_node.category_key[1:]] = category + self.db.prefs.set('user_categories', categories) + self.refresh_required.emit() + + def handle_drop(self, on_node, ids): + #print 'Dropped ids:', ids, on_node.tag + key = on_node.tag.category + if (key == 'authors' and len(ids) >= 5): + if not confirm(''+_('Changing the authors for several books can ' + 'take a while. Are you sure?') + +'
', 'tag_browser_drop_authors', self.parent()): + return + elif len(ids) > 15: + if not confirm(''+_('Changing the metadata for that many books ' + 'can take a while. Are you sure?') + +'
', 'tag_browser_many_changes', self.parent()): + return + + fm = self.db.metadata_for_field(key) + is_multiple = fm['is_multiple'] + val = on_node.tag.original_name + for id in ids: + mi = self.db.get_metadata(id, index_is_id=True) + + # Prepare to ignore the author, unless it is changed. Title is + # always ignored -- see the call to set_metadata + set_authors = False + + # Author_sort cannot change explicitly. Changing the author might + # change it. + mi.author_sort = None # Never will change by itself. + + if key == 'authors': + mi.authors = [val] + set_authors=True + elif fm['datatype'] == 'rating': + mi.set(key, len(val) * 2) + elif fm['is_custom'] and fm['datatype'] == 'series': + mi.set(key, val, extra=1.0) + elif is_multiple: + new_val = mi.get(key, []) + if val in new_val: + # Fortunately, only one field can change, so the continue + # won't break anything + continue + new_val.append(val) + mi.set(key, new_val) + else: + mi.set(key, val) + self.db.set_metadata(id, mi, set_title=False, + set_authors=set_authors, commit=False) + self.db.commit() + self.drag_drop_finished.emit(ids) + # }}} + + def _get_category_nodes(self, sort): + ''' + Called by __init__. Do not directly call this method. + ''' + self.row_map = [] + self.categories = {} + + # Get the categories + if self.search_restriction: + try: + data = self.db.get_categories(sort=sort, + icon_map=self.category_icon_map, + ids=self.db.search('', return_matches=True)) + except: + data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) + self.restriction_error.emit() + else: + data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) + + # Reconstruct the user categories, putting them into metadata + self.db.field_metadata.remove_dynamic_categories() + tb_cats = self.db.field_metadata + for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys(), + key=sort_key): + cat_name = '@' + user_cat # add the '@' to avoid name collision + while True: + try: + tb_cats.add_user_category(label=cat_name, name=user_cat) + dot = cat_name.rfind('.') + if dot < 0: + break + cat_name = cat_name[:dot] + except ValueError: + break + + for cat in sorted(self.db.prefs.get('grouped_search_terms', {}).keys(), + key=sort_key): + if (u'@' + cat) in data: + try: + tb_cats.add_user_category(label=u'@' + cat, name=cat) + except ValueError: + traceback.print_exc() + self.db.data.change_search_locations(self.db.field_metadata.get_search_terms()) + + if len(saved_searches().names()): + tb_cats.add_search_category(label='search', name=_('Searches')) + + if self.filter_categories_by: + for category in data.keys(): + data[category] = [t for t in data[category] + if lower(t.name).find(self.filter_categories_by) >= 0] + + tb_categories = self.db.field_metadata + for category in tb_categories: + if category in data: # The search category can come and go + self.row_map.append(category) + self.categories[category] = tb_categories[category]['name'] + return data + + def refresh(self, data=None): + ''' + Here to trap usages of refresh in the old architecture. Can eventually + be removed. + ''' + print ('TagsModel: refresh called!') + traceback.print_stack() + return False + + def create_node(self, *args, **kwargs): + node = TagTreeItem(*args, **kwargs) + self.node_map[id(node)] = node + return node + + def get_node(self, idx): + ans = self.node_map.get(idx.internalId(), self.root_item) + return ans + + def createIndex(self, row, column, internal_pointer=None): + idx = QAbstractItemModel.createIndex(self, row, column, + id(internal_pointer)) + return idx def index_for_category(self, name): for row, category in enumerate(self.category_nodes): @@ -853,20 +852,19 @@ class TagsModel(QAbstractItemModel): # {{{ def setData(self, index, value, role=Qt.EditRole): if not index.isValid(): - return NONE + return False # set up to reposition at the same item. We can do this except if # working with the last item and that item is deleted, in which case # we position at the parent label - path = index.model().path_for_index(index) val = unicode(value.toString()).strip() if not val: - error_dialog(self.tags_view, _('Item is blank'), + error_dialog(self.parent(), _('Item is blank'), _('An item cannot be set to nothing. Delete it instead.')).exec_() return False item = self.get_node(index) if item.type == TagTreeItem.CATEGORY and item.category_key.startswith('@'): if val.find('.') >= 0: - error_dialog(self.tags_view, _('Rename user category'), + error_dialog(self.parent(), _('Rename user category'), _('You cannot use periods in the name when ' 'renaming user categories'), show=True) return False @@ -886,7 +884,7 @@ class TagsModel(QAbstractItemModel): # {{{ if len(c) == len(ckey): if strcmp(ckey, nkey) != 0 and \ nkey_lower in user_cat_keys_lower: - error_dialog(self.tags_view, _('Rename user category'), + error_dialog(self.parent(), _('Rename user category'), _('The name %s is already used')%nkey, show=True) return False user_cats[nkey] = user_cats[ckey] @@ -895,16 +893,12 @@ class TagsModel(QAbstractItemModel): # {{{ rest = c[len(ckey):] if strcmp(ckey, nkey) != 0 and \ icu_lower(nkey + rest) in user_cat_keys_lower: - error_dialog(self.tags_view, _('Rename user category'), + error_dialog(self.parent(), _('Rename user category'), _('The name %s is already used')%(nkey+rest), show=True) return False user_cats[nkey + rest] = user_cats[ckey + rest] del user_cats[ckey + rest] - self.db.prefs.set('user_categories', user_cats) - self.tags_view.set_new_model() - # must not use 'self' below because the model has changed! - p = self.tags_view.model().find_category_node('@' + nkey) - self.tags_view.model().show_item_at_path(p) + self.user_categories_edited.emit(user_cats, nkey) # Does a refresh return True key = item.tag.category @@ -914,17 +908,17 @@ class TagsModel(QAbstractItemModel): # {{{ return False if key == 'authors': if val.find('&') >= 0: - error_dialog(self.tags_view, _('Invalid author name'), + error_dialog(self.parent(), _('Invalid author name'), _('Author names cannot contain & characters.')).exec_() return False if key == 'search': if val in saved_searches().names(): - error_dialog(self.tags_view, _('Duplicate search name'), + error_dialog(self.parent(), _('Duplicate search name'), _('The saved search name %s is already used.')%val).exec_() return False saved_searches().rename(unicode(item.data(role).toString()), val) item.tag.name = val - self.tags_view.search_item_renamed.emit() # Does a refresh + self.search_item_renamed.emit() # Does a refresh else: if key == 'series': self.db.rename_series(item.tag.id, val) @@ -937,18 +931,17 @@ class TagsModel(QAbstractItemModel): # {{{ elif self.db.field_metadata[key]['is_custom']: self.db.rename_custom_item(item.tag.id, val, label=self.db.field_metadata[key]['label']) - self.tags_view.tag_item_renamed.emit() + self.tag_item_renamed.emit() item.tag.name = val self.rename_item_in_all_user_categories(name, key, val) - self.tags_view.refresh_required.emit() - self.show_item_at_path(path) + self.refresh_required.emit() return True def rename_item_in_all_user_categories(self, item_name, item_category, new_name): ''' Search all user categories for items named item_name with category item_category and rename them to new_name. The caller must arrange to - redisplay the tree as appropriate (recount or set_new_model) + redisplay the tree as appropriate. ''' user_cats = self.db.prefs.get('user_categories', {}) for k in user_cats.keys(): @@ -965,7 +958,7 @@ class TagsModel(QAbstractItemModel): # {{{ ''' Search all user categories for items named item_name with category item_category and delete them. The caller must arrange to redisplay the - tree as appropriate (recount or set_new_model) + tree as appropriate. ''' user_cats = self.db.prefs.get('user_categories', {}) for cat in user_cats.keys(): @@ -1262,27 +1255,10 @@ class TagsModel(QAbstractItemModel): # {{{ return v return None - def show_item_at_path(self, path, box=False, - position=QTreeView.PositionAtCenter): - ''' - Scroll the browser and open categories to show the item referenced by - path. If possible, the item is placed in the center. If box=True, a - box is drawn around the item. - ''' - if path: - self.show_item_at_index(self.index_for_path(path), box=box, - position=position) - - def show_item_at_index(self, idx, box=False, - position=QTreeView.PositionAtCenter): - if idx.isValid(): - self.tags_view.setCurrentIndex(idx) - self.tags_view.scrollTo(idx, position) - self.tags_view.setCurrentIndex(idx) - if box: - tag_item = self.get_node(idx) - tag_item.boxed = True - self.dataChanged.emit(idx, idx) + def set_boxed(self, idx): + tag_item = self.get_node(idx) + tag_item.boxed = True + self.dataChanged.emit(idx, idx) def clear_boxed(self): ''' @@ -1310,8 +1286,5 @@ class TagsModel(QAbstractItemModel): # {{{ for i in xrange(self.rowCount(QModelIndex())): process_level(self.index(i, 0, QModelIndex())) - def get_filter_categories_by(self): - return self.filter_categories_by - # }}} diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index 0cafcd2b63..f8ae6e939f 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -7,11 +7,12 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal'), lambda match: '')]
+ remove_tags =[]
+ remove_tags.append(dict(name = 'div', attrs = {'class' : 'def element-date'}))
+ remove_tags.append(dict(name = 'div', attrs = {'class' : 'def silver'}))
+ remove_tags.append(dict(name = 'div', attrs = {'id' : 'content-main-column-right'}))
- remove_tags =[]
- remove_tags.append(dict(name = 'div', attrs = {'class' : 'def element-date'}))
- remove_tags.append(dict(name = 'div', attrs = {'class' : 'def silver'}))
- remove_tags.append(dict(name = 'div', attrs = {'id' : 'content-main-column-right'}))
-
-
- extra_css = '''
- .div-header {font-size: x-small; font-weight: bold}
- '''
+ extra_css = '''
+ .div-header {font-size: x-small; font-weight: bold}
+ '''
#h2 {font-size: x-large; font-weight: bold}
- def is_blocked(self, a):
- if a.findNextSibling('img') is None:
- return False
- else:
- return True
+ def is_blocked(self, a):
+ if a.findNextSibling('img') is None:
+ return False
+ else:
+ return True
- def find_last_issue(self):
- soup = self.index_to_soup('http://www.wprost.pl/archiwum/')
- a = 0
- if self.FIND_LAST_FULL_ISSUE:
- ico_blocked = soup.findAll('img', attrs={'src' : self.ICO_BLOCKED})
- a = ico_blocked[-1].findNext('a', attrs={'title' : re.compile('Zobacz spis tre.ci')})
- else:
- a = soup.find('a', attrs={'title' : re.compile('Zobacz spis tre.ci')})
- self.EDITION = a['href'].replace('/tygodnik/?I=', '')
- self.cover_url = a.img['src']
+ def find_last_issue(self):
+ soup = self.index_to_soup('http://www.wprost.pl/archiwum/')
+ a = 0
+ if self.FIND_LAST_FULL_ISSUE:
+ ico_blocked = soup.findAll('img', attrs={'src' : self.ICO_BLOCKED})
+ a = ico_blocked[-1].findNext('a', attrs={'title' : re.compile('Zobacz spis tre.ci')})
+ else:
+ a = soup.find('a', attrs={'title' : re.compile('Zobacz spis tre.ci')})
+ self.EDITION = a['href'].replace('/tygodnik/?I=', '')
+ self.cover_url = a.img['src']
- def parse_index(self):
- self.find_last_issue()
- soup = self.index_to_soup('http://www.wprost.pl/tygodnik/?I=' + self.EDITION)
- feeds = []
- for main_block in soup.findAll(attrs={'class':'main-block-s3 s3-head head-red3'}):
- articles = list(self.find_articles(main_block))
- if len(articles) > 0:
- section = self.tag_to_string(main_block)
- feeds.append((section, articles))
- return feeds
-
- def find_articles(self, main_block):
- for a in main_block.findAllNext( attrs={'style':['','padding-top: 15px;']}):
- if a.name in "td":
- break
- if self.EXCLUDE_LOCKED & self.is_blocked(a):
- continue
- yield {
- 'title' : self.tag_to_string(a),
- 'url' : 'http://www.wprost.pl' + a['href'],
- 'date' : '',
- 'description' : ''
- }
+ def parse_index(self):
+ self.find_last_issue()
+ soup = self.index_to_soup('http://www.wprost.pl/tygodnik/?I=' + self.EDITION)
+ feeds = []
+ for main_block in soup.findAll(attrs={'class':'main-block-s3 s3-head head-red3'}):
+ articles = list(self.find_articles(main_block))
+ if len(articles) > 0:
+ section = self.tag_to_string(main_block)
+ feeds.append((section, articles))
+ return feeds
+ def find_articles(self, main_block):
+ for a in main_block.findAllNext( attrs={'style':['','padding-top: 15px;']}):
+ if a.name in "td":
+ break
+ if self.EXCLUDE_LOCKED & self.is_blocked(a):
+ continue
+ yield {
+ 'title' : self.tag_to_string(a),
+ 'url' : 'http://www.wprost.pl' + a['href'],
+ 'date' : '',
+ 'description' : ''
+ }
From f0b58870f68f05f614ec9ce42436d4cd00010ae0 Mon Sep 17 00:00:00 2001
From: GRiker This setting should match your iTunes Preferences|Advanced setting. " + "Disabling will store copies of books transferred to iTunes in your calibre configuration directory. " + "Enabling indicates that iTunes is configured to store copies in your iTunes Media folder. ") ] EXTRA_CUSTOMIZATION_DEFAULT = [ True, True, + False, ] @@ -193,6 +199,7 @@ class ITUNES(DriverBase): # EXTRA_CUSTOMIZATION_MESSAGE indexes USE_SERIES_AS_CATEGORY = 0 CACHE_COVERS = 1 + USE_ITUNES_STORAGE = 2 OPEN_FEEDBACK_MESSAGE = _( 'Apple device detected, launching iTunes, please wait ...') @@ -281,6 +288,7 @@ class ITUNES(DriverBase): description_prefix = "added by calibre" ejected = False iTunes= None + iTunes_local_storage = None library_orphans = None log = Log() manual_sync_mode = False @@ -825,7 +833,7 @@ class ITUNES(DriverBase): # Confirm/create thumbs archive if not os.path.exists(self.cache_dir): if DEBUG: - self.log.info(" creating thumb cache '%s'" % self.cache_dir) + self.log.info(" creating thumb cache at '%s'" % self.cache_dir) os.makedirs(self.cache_dir) if not os.path.exists(self.archive_path): @@ -837,6 +845,17 @@ class ITUNES(DriverBase): if DEBUG: self.log.info(" existing thumb cache at '%s'" % self.archive_path) + # If enabled in config options, create/confirm an iTunes storage folder + if not self.settings().extra_customization[self.USE_ITUNES_STORAGE]: + self.iTunes_local_storage = os.path.join(config_dir,'iTunes storage') + if not os.path.exists(self.iTunes_local_storage): + if DEBUG: + self.log(" creating iTunes_local_storage at '%s'" % self.iTunes_local_storage) + os.mkdir(self.iTunes_local_storage) + else: + if DEBUG: + self.log(" existing iTunes_local_storage at '%s'" % self.iTunes_local_storage) + def remove_books_from_metadata(self, paths, booklists): ''' Remove books from the metadata list. This function must not communicate @@ -1281,50 +1300,27 @@ class ITUNES(DriverBase): if DEBUG: self.log.info(" ITUNES._add_new_copy()") - def _save_last_known_iTunes_storage(lb_added): - if isosx: - fp = lb_added.location().path - index = fp.rfind('/Books') + len('/Books') - last_known_iTunes_storage = fp[:index] - elif iswindows: - fp = lb_added.Location - index = fp.rfind('\Books') + len('\Books') - last_known_iTunes_storage = fp[:index] - dynamic['last_known_iTunes_storage'] = last_known_iTunes_storage - self.log.warning(" last_known_iTunes_storage: %s" % last_known_iTunes_storage) - db_added = None lb_added = None + # If using iTunes_local_storage, copy the file, redirect iTunes to use local copy + if not self.settings().extra_customization[self.USE_ITUNES_STORAGE]: + local_copy = os.path.join(self.iTunes_local_storage, str(metadata.uuid) + os.path.splitext(fpath)[1]) + shutil.copyfile(fpath,local_copy) + fpath = local_copy + if self.manual_sync_mode: ''' - This is the unsupported direct-connect mode. - In an attempt to avoid resetting the iTunes library Media folder, don't try to - add the book to iTunes if the last_known_iTunes_storage path is inaccessible. - This means that the path has to be set at least once, probably by using - 'Connect to iTunes' and doing a transfer. + Unsupported direct-connect mode. ''' self.log.warning(" unsupported direct connect mode") db_added = self._add_device_book(fpath, metadata) - last_known_iTunes_storage = dynamic.get('last_known_iTunes_storage', None) - if last_known_iTunes_storage is not None: - if os.path.exists(last_known_iTunes_storage): - if DEBUG: - self.log.warning(" iTunes storage online, adding to library") - lb_added = self._add_library_book(fpath, metadata) - else: - if DEBUG: - self.log.warning(" iTunes storage not online, can't add to library") - - if lb_added: - _save_last_known_iTunes_storage(lb_added) + lb_added = self._add_library_book(fpath, metadata) if not lb_added and DEBUG: self.log.warn(" failed to add '%s' to iTunes, iTunes Media folder inaccessible" % metadata.title) else: lb_added = self._add_library_book(fpath, metadata) - if lb_added: - _save_last_known_iTunes_storage(lb_added) - else: + if not lb_added: raise UserFeedback("iTunes Media folder inaccessible", details="Failed to add '%s' to iTunes" % metadata.title, level=UserFeedback.WARN) @@ -1520,7 +1516,7 @@ class ITUNES(DriverBase): else: self.log.error(" book_playlist not found") - if len(dev_books): + if dev_books is not None and len(dev_books): first_book = dev_books[0] if False: self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.name(), first_book.artist())) @@ -1551,7 +1547,7 @@ class ITUNES(DriverBase): dev_books = pl.Tracks break - if dev_books.Count: + if dev_books is not None and dev_books.Count: first_book = dev_books.Item(1) #if DEBUG: #self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.Name, first_book.Artist)) @@ -2526,7 +2522,15 @@ class ITUNES(DriverBase): self.log.info(" processing %s" % fp) if fp.startswith(prefs['library_path']): self.log.info(" '%s' stored in calibre database, not removed" % cached_book['title']) + elif not self.settings().extra_customization[self.USE_ITUNES_STORAGE] and \ + fp.startswith(self.iTunes_local_storage) and \ + os.path.exists(fp): + # Delete the copy in iTunes_local_storage + os.remove(fp) + if DEBUG: + self.log(" removing from iTunes_local_storage") else: + # Delete from iTunes Media folder if os.path.exists(fp): os.remove(fp) if DEBUG: @@ -2544,12 +2548,6 @@ class ITUNES(DriverBase): os.rmdir(author_storage_path) if DEBUG: self.log.info(" removing empty author directory") - ''' - else: - if DEBUG: - self.log.info(" author_storage_path not empty:") - self.log.info(" %s" % '\n'.join(author_files)) - ''' else: self.log.info(" '%s' does not exist at storage location" % cached_book['title']) @@ -2586,7 +2584,15 @@ class ITUNES(DriverBase): self.log.info(" processing %s" % fp) if fp.startswith(prefs['library_path']): self.log.info(" '%s' stored in calibre database, not removed" % cached_book['title']) + elif not self.settings().extra_customization[self.USE_ITUNES_STORAGE] and \ + fp.startswith(self.iTunes_local_storage) and \ + os.path.exists(fp): + # Delete the copy in iTunes_local_storage + os.remove(fp) + if DEBUG: + self.log(" removing from iTunes_local_storage") else: + # Delete from iTunes Media folder if os.path.exists(fp): os.remove(fp) if DEBUG: @@ -3234,6 +3240,17 @@ class ITUNES_ASYNC(ITUNES): if DEBUG: self.log.info(" existing thumb cache at '%s'" % self.archive_path) + # If enabled in config options, create/confirm an iTunes storage folder + if not self.settings().extra_customization[self.USE_ITUNES_STORAGE]: + self.iTunes_local_storage = os.path.join(config_dir,'iTunes storage') + if not os.path.exists(self.iTunes_local_storage): + if DEBUG: + self.log(" creating iTunes_local_storage at '%s'" % self.iTunes_local_storage) + os.mkdir(self.iTunes_local_storage) + else: + if DEBUG: + self.log(" existing iTunes_local_storage at '%s'" % self.iTunes_local_storage) + def sync_booklists(self, booklists, end_session=True): ''' Update metadata on device. From 02a69476e16f0f7cc986ab327ec15beef9807a09 Mon Sep 17 00:00:00 2001 From: Kovid Goyal' + + _('Enter a template to be used to create a link for' + 'an author in the books information dialog. This template will ' + 'be used when no link has been provided for the author using ' + 'Manage Authors. You can use the values {author} and ' + '{author_sort}, and any template function.') + ' ') # This option is no longer used. It remains for compatibility with upgrades # so the value can be migrated diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index ef21773ae4..1927b1448e 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -5,6 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal%s | %s | '%(name,
diff --git a/src/calibre/gui2/dialogs/restore_library.py b/src/calibre/gui2/dialogs/restore_library.py
index a57d6c86c1..60b224d1cd 100644
--- a/src/calibre/gui2/dialogs/restore_library.py
+++ b/src/calibre/gui2/dialogs/restore_library.py
@@ -54,7 +54,7 @@ class DBRestore(QDialog):
def reject(self):
self.rejected = True
self.restorer.progress_callback = lambda x, y: x
- QDialog.rejecet(self)
+ QDialog.reject(self)
def update(self):
if self.restorer.is_alive():
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index a2850679f1..841193373b 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -138,6 +138,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
(_('Partitioned'), 'partition')]
r('tags_browser_partition_method', gprefs, choices=choices)
r('tags_browser_collapse_at', gprefs)
+ r('default_author_link', config)
choices = set([k for k in db.field_metadata.all_field_keys()
if db.field_metadata[k]['is_category'] and
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index cc9133a36f..8dadfe3424 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -192,7 +192,7 @@
Enter a template that will be used to create a link for +an author in the books information dialog. Used when no link has been +provided for the author in Manage Authors. Enter a template that will be used to create a link for -an author in the books information dialog. Used when no link has been -provided for the author in Manage Authors. %s | %s | '%(name,
diff --git a/src/calibre/gui2/tag_browser/ui.py b/src/calibre/gui2/tag_browser/ui.py
index 0b01e20154..509f923be1 100644
--- a/src/calibre/gui2/tag_browser/ui.py
+++ b/src/calibre/gui2/tag_browser/ui.py
@@ -270,12 +270,14 @@ class TagBrowserMixin(object): # {{{
editor = EditAuthorsDialog(parent, db, id, select_sort)
d = editor.exec_()
if d:
- for (id, old_author, new_author, new_sort) in editor.result:
+ for (id, old_author, new_author, new_sort, new_link) in editor.result:
if old_author != new_author:
# The id might change if the new author already exists
id = db.rename_author(id, new_author)
db.set_sort_field_for_author(id, unicode(new_sort),
commit=False, notify=False)
+ db.set_link_field_for_author(id, unicode(new_link),
+ commit=False, notify=False)
db.commit()
self.library_view.model().refresh()
self.tags_view.recount()
From e762f41d46d4058164cf11304c56c667fdc6fcab Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 28 Jun 2011 21:40:44 +0100
Subject: [PATCH 44/47] Add edit link context menu item to tag browser Make
edit_authors remember geometry
---
.../gui2/dialogs/edit_authors_dialog.py | 40 +++++++++++++++++--
src/calibre/gui2/tag_browser/ui.py | 4 +-
src/calibre/gui2/tag_browser/view.py | 10 ++++-
3 files changed, 47 insertions(+), 7 deletions(-)
diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.py b/src/calibre/gui2/dialogs/edit_authors_dialog.py
index 1087c3cb82..300715c6e0 100644
--- a/src/calibre/gui2/dialogs/edit_authors_dialog.py
+++ b/src/calibre/gui2/dialogs/edit_authors_dialog.py
@@ -4,10 +4,11 @@ __docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
from PyQt4.Qt import (Qt, QDialog, QTableWidgetItem, QAbstractItemView, QIcon,
- QDialogButtonBox, QFrame, QLabel, QTimer, QMenu, QApplication)
+ QDialogButtonBox, QFrame, QLabel, QTimer, QMenu, QApplication,
+ QByteArray)
from calibre.ebooks.metadata import author_to_author_sort
-from calibre.gui2 import error_dialog
+from calibre.gui2 import error_dialog, gprefs
from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog
from calibre.utils.icu import sort_key
@@ -20,7 +21,7 @@ class tableItem(QTableWidgetItem):
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
- def __init__(self, parent, db, id_to_select, select_sort):
+ def __init__(self, parent, db, id_to_select, select_sort, select_link):
QDialog.__init__(self, parent)
Ui_EditAuthorsDialog.__init__(self)
self.setupUi(self)
@@ -29,6 +30,14 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
+ try:
+ self.table_column_widths = \
+ gprefs.get('manage_authors_table_widths', None)
+ geom = gprefs.get('manage_authors_dialog_geometry', bytearray(''))
+ self.restoreGeometry(QByteArray(geom))
+ except:
+ pass
+
self.buttonBox.accepted.connect(self.accepted)
# Set up the column headings
@@ -65,6 +74,8 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
if id == id_to_select:
if select_sort:
select_item = sort
+ elif select_link:
+ select_item = link
else:
select_item = aut
self.table.resizeColumnsToContents()
@@ -122,6 +133,28 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested .connect(self.show_context_menu)
+ def save_state(self):
+ self.table_column_widths = []
+ for c in range(0, self.table.columnCount()):
+ self.table_column_widths.append(self.table.columnWidth(c))
+ gprefs['manage_authors_table_widths'] = self.table_column_widths
+ gprefs['manage_authors_dialog_geometry'] = bytearray(self.saveGeometry())
+
+ def resizeEvent(self, *args):
+ QDialog.resizeEvent(self, *args)
+ if self.table_column_widths is not None:
+ for c,w in enumerate(self.table_column_widths):
+ self.table.setColumnWidth(c, w)
+ else:
+ # the vertical scroll bar might not be rendered, so might not yet
+ # have a width. Assume 25. Not a problem because user-changed column
+ # widths will be remembered
+ w = self.table.width() - 25 - self.table.verticalHeader().width()
+ w /= self.table.columnCount()
+ for c in range(0, self.table.columnCount()):
+ self.table.setColumnWidth(c, w)
+ self.save_state()
+
def show_context_menu(self, point):
self.context_item = self.table.itemAt(point)
case_menu = QMenu(_('Change Case'))
@@ -238,6 +271,7 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
self.auth_col.setIcon(self.blank_icon)
def accepted(self):
+ self.save_state()
self.result = []
for row in range(0,self.table.rowCount()):
id = self.table.item(row, 0).data(Qt.UserRole).toInt()[0]
diff --git a/src/calibre/gui2/tag_browser/ui.py b/src/calibre/gui2/tag_browser/ui.py
index 509f923be1..d7e504b3e9 100644
--- a/src/calibre/gui2/tag_browser/ui.py
+++ b/src/calibre/gui2/tag_browser/ui.py
@@ -262,12 +262,12 @@ class TagBrowserMixin(object): # {{{
self.library_view.select_rows(ids)
# refreshing the tags view happens at the emit()/call() site
- def do_author_sort_edit(self, parent, id, select_sort=True):
+ def do_author_sort_edit(self, parent, id, select_sort=True, select_link=False):
'''
Open the manage authors dialog
'''
db = self.library_view.model().db
- editor = EditAuthorsDialog(parent, db, id, select_sort)
+ editor = EditAuthorsDialog(parent, db, id, select_sort, select_link)
d = editor.exec_()
if d:
for (id, old_author, new_author, new_sort, new_link) in editor.result:
diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py
index 586d01ff87..788d85f79e 100644
--- a/src/calibre/gui2/tag_browser/view.py
+++ b/src/calibre/gui2/tag_browser/view.py
@@ -66,7 +66,7 @@ class TagsView(QTreeView): # {{{
tag_list_edit = pyqtSignal(object, object)
saved_search_edit = pyqtSignal(object)
rebuild_saved_searches = pyqtSignal()
- author_sort_edit = pyqtSignal(object, object)
+ author_sort_edit = pyqtSignal(object, object, object, object)
tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal()
drag_drop_finished = pyqtSignal(object)
@@ -277,7 +277,10 @@ class TagsView(QTreeView): # {{{
self.saved_search_edit.emit(category)
return
if action == 'edit_author_sort':
- self.author_sort_edit.emit(self, index)
+ self.author_sort_edit.emit(self, index, True, False)
+ return
+ if action == 'edit_author_link':
+ self.author_sort_edit.emit(self, index, False, True)
return
reset_filter_categories = True
@@ -346,6 +349,9 @@ class TagsView(QTreeView): # {{{
self.context_menu.addAction(_('Edit sort for %s')%display_name(tag),
partial(self.context_menu_handler,
action='edit_author_sort', index=tag.id))
+ self.context_menu.addAction(_('Edit link for %s')%display_name(tag),
+ partial(self.context_menu_handler,
+ action='edit_author_link', index=tag.id))
# is_editable is also overloaded to mean 'can be added
# to a user category'
From f07d04dc332096dcc8ad56873df2aa369e561f82 Mon Sep 17 00:00:00 2001
From: Kovid Goyal |