From 19eb3ad879b636186b74de3a0a82c6aabb54bf61 Mon Sep 17 00:00:00 2001 From: GRiker Date: Tue, 31 Jul 2012 10:00:17 -0600 Subject: [PATCH 01/27] wip --- src/calibre/library/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index d5defe96c7..4fc268ce7d 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -796,8 +796,10 @@ def catalog_option_parser(args): return parser, plugin, log def command_catalog(args, dbpath): + print("library.cli:command_catalog() EXPERIMENTAL MODE") parser, plugin, log = catalog_option_parser(args) opts, args = parser.parse_args(sys.argv[1:]) + if len(args) < 2: parser.print_help() print From add5bb824d7df55a1454bd0ed7dfbeff33c627dd Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 3 Aug 2012 03:23:15 -0600 Subject: [PATCH 02/27] WIP - changes to support prefix rules --- resources/catalog/stylesheet.css | 13 +- src/calibre/gui2/catalog/catalog_epub_mobi.py | 199 +++++++-- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 169 +++----- src/calibre/library/catalogs/epub_mobi.py | 35 +- .../library/catalogs/epub_mobi_builder.py | 388 +++++++++--------- src/calibre/library/cli.py | 2 +- 6 files changed, 437 insertions(+), 369 deletions(-) diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css index 55a7c9fbec..40b0d0eb4e 100644 --- a/resources/catalog/stylesheet.css +++ b/resources/catalog/stylesheet.css @@ -151,14 +151,23 @@ p.title { font-size:xx-large; } -p.wishlist_item, p.unread_book, p.read_book { - text-align:left; +p.wishlist_item, p.unread_book, p.read_book, p.line_item { + font-family:monospace; margin-top:0px; margin-bottom:0px; margin-left:2em; + text-align:left; text-indent:-2em; } +span.prefix {} +span.entry { + font-family: serif; + } + +/* +* Book Descriptions +*/ td.publisher, td.date { font-weight:bold; text-align:center; diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 3cfd94cc6e..7d7119bbf2 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -10,8 +10,11 @@ from calibre.ebooks.conversion.config import load_defaults from calibre.gui2 import gprefs from catalog_epub_mobi_ui import Ui_Form -from PyQt4.Qt import QCheckBox, QComboBox, QDoubleSpinBox, QLineEdit, \ - QRadioButton, QWidget +from PyQt4 import QtGui +from PyQt4.Qt import (Qt, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, + QHBoxLayout, QIcon, QLabel, QLineEdit, + QPlainTextEdit, QRadioButton, QSize, QTableWidget, QTableWidgetItem, + QVBoxLayout, QWidget) class PluginWidget(QWidget,Ui_Form): @@ -36,6 +39,7 @@ class PluginWidget(QWidget,Ui_Form): DoubleSpinBoxControls = [] LineEditControls = [] RadioButtonControls = [] + TableWidgetControls = [] for item in self.__dict__: if type(self.__dict__[item]) is QCheckBox: @@ -48,6 +52,8 @@ class PluginWidget(QWidget,Ui_Form): LineEditControls.append(str(self.__dict__[item].objectName())) elif type(self.__dict__[item]) is QRadioButton: RadioButtonControls.append(str(self.__dict__[item].objectName())) + elif type(self.__dict__[item]) is QTableWidget: + TableWidgetControls.append(str(self.__dict__[item].objectName())) option_fields = zip(CheckBoxControls, [True for i in CheckBoxControls], @@ -60,15 +66,41 @@ class PluginWidget(QWidget,Ui_Form): ['radio_button' for i in RadioButtonControls]) # LineEditControls - option_fields += zip(['exclude_genre'],['\[.+\]'],['line_edit']) + option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit']) option_fields += zip(['exclude_pattern'],[None],['line_edit']) option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit']) - option_fields += zip(['read_pattern'],['+'],['line_edit']) - option_fields += zip(['wishlist_tag'],['Wishlist'],['line_edit']) # SpinBoxControls option_fields += zip(['thumb_width'],[1.00],['spin_box']) + # Prefix rules TableWidget + # ordinal/enabled/rule name/source field/pattern/prefix + #option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'], + # [(1,True,'Read book','Tags','+','+'), + # (2,True,'Wishlist','Tags','Wishlist','x'), + # (3,False,'','','','')], + # ['table_widget','table_widget','table_widget']) + option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'], + [{'ordinal':1, + 'enabled':True, + 'name':'Read book', + 'field':'Tags', + 'pattern':'+', + 'prefix':'+'}, + {'ordinal':2, + 'enabled':True, + 'name':'Wishlist', + 'field':'Tags', + 'pattern':'Wishlist', + 'prefix':'x'}, + {'ordinal':3, + 'enabled':False, + 'name':'', + 'field':'', + 'pattern':'', + 'prefix':''}], + ['table_widget','table_widget','table_widget']) + self.OPTION_FIELDS = option_fields def initialize(self, name, db): @@ -78,23 +110,25 @@ class PluginWidget(QWidget,Ui_Form): ['generate_titles','generate_series','generate_genres', 'generate_recently_added','generate_descriptions','include_hr'] ComboBoxControls (c_type: combo_box): - ['read_source_field','exclude_source_field','header_note_source_field', + ['exclude_source_field','header_note_source_field', 'merge_source_field'] LineEditControls (c_type: line_edit): - ['exclude_genre','exclude_pattern','exclude_tags','read_pattern', - 'wishlist_tag'] + ['exclude_genre','exclude_pattern','exclude_tags'] RadioButtonControls (c_type: radio_button): ['merge_before','merge_after'] SpinBoxControls (c_type: spin_box): ['thumb_width'] + TableWidgetControls (c_type: table_widget): + ['prefix_rules_tw'] ''' - self.name = name self.db = db - self.populateComboBoxes() + self.all_custom_fields = self.db.custom_field_keys() + self.populate_combo_boxes() # Update dialog fields from stored options + prefix_rules = [] for opt in self.OPTION_FIELDS: c_name, c_def, c_type = opt opt_value = gprefs.get(self.name + '_' + c_name, c_def) @@ -114,11 +148,8 @@ class PluginWidget(QWidget,Ui_Form): getattr(self, c_name).setChecked(opt_value) elif c_type in ['spin_box']: getattr(self, c_name).setValue(float(opt_value)) - - # Init self.read_source_field_name - cs = unicode(self.read_source_field.currentText()) - read_source_spec = self.read_source_fields[cs] - self.read_source_field_name = read_source_spec['field'] + elif c_type in ['table_widget'] and c_name == 'prefix_rules_tw': + prefix_rules.append(opt_value) # Init self.exclude_source_field_name self.exclude_source_field_name = '' @@ -147,6 +178,32 @@ class PluginWidget(QWidget,Ui_Form): # Hook changes to Description section self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) + # Init prefix_rules + self.initialize_prefix_rules(prefix_rules) + self.populate_prefix_rules(prefix_rules) + + def initialize_prefix_rules(self, rules): + + # Assign icons to buttons + self.move_rule_up_tb.setToolTip('Move rule up') + self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) + self.add_rule_tb.setToolTip('Add a new rule') + self.add_rule_tb.setIcon(QIcon(I('plus.png'))) + self.delete_rule_tb.setToolTip('Delete selected rule') + self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) + self.move_rule_down_tb.setToolTip('Move rule down') + self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) + + # Configure the QTableWidget + # enabled/rule name/source field/pattern/prefix + self.prefix_rules_tw.clear() + header_labels = ['','Name','Source','Pattern','Prefix'] + self.prefix_rules_tw.setColumnCount(len(header_labels)) + self.prefix_rules_tw.setHorizontalHeaderLabels(header_labels) + self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) + self.prefix_rules_tw.setRowCount(len(rules)) + self.prefix_rules_tw.setSortingEnabled(False) + def options(self): # Save/return the current options # exclude_genre stores literally @@ -203,7 +260,7 @@ class PluginWidget(QWidget,Ui_Form): print " %s: %s" % (opt, repr(opts_dict[opt])) return opts_dict - def populateComboBoxes(self): + def populate_combo_boxes(self): # Custom column types declared in # gui2.preferences.create_custom_column:CreateCustomColumn() # As of 0.7.34: @@ -219,25 +276,9 @@ class PluginWidget(QWidget,Ui_Form): # text Column shown in the tag browser # *text Comma-separated text, like tags, shown in tag browser - all_custom_fields = self.db.custom_field_keys() - # Populate the 'Read book' hybrid - custom_fields = {} - custom_fields['Tag'] = {'field':'tag', 'datatype':u'text'} - for custom_field in all_custom_fields: - field_md = self.db.metadata_for_field(custom_field) - if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: - custom_fields[field_md['name']] = {'field':custom_field, - 'datatype':field_md['datatype']} - # Add the sorted eligible fields to the combo box - for cf in sorted(custom_fields): - self.read_source_field.addItem(cf) - self.read_source_fields = custom_fields - self.read_source_field.currentIndexChanged.connect(self.read_source_field_changed) - - # Populate the 'Excluded books' hybrid custom_fields = {} - for custom_field in all_custom_fields: + for custom_field in self.all_custom_fields: field_md = self.db.metadata_for_field(custom_field) if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: custom_fields[field_md['name']] = {'field':custom_field, @@ -253,7 +294,7 @@ class PluginWidget(QWidget,Ui_Form): # Populate the 'Header note' combo box custom_fields = {} - for custom_field in all_custom_fields: + for custom_field in self.all_custom_fields: field_md = self.db.metadata_for_field(custom_field) if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: custom_fields[field_md['name']] = {'field':custom_field, @@ -269,7 +310,7 @@ class PluginWidget(QWidget,Ui_Form): # Populate the 'Merge with Comments' combo box custom_fields = {} - for custom_field in all_custom_fields: + for custom_field in self.all_custom_fields: field_md = self.db.metadata_for_field(custom_field) if field_md['datatype'] in ['text','comments','composite']: custom_fields[field_md['name']] = {'field':custom_field, @@ -285,6 +326,42 @@ class PluginWidget(QWidget,Ui_Form): self.merge_after.setEnabled(False) self.include_hr.setEnabled(False) + def populate_prefix_rules(self,rules): + + def populate_table_row(row, data): + self.prefix_rules_tw.blockSignals(True) + self.prefix_rules_tw.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) + self.prefix_rules_tw.setItem(row, 1, QTableWidgetItem(data['name'])) + set_source_field_in_row(row, field=data['field']) + + self.prefix_rules_tw.setItem(row, 3, QTableWidgetItem(data['pattern'])) + self.prefix_rules_tw.setItem(row, 4, QTableWidgetItem(data['prefix'])) + self.prefix_rules_tw.blockSignals(False) + + def set_source_field_in_row(row, field=''): + custom_fields = {} + custom_fields['Tags'] = {'field':'tag', 'datatype':u'text'} + for custom_field in self.all_custom_fields: + field_md = self.db.metadata_for_field(custom_field) + if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: + custom_fields[field_md['name']] = {'field':custom_field, + 'datatype':field_md['datatype']} + self.prefix_rules_tw.setCellWidget(row, 2, SourceFieldComboBox(self, sorted(custom_fields.keys()), field)) + # Need to connect to something that monitors changes to control pattern field based on source_field type + + # Entry point + + for row, data in enumerate(sorted(rules, key=lambda k: k['ordinal'])): + populate_table_row(row, data) + + + + + + # Do this after populating cells + self.prefix_rules_tw.resizeColumnsToContents() + + def read_source_field_changed(self,new_index): ''' Process changes in the read_source_field combo box @@ -388,3 +465,55 @@ class PluginWidget(QWidget,Ui_Form): Process changes in the thumb_width spin box ''' pass + + +class CheckableTableWidgetItem(QTableWidgetItem): + ''' + Borrowed from kiwidude + ''' + + def __init__(self, checked=False, is_tristate=False): + QTableWidgetItem.__init__(self, '') + self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled )) + if is_tristate: + self.setFlags(self.flags() | Qt.ItemIsTristate) + if checked: + self.setCheckState(Qt.Checked) + else: + if is_tristate and checked is None: + self.setCheckState(Qt.PartiallyChecked) + else: + self.setCheckState(Qt.Unchecked) + + def get_boolean_value(self): + ''' + Return a boolean value indicating whether checkbox is checked + If this is a tristate checkbox, a partially checked value is returned as None + ''' + if self.checkState() == Qt.PartiallyChecked: + return None + else: + return self.checkState() == Qt.Checked + +class NoWheelComboBox(QComboBox): + + def wheelEvent (self, event): + # Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid + event.ignore() + +class SourceFieldComboBox(NoWheelComboBox): + + def __init__(self, parent, format_names, selected_text): + NoWheelComboBox.__init__(self, parent) + self.populate_combo(format_names, selected_text) + + def populate_combo(self, format_names, selected_text): + self.addItems(['']) + self.addItems(format_names) + if selected_text: + idx = self.findText(selected_text) + self.setCurrentIndex(idx) + else: + self.setCurrentIndex(0) + + diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 5136f86fae..31c5063a12 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -7,7 +7,7 @@ 0 0 650 - 596 + 603 @@ -203,6 +203,9 @@ e.g., [Project Gutenberg]</p> Excluded books + + QFormLayout::FieldsStayAtSizeHint + @@ -323,96 +326,66 @@ Default: ~,Catalog - + 0 0 - - - 0 - 0 - - - - Matching books will be displayed with a check mark - - Read books + Prefix rules - - - - - QLayout::SetDefaultConstraint - + + + - - - - 175 - 0 - + + + + 0 + 0 + - 200 - 16777215 + 16777215 + 118 - - &Column/value - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - read_source_field - - - - - 0 - 0 - - - - Column containing 'read' status - - - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 18 - - - - - - - - 150 - 0 - - - - 'read book' pattern - - - - - + + + + + ... + + + + + + + ... + + + + + + + ... + + + + + + + ... + + + + @@ -440,49 +413,7 @@ Default: ~,Catalog QFormLayout::FieldsStayAtSizeHint - - - - - - - 175 - 0 - - - - - 200 - 16777215 - - - - - - - &Wishlist tag - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - wishlist_tag - - - - - - - Books tagged as Wishlist items will be displayed with an X - - - - - - + @@ -542,7 +473,7 @@ Default: ~,Catalog - + @@ -599,7 +530,7 @@ Default: ~,Catalog - + diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index 81cb247236..1e9935427c 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -51,7 +51,8 @@ class EPUB_MOBI(CatalogPlugin): default=':', dest='exclude_book_marker', action = None, - help=_("field:pattern specifying custom field/contents indicating book should be excluded.\n" + help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n" + "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n" "Default: '%default'\n" "Applies to ePub, MOBI output formats")), Option('--exclude-genre', @@ -121,7 +122,7 @@ class EPUB_MOBI(CatalogPlugin): default='::', dest='merge_comments', action = None, - help=_(":[before|after]:[True|False] specifying:\n" + help=_("#:[before|after]:[True|False] specifying:\n" " Custom field containing notes to merge with Comments\n" " [before|after] Placement of notes with respect to Comments\n" " [True|False] - A horizontal rule is inserted between notes and Comments\n" @@ -134,11 +135,14 @@ class EPUB_MOBI(CatalogPlugin): help=_("Specifies the output profile. In some cases, an output profile is required to optimize the catalog for the device. For example, 'kindle' or 'kindle_dx' creates a structured Table of Contents with Sections and Articles.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), - Option('--read-book-marker', - default='tag:+', - dest='read_book_marker', - action = None, - help=_("field:pattern indicating book has been read.\n" "Default: '%default'\n" + Option('--prefix-rules', + default="(('Read books','tags','+','\u2713'),('Wishlist items','tags','Wishlist','\u00d7'))", + dest='prefix_rules', + action=None, + help=_("Specifies the rules used to include prefixes indicating read books, wishlist items and other user-specifed prefixes.\n" + "The model for a prefix rule is ('','','','').\n" + "When multiple rules are defined, the first matching rule will be used.\n" + "Default: '%default'\n" "Applies to ePub, MOBI output formats")), Option('--thumb-width', default='1.0', @@ -148,12 +152,6 @@ class EPUB_MOBI(CatalogPlugin): "Range: 1.0 - 2.0\n" "Default: '%default'\n" "Applies to ePub, MOBI output formats")), - Option('--wishlist-tag', - default='Wishlist', - dest='wishlist_tag', - action = None, - help=_("Tag indicating book to be displayed as wishlist item.\n" "Default: '%default'\n" - "Applies to: ePub, MOBI output formats")), ] # }}} @@ -276,6 +274,15 @@ class EPUB_MOBI(CatalogPlugin): log.error("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width,self.THUMB_SMALLEST)) opts.thumb_width = "1.0" + # Pre-process prefix_rules + try: + opts.prefix_rules = eval(opts.prefix_rules) + except: + log.error("malformed --prefix-rules: %s" % opts.prefix_rules) + raise + for rule in opts.prefix_rules: + if len(rule) != 4: + log.error("incorrect number of args for --prefix-rules: %s" % repr(rule)) # Display opts keys = opts_dict.keys() @@ -285,7 +292,7 @@ class EPUB_MOBI(CatalogPlugin): if key in ['catalog_title','authorClip','connected_kindle','descriptionClip', 'exclude_book_marker','exclude_genre','exclude_tags', 'header_note_source_field','merge_comments', - 'output_profile','read_book_marker', + 'output_profile','prefix_rules','read_book_marker', 'search_text','sort_by','sort_descriptions_by_author','sync', 'thumb_width','wishlist_tag']: build_log.append(" %s: %s" % (key, repr(opts_dict[key]))) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 76f96a3397..949b1a6b81 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -72,6 +72,7 @@ class CatalogBuilder(object): self.__currentStep = 0.0 self.__creator = opts.creator self.__db = db + self.__defaultPrefix = None self.__descriptionClip = opts.descriptionClip self.__error = [] self.__generateForKindle = True if (self.opts.fmt == 'mobi' and \ @@ -91,10 +92,9 @@ class CatalogBuilder(object): self.__output_profile = None self.__playOrder = 1 self.__plugin = plugin + self.__prefixRules = [] self.__progressInt = 0.0 self.__progressString = '' - f, _, p = opts.read_book_marker.partition(':') - self.__read_book_marker = {'field':f, 'pattern':p} f, p, hr = self.opts.merge_comments.split(':') self.__merge_comments = {'field':f, 'position':p, 'hr':hr} self.__reporter = report_progress @@ -113,6 +113,9 @@ class CatalogBuilder(object): self.__output_profile = profile break + # Process prefix rules + self.processPrefixRules() + # Confirm/create thumbs archive. if self.opts.generate_descriptions: if not os.path.exists(self.__cache_dir): @@ -269,6 +272,13 @@ class CatalogBuilder(object): return self.__db return property(fget=fget) @dynamic_property + def defaultPrefix(self): + def fget(self): + return self.__defaultPrefix + def fset(self, val): + self.__defaultPrefix = val + return property(fget=fget, fset=fset) + @dynamic_property def descriptionClip(self): def fget(self): return self.__descriptionClip @@ -363,6 +373,13 @@ class CatalogBuilder(object): return self.__plugin return property(fget=fget) @dynamic_property + def prefixRules(self): + def fget(self): + return self.__prefixRules + def fset(self, val): + self.__prefixRules = val + return property(fget=fget, fset=fset) + @dynamic_property def progressInt(self): def fget(self): return self.__progressInt @@ -437,27 +454,12 @@ class CatalogBuilder(object): return property(fget=fget, fset=fset) @dynamic_property - def MISSING_SYMBOL(self): - def fget(self): - return self.__output_profile.missing_char - return property(fget=fget) - @dynamic_property - def NOT_READ_SYMBOL(self): - def fget(self): - return '%s' % self.__output_profile.read_char - return property(fget=fget) - @dynamic_property def READING_SYMBOL(self): def fget(self): return '' if self.generateForKindle else \ '+' return property(fget=fget) @dynamic_property - def READ_SYMBOL(self): - def fget(self): - return self.__output_profile.read_char - return property(fget=fget) - @dynamic_property def FULL_RATING_SYMBOL(self): def fget(self): return self.__output_profile.ratings_char @@ -750,7 +752,7 @@ Author '{0}': if record['cover']: this_title['cover'] = re.sub('&', '&', record['cover']) - this_title['read'] = self.discoverReadStatus(record) + this_title['prefix'] = self.discoverPrefix(record) if record['tags']: this_title['tags'] = self.processSpecialTags(record['tags'], @@ -991,28 +993,16 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in book.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if book['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif book['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 + # Link to book aTag = Tag(soup, "a") @@ -1026,12 +1016,12 @@ Author '{0}': else: formatted_title = self.by_titles_normal_title_template.format(**args).rstrip() aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) - ptc += 1 + spanTag.insert(stc, aTag) + stc += 1 # Dot - pBookTag.insert(ptc, NavigableString(" · ")) - ptc += 1 + spanTag.insert(stc, NavigableString(" · ")) + stc += 1 # Link to author emTag = Tag(soup, "em") @@ -1040,7 +1030,10 @@ Author '{0}': aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) aTag.insert(0, NavigableString(book['author'])) emTag.insert(0,aTag) - pBookTag.insert(ptc, emTag) + spanTag.insert(stc, emTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 if divRunningTag is not None: @@ -1172,10 +1165,8 @@ Author '{0}': aTag['href'] = "%s.html#%s_series" % ('BySeries', re.sub('\W','',book['series']).lower()) aTag.insert(0, book['series']) - #pSeriesTag.insert(0, NavigableString(self.NOT_READ_SYMBOL)) pSeriesTag.insert(0, aTag) else: - #pSeriesTag.insert(0,NavigableString(self.NOT_READ_SYMBOL + '%s' % book['series'])) pSeriesTag.insert(0,NavigableString('%s' % book['series'])) if author_count == 1: @@ -1189,28 +1180,15 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in book.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if book['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif book['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 aTag = Tag(soup, "a") if self.opts.generate_descriptions: @@ -1227,7 +1205,9 @@ Author '{0}': non_series_books += 1 aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) + spanTag.insert(ptc, aTag) + stc += 1 + pBookTag.insert(ptc, spanTag) ptc += 1 if author_count == 1: @@ -1337,28 +1317,15 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in new_entry.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if new_entry['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif new_entry['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 aTag = Tag(soup, "a") if self.opts.generate_descriptions: @@ -1372,7 +1339,10 @@ Author '{0}': formatted_title = self.by_month_added_normal_title_template.format(**args).rstrip() non_series_books += 1 aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) + spanTag.insert(stc, aTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 divTag.insert(dtc, pBookTag) @@ -1393,28 +1363,15 @@ Author '{0}': for new_entry in date_range_list: # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in new_entry.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if new_entry['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif new_entry['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(new_entry['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 aTag = Tag(soup, "a") if self.opts.generate_descriptions: @@ -1427,12 +1384,12 @@ Author '{0}': else: formatted_title = self.by_recently_added_normal_title_template.format(**args).rstrip() aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) - ptc += 1 + spanTag.insert(stc, aTag) + stc += 1 # Dot - pBookTag.insert(ptc, NavigableString(" · ")) - ptc += 1 + spanTag.insert(stc, NavigableString(" · ")) + stc += 1 # Link to author emTag = Tag(soup, "em") @@ -1441,7 +1398,10 @@ Author '{0}': aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author'])) aTag.insert(0, NavigableString(new_entry['author'])) emTag.insert(0,aTag) - pBookTag.insert(ptc, emTag) + spanTag.insert(stc, emTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 divTag.insert(dtc, pBookTag) @@ -1799,30 +1759,16 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - book['read'] = self.discoverReadStatus(book) + book['prefix'] = self.discoverPrefix(book) + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in book.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if book.get('read', False): - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif book['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 aTag = Tag(soup, "a") if self.opts.generate_descriptions: @@ -1838,12 +1784,13 @@ Author '{0}': args = self.generateFormatArgs(book) formatted_title = self.by_series_title_template.format(**args).rstrip() aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) - ptc += 1 + + spanTag.insert(stc, aTag) + stc += 1 # · - pBookTag.insert(ptc, NavigableString(' · ')) - ptc += 1 + spanTag.insert(stc, NavigableString(' · ')) + stc += 1 # Link to author aTag = Tag(soup, "a") @@ -1851,7 +1798,10 @@ Author '{0}': aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(escape(' & '.join(book['authors'])))) aTag.insert(0, NavigableString(' & '.join(book['authors']))) - pBookTag.insert(ptc, aTag) + spanTag.insert(stc, aTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 divTag.insert(dtc, pBookTag) @@ -1905,7 +1855,7 @@ Author '{0}': this_book['author'] = book['author'] this_book['title'] = book['title'] this_book['author_sort'] = capitalize(book['author_sort']) - this_book['read'] = book['read'] + this_book['prefix'] = book['prefix'] this_book['tags'] = book['tags'] this_book['id'] = book['id'] this_book['series'] = book['series'] @@ -3165,7 +3115,7 @@ Author '{0}': if not os.path.isdir(images_path): os.makedirs(images_path) - def discoverReadStatus(self, record): + def discoverPrefix(self, record): ''' Given a field:pattern spec, discover if this book marked as read @@ -3177,25 +3127,42 @@ Author '{0}': datatype datetime: #field_name:.* ''' - # Legacy handling of special 'read' tag - field = self.__read_book_marker['field'] - pat = self.__read_book_marker['pattern'] - if field == 'tag' and pat in record['tags']: - return True + if self.opts.verbose: + self.opts.log.info("\tevaluating %s (%s) for prefix matches" % (record['title'], record['authors'])) + # Compare the record to each rule looking for a match + for rule in self.prefixRules: + if False and self.opts.verbose: + self.opts.log.info("\t evaluating prefix_rule '%s'" % rule['name']) - field_contents = self.__db.get_field(record['id'], - field, + # Literal comparison for Tags field + if rule['field'].lower() == 'tags': + if rule['pattern'].lower() in map(unicode.lower,record['tags']): + if self.opts.verbose: + self.opts.log.info("\t '%s' found in '%s' (%s)" % + (rule['pattern'], rule['field'], rule['name'])) + return rule['prefix'] + + # Regex comparison for custom field + elif rule['field'].startswith('#'): + field_contents = self.__db.get_field(record['id'], + rule['field'], index_is_id=True) - if field_contents: - try: - if re.search(pat, unicode(field_contents), - re.IGNORECASE) is not None: - return True - except: - # Compiling of pat failed, ignore it - pass + if field_contents: + try: + if re.search(rule['pattern'], unicode(field_contents), + re.IGNORECASE) is not None: + if self.opts.verbose: + self.opts.log.info("\t '%s' found in '%s' (%s)" % + (rule['pattern'], rule['field'], rule['name'])) + return rule['prefix'] + except: + # Compiling of pat failed, ignore it + pass + + if False and self.opts.verbose: + self.opts.log.info("\t No prefix match found") + return None - return False def filterDbTags(self, tags): # Remove the special marker tags from the database's tag list, @@ -3227,9 +3194,13 @@ Author '{0}': if tag in self.markerTags: excluded_tags.append(tag) continue - if re.search(self.opts.exclude_genre, tag): - excluded_tags.append(tag) - continue + try: + if re.search(self.opts.exclude_genre, tag): + excluded_tags.append(tag) + continue + except: + self.opts.log.error("\tfilterDbTags(): malformed --exclude-genre regex pattern: %s" % self.opts.exclude_genre) + if tag == ' ': continue @@ -3266,6 +3237,20 @@ Author '{0}': else: return None + def formatPrefix(self,prefix_char,soup): + # Generate the HTML for the prefix portion of the listing + spanTag = Tag(soup, "span") + if prefix_char is None: + spanTag['style'] = "color:white" + spanTag.insert(0,NavigableString(self.defaultPrefix)) + # 2e3a is 'two-em dash', which matches width in Kindle Previewer + # too wide in calibre viewer + # minimal visual distraction + # spanTag.insert(0,NavigableString(u'\u2e3a')) + else: + spanTag.insert(0,NavigableString(prefix_char)) + return spanTag + def generateAuthorAnchor(self, author): # Strip white space to '' return re.sub("\W","", author) @@ -3359,28 +3344,15 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in book.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if book['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif book['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 # Add the book title aTag = Tag(soup, "a") @@ -3398,7 +3370,10 @@ Author '{0}': non_series_books += 1 aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) + spanTag.insert(stc, aTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 divTag.insert(dtc, pBookTag) @@ -3463,15 +3438,13 @@ Author '{0}': # Author, author_prefix (read|reading|none symbol or missing symbol) author = book['author'] - if self.opts.wishlist_tag in book.get('tags', []): - author_prefix = self.MISSING_SYMBOL + " by " + + if book['prefix']: + author_prefix = book['prefix'] + " by " + elif self.opts.connected_kindle and book['id'] in self.bookmarked_books: + author_prefix = self.READING_SYMBOL + " by " else: - if book['read']: - author_prefix = self.READ_SYMBOL + " by " - elif self.opts.connected_kindle and book['id'] in self.bookmarked_books: - author_prefix = self.READING_SYMBOL + " by " - else: - author_prefix = "by " + author_prefix = "by " # Genres genres = '' @@ -4005,6 +3978,22 @@ Author '{0}': return merged + def processPrefixRules(self): + # Put the prefix rules into an ordered list of dicts + try: + for rule in self.opts.prefix_rules: + prefix_rule = {} + prefix_rule['name'] = rule[0] + prefix_rule['field'] = rule[1] + prefix_rule['pattern'] = rule[2] + prefix_rule['prefix'] = rule[3] + self.prefixRules.append(prefix_rule) + except: + self.opts.log.error("malformed self.opts.prefix_rules: %s" % repr(self.opts.prefix_rules)) + raise + # Use the highest order prefix symbol as default + self.defaultPrefix = self.opts.prefix_rules[0][3] + def processExclusions(self, data_set): ''' Remove excluded entries @@ -4026,17 +4015,20 @@ Author '{0}': return filtered_data_set def processSpecialTags(self, tags, this_title, opts): + tag_list = [] - for tag in tags: - tag = self.convertHTMLEntities(tag) - if re.search(opts.exclude_genre, tag): - continue - elif self.__read_book_marker['field'] == 'tag' and \ - tag == self.__read_book_marker['pattern']: - # remove 'read' tag - continue - else: - tag_list.append(tag) + + try: + for tag in tags: + tag = self.convertHTMLEntities(tag) + if re.search(opts.exclude_genre, tag): + continue + else: + tag_list.append(tag) + except: + self.opts.log.error("\tprocessSpecialTags(): malformed --exclude-genre regex pattern: %s" % opts.exclude_genre) + return tags + return tag_list def updateProgressFullStep(self, description): diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 4fc268ce7d..2b65ed37da 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -719,6 +719,7 @@ def catalog_option_parser(args): def add_plugin_parser_options(fmt, parser, log): # Fetch the extension-specific CLI options from the plugin + # library.catalogs..py plugin = plugin_for_catalog_format(fmt) for option in plugin.cli_options: if option.action: @@ -796,7 +797,6 @@ def catalog_option_parser(args): return parser, plugin, log def command_catalog(args, dbpath): - print("library.cli:command_catalog() EXPERIMENTAL MODE") parser, plugin, log = catalog_option_parser(args) opts, args = parser.parse_args(sys.argv[1:]) From 6c781af4d78e13c5dee03432ac789fc8ed93778b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 4 Aug 2012 23:28:18 +0530 Subject: [PATCH 03/27] Sueddeutsche Mobil by Andreas Zeiser --- recipes/sueddeutsche_mobil.recipe | 117 ++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 recipes/sueddeutsche_mobil.recipe diff --git a/recipes/sueddeutsche_mobil.recipe b/recipes/sueddeutsche_mobil.recipe new file mode 100644 index 0000000000..d1b08cbcba --- /dev/null +++ b/recipes/sueddeutsche_mobil.recipe @@ -0,0 +1,117 @@ +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +__license__ = 'GPL v3' +__copyright__ = '2012, Andreas Zeiser ' +''' +szmobil.sueddeutsche.de/ +''' + +from calibre import strftime +from calibre.web.feeds.recipes import BasicNewsRecipe +import re + +class SZmobil(BasicNewsRecipe): + title = u'Süddeutsche Zeitung mobil' + __author__ = u'Andreas Zeiser' + description = u'Nachrichten aus Deutschland. Zugriff auf kostenpflichtiges Abo SZ mobil.' + publisher = u'Sueddeutsche Zeitung' + language = u'de' + publication_type = u'newspaper' + category = u'news, politics, Germany' + + no_stylesheets = True + oldest_article = 2 + encoding = 'iso-8859-1' + needs_subscription = True + remove_empty_feeds = True + delay = 1 + cover_source = 'http://www.sueddeutsche.de/verlag' + + timefmt = ' [%a, %d %b, %Y]' + + root_url ='http://szmobil.sueddeutsche.de/' + keep_only_tags = [dict(name='div', attrs={'class':'article'})] + + def get_cover_url(self): + src = self.index_to_soup(self.cover_source) + image_url = src.find(attrs={'class':'preview-image'}) + return image_url.div.img['src'] + + def get_browser(self): + browser = BasicNewsRecipe.get_browser(self) + + # Login via fetching of Streiflicht -> Fill out login request + url = self.root_url + 'show.php?id=streif' + browser.open(url) + + browser.select_form(nr=0) # to select the first form + browser['username'] = self.username + browser['password'] = self.password + browser.submit() + + return browser + + def parse_index(self): + # find all sections + src = self.index_to_soup('http://szmobil.sueddeutsche.de') + feeds = [] + for itt in src.findAll('a',href=True): + if itt['href'].startswith('show.php?section'): + feeds.append( (itt.string[0:-2],itt['href']) ) + + all_articles = [] + for feed in feeds: + feed_url = self.root_url + feed[1] + feed_title = feed[0] + + self.report_progress(0, ('Fetching feed')+' %s...'%(feed_title if feed_title else feed_url)) + + src = self.index_to_soup(feed_url) + articles = [] + shorttitles = dict() + for itt in src.findAll('a', href=True): + if itt['href'].startswith('show.php?id='): + article_url = itt['href'] + article_id = int(re.search("id=(\d*)&etag=", itt['href']).group(1)) + + # first check if link is a special article in section "Meinungsseite" + if itt.find('strong')!= None: + article_name = itt.strong.string + article_shorttitle = itt.contents[1] + + articles.append( (article_name, article_url, article_id) ) + shorttitles[article_id] = article_shorttitle + continue + + + # candidate for a general article + if itt.string == None: + article_name = '' + else: + article_name = itt.string + + if (article_name[0:10] == " mehr"): + # just another link ("mehr") to an article + continue + + if itt.has_key('id'): + shorttitles[article_id] = article_name + else: + articles.append( (article_name, article_url, article_id) ) + + feed_articles = [] + for article_name, article_url, article_id in articles: + url = self.root_url + article_url + title = article_name + pubdate = strftime('%a, %d %b') + description = '' + if shorttitles.has_key(article_id): + description = shorttitles[article_id] + # we do not want the flag ("Impressum") + if "HERAUSGEGEBEN VOM" in description: + continue + d = dict(title=title, url=url, date=pubdate, description=description, content='') + feed_articles.append(d) + all_articles.append( (feed_title, feed_articles) ) + + return all_articles + From 5aa627e5edfc825378b78b61116ddee49bc6781c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 5 Aug 2012 09:28:47 +0530 Subject: [PATCH 04/27] Update Metro UK --- recipes/metro_uk.recipe | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/recipes/metro_uk.recipe b/recipes/metro_uk.recipe index fa5d5c19c8..5b7b3a64ed 100644 --- a/recipes/metro_uk.recipe +++ b/recipes/metro_uk.recipe @@ -1,31 +1,42 @@ from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1306097511(BasicNewsRecipe): title = u'Metro UK' - description = 'News as provide by The Metro -UK' + description = 'Author Dave Asbury : News as provide by The Metro -UK' #timefmt = '' __author__ = 'Dave Asbury' - #last update 9/6/12 + #last update 4/8/12 cover_url = 'http://profile.ak.fbcdn.net/hprofile-ak-snc4/276636_117118184990145_2132092232_n.jpg' - #no_stylesheets = True + no_stylesheets = True oldest_article = 1 - max_articles_per_feed = 10 + max_articles_per_feed = 12 remove_empty_feeds = True remove_javascript = True - auto_cleanup = True + #auto_cleanup = True encoding = 'UTF-8' - + cover_url ='http://profile.ak.fbcdn.net/hprofile-ak-snc4/157897_117118184990145_840702264_n.jpg' language = 'en_GB' masthead_url = 'http://e-edition.metro.co.uk/images/metro_logo.gif' + extra_css = ''' + h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:1.6em;} + h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:1.2em;} + p{font-family:Arial,Helvetica,sans-serif;font-size:1.0em;} + body{font-family:Helvetica,Arial,sans-serif;font-size:1.0em;} + ''' keep_only_tags = [ - - ] + #dict(name='h1'), + #dict(name='h2'), + #dict(name='div', attrs={'class' : ['row','article','img-cnt figure','clrd']}) + #dict(name='h3'), + #dict(attrs={'class' : 'BText'}), + ] remove_tags = [ - + dict(name='span',attrs={'class' : 'share'}), + dict(name='li'), + dict(attrs={'class' : ['twitter-share-button','header-forms','hdr-lnks','close','art-rgt','fd-gr1-b clrd google-article','news m12 clrd clr-b p5t shareBtm','item-ds csl-3-img news','c-1of3 c-last','c-1of1','pd','item-ds csl-3-img sport']}), + dict(attrs={'id' : ['','sky-left','sky-right','ftr-nav','and-ftr','notificationList','logo','miniLogo','comments-news','metro_extras']}) ] - + remove_tags_before = dict(name='h1') + #remove_tags_after = dict(attrs={'id':['topic-buttons']}) feeds = [ (u'News', u'http://www.metro.co.uk/rss/news/'), (u'Money', u'http://www.metro.co.uk/rss/money/'), (u'Sport', u'http://www.metro.co.uk/rss/sport/'), (u'Film', u'http://www.metro.co.uk/rss/metrolife/film/'), (u'Music', u'http://www.metro.co.uk/rss/metrolife/music/'), (u'TV', u'http://www.metro.co.uk/rss/tv/'), (u'Showbiz', u'http://www.metro.co.uk/rss/showbiz/'), (u'Weird News', u'http://www.metro.co.uk/rss/weird/'), (u'Travel', u'http://www.metro.co.uk/rss/travel/'), (u'Lifestyle', u'http://www.metro.co.uk/rss/lifestyle/'), (u'Books', u'http://www.metro.co.uk/rss/lifestyle/books/'), (u'Food', u'http://www.metro.co.uk/rss/lifestyle/restaurants/')] - extra_css = ''' - body{ text-align: justify; font-family:Arial,Helvetica,sans-serif; font-size:11px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:normal;} - ''' From b402a8ad2e431e5bb670623c62eb4135153da924 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 5 Aug 2012 09:47:51 +0530 Subject: [PATCH 05/27] eKundelek.pl by Artur Stachecki --- recipes/ekundelek_pl.recipe | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 recipes/ekundelek_pl.recipe diff --git a/recipes/ekundelek_pl.recipe b/recipes/ekundelek_pl.recipe new file mode 100644 index 0000000000..ebc5d39bbd --- /dev/null +++ b/recipes/ekundelek_pl.recipe @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = u'2012, Artur Stachecki ' + +from calibre.web.feeds.news import BasicNewsRecipe + +class swiatczytnikow(BasicNewsRecipe): + title = u'eKundelek' + description = u'Najsympatyczniejszy blog o e-czytnikach Kindle' + language = 'pl' + __author__ = u'Artur Stachecki' + oldest_article = 7 + max_articles_per_feed = 100 + + remove_tags = [dict(name = 'div', attrs = {'class' : 'feedflare'})] + + feeds = [(u'Wpisy', u'http://feeds.feedburner.com/Ekundelekpl?format=xml')] From 4c8a0231f2f2129f63108466120a348810130e4d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 5 Aug 2012 08:21:20 +0200 Subject: [PATCH 06/27] Do not attempt to display hierarchy in the tag browser if categories are not sorted by name. --- src/calibre/gui2/tag_browser/model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 012f441bea..dff2d0143d 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -425,7 +425,8 @@ class TagsModel(QAbstractItemModel): # {{{ last_idx = -collapse category_is_hierarchical = not ( key in ['authors', 'publisher', 'news', 'formats', 'rating'] or - key not in self.db.prefs.get('categories_using_hierarchy', [])) + key not in self.db.prefs.get('categories_using_hierarchy', []) or + config['sort_tags_by'] != 'name') for idx,tag in enumerate(data[key]): components = None From 67c98bae56fdfad46aca98a3a002b8ec896bd384 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sun, 5 Aug 2012 04:38:35 -0600 Subject: [PATCH 07/27] Stable version with prefix_rules* functions --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 460 +++++++++--------- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 12 +- .../library/catalogs/epub_mobi_builder.py | 39 +- 3 files changed, 256 insertions(+), 255 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 7f76406718..54e8bf5db9 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -112,131 +112,6 @@ class PluginWidget(QWidget,Ui_Form): 'datatype':field_md['datatype']} self.eligible_custom_fields = custom_fields - def generatePrefixList(self): - def prefix_sorter(item): - key = item - if item[0] == "_": - key = 'zzz' + item - return key - - - # Create a list of prefixes for user selection - raw_prefix_list = [ - ('Ampersand',u'&'), - ('Angle left double',u'\u00ab'), - ('Angle left',u'\u2039'), - ('Angle right double',u'\u00bb'), - ('Angle right',u'\u203a'), - ('Arrow double',u'\u2194'), - ('Arrow down',u'\u2193'), - ('Arrow left',u'\u2190'), - ('Arrow right',u'\u2192'), - ('Arrow up',u'\u2191'), - ('Asterisk',u'*'), - ('At sign',u'@'), - ('Bullet smallest',u'\u22c5'), - ('Bullet small',u'\u00b7'), - ('Bullet',u'\u2022'), - ('Caret',u'^'), - ('Checkmark',u'\u2713'), - ('Copyright',u'\u00a9'), - ('Currency dollar',u'$'), - ('Currency euro',u'\u20ac'), - ('Dagger double',u'\u2021'), - ('Dagger',u'\u2020'), - ('Degree',u'\u00b0'), - ('Dots3',u'\u2234'), - ('Hash',u'#'), - ('Infinity',u'\u221e'), - ('Lozenge',u'\u25ca'), - ('Math divide',u'\u00f7'), - ('Math empty',u'\u2205'), - ('Math equals',u'='), - ('Math minus',u'\u2212'), - ('Math plus circled',u'\u2295'), - ('Math times circled',u'\u2297'), - ('Math times',u'\u00d7'), - ('O slash',u'\u00d8'), - ('Paragraph',u'\u00b6'), - ('Percent',u'%'), - ('Plus-or-minus',u'\u00b1'), - ('Plus',u'+'), - ('Punctuation colon',u':'), - ('Punctuation colon-semi',u';'), - ('Punctuation exclamation',u'!'), - ('Punctuation question',u'?'), - ('Registered trademark',u'\u00ae'), - ('Section',u'\u00a7'), - ('Tilde',u'~'), - ('Vertical bar',u'|'), - ('Vertical bar broken',u'\u00a6'), - ('_0',u'0'), - ('_1',u'1'), - ('_2',u'2'), - ('_3',u'3'), - ('_4',u'4'), - ('_5',u'5'), - ('_6',u'6'), - ('_7',u'7'), - ('_8',u'8'), - ('_9',u'9'), - ('_A',u'A'), - ('_B',u'B'), - ('_C',u'C'), - ('_D',u'D'), - ('_E',u'E'), - ('_F',u'F'), - ('_G',u'G'), - ('_H',u'H'), - ('_I',u'I'), - ('_J',u'J'), - ('_K',u'K'), - ('_L',u'L'), - ('_M',u'M'), - ('_N',u'N'), - ('_O',u'O'), - ('_P',u'P'), - ('_Q',u'Q'), - ('_R',u'R'), - ('_S',u'S'), - ('_T',u'T'), - ('_U',u'U'), - ('_V',u'V'), - ('_W',u'W'), - ('_X',u'X'), - ('_Y',u'Y'), - ('_Z',u'Z'), - ('_a',u'a'), - ('_b',u'b'), - ('_c',u'c'), - ('_d',u'd'), - ('_e',u'e'), - ('_f',u'f'), - ('_g',u'g'), - ('_h',u'h'), - ('_i',u'i'), - ('_j',u'j'), - ('_k',u'k'), - ('_l',u'l'), - ('_m',u'm'), - ('_n',u'n'), - ('_o',u'o'), - ('_p',u'p'), - ('_q',u'q'), - ('_r',u'r'), - ('_s',u's'), - ('_t',u't'), - ('_u',u'u'), - ('_v',u'v'), - ('_w',u'w'), - ('_x',u'x'), - ('_y',u'y'), - ('_z',u'z'), - ] - #raw_prefix_list = sorted(raw_prefix_list, key=lambda k: sort_key(k[0])) - raw_prefix_list = sorted(raw_prefix_list, key=prefix_sorter) - self.prefixes = [x[1] for x in raw_prefix_list] - def initialize(self, name, db): ''' @@ -259,7 +134,6 @@ class PluginWidget(QWidget,Ui_Form): self.name = name self.db = db self.fetchEligibleCustomFields() - self.generatePrefixList() self.populate_combo_boxes() @@ -315,41 +189,8 @@ class PluginWidget(QWidget,Ui_Form): # Hook changes to Description section self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) - # Neaten up the prefix rules - self.prefix_rules_initialize() - self.populate_prefix_rules(prefix_rules) - self.prefix_rules_tw.resizeColumnsToContents() - self.prefix_rules_resize_name(1.5) - self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) - - def prefix_rules_initialize(self): - # Assign icons to buttons, hook clicks - self.move_rule_up_tb.setToolTip('Move rule up') - self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) - self.move_rule_up_tb.clicked.connect(self.prefix_rules_move_row_up) - - self.add_rule_tb.setToolTip('Add a new rule') - self.add_rule_tb.setIcon(QIcon(I('plus.png'))) - self.add_rule_tb.clicked.connect(self.prefix_rules_add_row) - - self.delete_rule_tb.setToolTip('Delete selected rule') - self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) - self.delete_rule_tb.clicked.connect(self.prefix_rules_delete_row) - - self.move_rule_down_tb.setToolTip('Move rule down') - self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) - self.move_rule_down_tb.clicked.connect(self.prefix_rules_move_row_down) - - # Configure the QTableWidget - # enabled/rule name/source field/pattern/prefix - self.prefix_rules_tw.clear() - header_labels = ['','Name','Prefix','Source','Pattern'] - self.prefix_rules_tw.setColumnCount(len(header_labels)) - self.prefix_rules_tw.setHorizontalHeaderLabels(header_labels) - self.prefix_rules_tw.setSortingEnabled(False) - self.prefix_rules_tw.setSelectionBehavior(QAbstractItemView.SelectRows) - - self.prefix_rules_tw.cellDoubleClicked.connect(self.prefix_rules_cell_double_clicked) + # Initialize prefix rules + self.prefix_rules_initialize(prefix_rules) def options(self): # Save/return the current options @@ -505,75 +346,18 @@ class PluginWidget(QWidget,Ui_Form): self.merge_after.setEnabled(False) self.include_hr.setEnabled(False) - def populate_prefix_rules(self,rules): - # Format of rules list is different if default values vs retrieved JSON - # Hack to normalize list style - if type(rules[0]) is list: - rules = rules[0] - self.prefix_rules_tw.setFocus() - for row, rule in enumerate(rules): - self.prefix_rules_tw.insertRow(row) - self.prefix_rules_select_and_scroll_to_row(row) - self.populate_prefix_rules_table_row(row, rule) - self.prefix_rules_tw.selectRow(0) - - def populate_prefix_rules_table_row(self, row, data): - - def set_prefix_field_in_row(row, col, field=''): - prefix_combo = PrefixRulesComboBox(self, self.prefixes, field) - self.prefix_rules_tw.setCellWidget(row, col, prefix_combo) - - def set_rule_name_in_row(row, col, name=''): - rule_name = QLineEdit(name) - rule_name.home(False) - rule_name.editingFinished.connect(self.prefix_rules_rule_name_edited) - self.prefix_rules_tw.setCellWidget(row, col, rule_name) - - def set_source_field_in_row(row, col, field=''): - source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) - source_combo.currentIndexChanged.connect(partial(self.prefix_rules_source_index_changed, source_combo, row)) - #source_combo.currentIndexChanged.connect(self.prefix_rules_source_index_changed, source_combo, row) - self.prefix_rules_tw.setCellWidget(row, col, source_combo) - return source_combo - - - # Entry point - self.prefix_rules_tw.blockSignals(True) - #print("populate_prefix_rules_table_row processing rule:\n%s\n" % data) - - # Column 0: Enabled - self.prefix_rules_tw.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) - - # Column 1: Rule name - #rule_name = QTableWidgetItem(data['name']) - #self.prefix_rules_tw.setItem(row, 1, rule_name) - set_rule_name_in_row(row, 1, name=data['name']) - - # Column 2: Prefix - set_prefix_field_in_row(row, 2, field=data['prefix']) - - # Column 3: Source field - source_combo = set_source_field_in_row(row, 3, field=data['field']) - - # Column 4: Pattern - # The contents of the Pattern field is driven by the Source field - self.prefix_rules_source_index_changed(source_combo, row, 4, pattern=data['pattern']) - - self.prefix_rules_tw.blockSignals(False) def prefix_rules_add_row(self): # Called when '+' clicked self.prefix_rules_tw.setFocus() row = self.prefix_rules_tw.currentRow() + 1 self.prefix_rules_tw.insertRow(row) - self.populate_prefix_rules_table_row(row, self.prefix_rules_create_blank_row_data()) + self.prefix_rules_populate_table_row(row, self.prefix_rules_create_blank_row_data()) self.prefix_rules_select_and_scroll_to_row(row) self.prefix_rules_tw.resizeColumnsToContents() + # Just in case table was empty self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) - def prefix_rules_cell_double_clicked(self, row, col): - print("prefix_rules_cell_double_clicked: row:%d col:%d" % (row, col)) - def prefix_rules_convert_row_to_data(self, row): data = self.prefix_rules_create_blank_row_data() data['ordinal'] = row @@ -612,6 +396,142 @@ class PluginWidget(QWidget,Ui_Form): elif self.prefix_rules_tw.rowCount() > 0: self.prefix_rules_select_and_scroll_to_row(first_sel_row - 1) + def prefix_rules_generate_prefix_list(self): + def prefix_sorter(item): + key = item + if item[0] == "_": + key = 'zzz' + item + return key + + + # Create a list of prefixes for user selection + raw_prefix_list = [ + ('Ampersand',u'&'), + ('Angle left double',u'\u00ab'), + ('Angle left',u'\u2039'), + ('Angle right double',u'\u00bb'), + ('Angle right',u'\u203a'), + ('Arrow carriage return',u'\u21b5'), + ('Arrow double',u'\u2194'), + ('Arrow down',u'\u2193'), + ('Arrow left',u'\u2190'), + ('Arrow right',u'\u2192'), + ('Arrow up',u'\u2191'), + ('Asterisk',u'*'), + ('At sign',u'@'), + ('Bullet smallest',u'\u22c5'), + ('Bullet small',u'\u00b7'), + ('Bullet',u'\u2022'), + ('Cards clubs',u'\u2663'), + ('Cards diamonds',u'\u2666'), + ('Cards hearts',u'\u2665'), + ('Cards spades',u'\u2660'), + ('Caret',u'^'), + ('Checkmark',u'\u2713'), + ('Copyright circle c',u'\u00a9'), + ('Copyright circle r',u'\u00ae'), + ('Copyright trademark',u'\u2122'), + ('Currency cent',u'\u00a2'), + ('Currency dollar',u'$'), + ('Currency euro',u'\u20ac'), + ('Currency pound',u'\u00a3'), + ('Currency yen',u'\u00a5'), + ('Dagger double',u'\u2021'), + ('Dagger',u'\u2020'), + ('Degree',u'\u00b0'), + ('Dots3',u'\u2234'), + ('Hash',u'#'), + ('Infinity',u'\u221e'), + ('Lozenge',u'\u25ca'), + ('Math divide',u'\u00f7'), + ('Math empty',u'\u2205'), + ('Math equals',u'='), + ('Math minus',u'\u2212'), + ('Math plus circled',u'\u2295'), + ('Math times circled',u'\u2297'), + ('Math times',u'\u00d7'), + ('O slash',u'\u00d8'), + ('Paragraph',u'\u00b6'), + ('Percent',u'%'), + ('Plus-or-minus',u'\u00b1'), + ('Plus',u'+'), + ('Punctuation colon',u':'), + ('Punctuation colon-semi',u';'), + ('Punctuation exclamation',u'!'), + ('Punctuation question',u'?'), + ('Punctuation period',u'.'), + ('Punctuation slash back',u'\\'), + ('Punctuation slash forward',u'/'), + ('Section',u'\u00a7'), + ('Tilde',u'~'), + ('Vertical bar',u'|'), + ('Vertical bar broken',u'\u00a6'), + ('_0',u'0'), + ('_1',u'1'), + ('_2',u'2'), + ('_3',u'3'), + ('_4',u'4'), + ('_5',u'5'), + ('_6',u'6'), + ('_7',u'7'), + ('_8',u'8'), + ('_9',u'9'), + ('_A',u'A'), + ('_B',u'B'), + ('_C',u'C'), + ('_D',u'D'), + ('_E',u'E'), + ('_F',u'F'), + ('_G',u'G'), + ('_H',u'H'), + ('_I',u'I'), + ('_J',u'J'), + ('_K',u'K'), + ('_L',u'L'), + ('_M',u'M'), + ('_N',u'N'), + ('_O',u'O'), + ('_P',u'P'), + ('_Q',u'Q'), + ('_R',u'R'), + ('_S',u'S'), + ('_T',u'T'), + ('_U',u'U'), + ('_V',u'V'), + ('_W',u'W'), + ('_X',u'X'), + ('_Y',u'Y'), + ('_Z',u'Z'), + ('_a',u'a'), + ('_b',u'b'), + ('_c',u'c'), + ('_d',u'd'), + ('_e',u'e'), + ('_f',u'f'), + ('_g',u'g'), + ('_h',u'h'), + ('_i',u'i'), + ('_j',u'j'), + ('_k',u'k'), + ('_l',u'l'), + ('_m',u'm'), + ('_n',u'n'), + ('_o',u'o'), + ('_p',u'p'), + ('_q',u'q'), + ('_r',u'r'), + ('_s',u's'), + ('_t',u't'), + ('_u',u'u'), + ('_v',u'v'), + ('_w',u'w'), + ('_x',u'x'), + ('_y',u'y'), + ('_z',u'z'), + ] + raw_prefix_list = sorted(raw_prefix_list, key=prefix_sorter) + self.prefixes = [x[1] for x in raw_prefix_list] + def prefix_rules_get_data(self): data_items = [] for row in range(self.prefix_rules_tw.rowCount()): @@ -625,6 +545,42 @@ class PluginWidget(QWidget,Ui_Form): 'prefix':data['prefix']}) return data_items + def prefix_rules_initialize(self, prefix_rules): + # Assign icons to buttons, hook clicks + self.move_rule_up_tb.setToolTip('Move rule up') + self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) + self.move_rule_up_tb.clicked.connect(self.prefix_rules_move_row_up) + + self.add_rule_tb.setToolTip('Add a new rule') + self.add_rule_tb.setIcon(QIcon(I('plus.png'))) + self.add_rule_tb.clicked.connect(self.prefix_rules_add_row) + + self.delete_rule_tb.setToolTip('Delete selected rule') + self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) + self.delete_rule_tb.clicked.connect(self.prefix_rules_delete_row) + + self.move_rule_down_tb.setToolTip('Move rule down') + self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) + self.move_rule_down_tb.clicked.connect(self.prefix_rules_move_row_down) + + # Configure the QTableWidget + # enabled/prefix/rule name/source field/pattern + self.prefix_rules_tw.clear() + header_labels = ['','Name','Prefix','Source','Pattern'] + self.prefix_rules_tw.setColumnCount(len(header_labels)) + self.prefix_rules_tw.setHorizontalHeaderLabels(header_labels) + self.prefix_rules_tw.setSortingEnabled(False) + self.prefix_rules_tw.setSelectionBehavior(QAbstractItemView.SelectRows) + + # Generate the prefix list + self.prefix_rules_generate_prefix_list() + + # Populate the table + self.prefix_rules_populate(prefix_rules) + self.prefix_rules_tw.resizeColumnsToContents() + self.prefix_rules_resize_name(1.5) + self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) + def prefix_rules_move_row_down(self): self.prefix_rules_tw.setFocus() rows = self.prefix_rules_tw.selectionModel().selectedRows() @@ -649,7 +605,7 @@ class PluginWidget(QWidget,Ui_Form): self.prefix_rules_tw.insertRow(src_row) # Populate it with the saved data - self.populate_prefix_rules_table_row(src_row, saved_data) + self.prefix_rules_populate_table_row(src_row, saved_data) self.blockSignals(False) scroll_to_row = last_sel_row + 1 if scroll_to_row < self.prefix_rules_tw.rowCount() - 1: @@ -671,7 +627,7 @@ class PluginWidget(QWidget,Ui_Form): # Add a row below us with the source data self.prefix_rules_tw.insertRow(selrow.row() + 1) - self.populate_prefix_rules_table_row(selrow.row() + 1, saved_data) + self.prefix_rules_populate_table_row(selrow.row() + 1, saved_data) # Delete the row above self.prefix_rules_tw.removeRow(selrow.row() - 1) @@ -682,6 +638,63 @@ class PluginWidget(QWidget,Ui_Form): scroll_to_row = scroll_to_row - 1 self.prefix_rules_tw.scrollToItem(self.prefix_rules_tw.item(scroll_to_row, 0)) + def prefix_rules_populate(self,rules): + # Format of rules list is different if default values vs retrieved JSON + # Hack to normalize list style + if type(rules[0]) is list: + rules = rules[0] + self.prefix_rules_tw.setFocus() + rules = sorted(rules, key=lambda k: k['ordinal']) + for row, rule in enumerate(rules): + self.prefix_rules_tw.insertRow(row) + self.prefix_rules_select_and_scroll_to_row(row) + self.prefix_rules_populate_table_row(row, rule) + self.prefix_rules_tw.selectRow(0) + + def prefix_rules_populate_table_row(self, row, data): + + def set_prefix_field_in_row(row, col, field=''): + prefix_combo = PrefixRulesComboBox(self, self.prefixes, field) + self.prefix_rules_tw.setCellWidget(row, col, prefix_combo) + + def set_rule_name_in_row(row, col, name=''): + rule_name = QLineEdit(name) + rule_name.home(False) + rule_name.editingFinished.connect(self.prefix_rules_rule_name_edited) + self.prefix_rules_tw.setCellWidget(row, col, rule_name) + + def set_source_field_in_row(row, col, field=''): + source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) + source_combo.currentIndexChanged.connect(partial(self.prefix_rules_source_index_changed, source_combo, row)) + #source_combo.currentIndexChanged.connect(self.prefix_rules_source_index_changed, source_combo, row) + self.prefix_rules_tw.setCellWidget(row, col, source_combo) + return source_combo + + + # Entry point + self.prefix_rules_tw.blockSignals(True) + #print("prefix_rules_populate_table_row processing rule:\n%s\n" % data) + + # Column 0: Enabled + self.prefix_rules_tw.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) + + # Column 1: Rule name + #rule_name = QTableWidgetItem(data['name']) + #self.prefix_rules_tw.setItem(row, 1, rule_name) + set_rule_name_in_row(row, 1, name=data['name']) + + # Column 2: Prefix + set_prefix_field_in_row(row, 2, field=data['prefix']) + + # Column 3: Source field + source_combo = set_source_field_in_row(row, 3, field=data['field']) + + # Column 4: Pattern + # The contents of the Pattern field is driven by the Source field + self.prefix_rules_source_index_changed(source_combo, row, 4, pattern=data['pattern']) + + self.prefix_rules_tw.blockSignals(False) + def prefix_rules_resize_name(self, scale): current_width = self.prefix_rules_tw.columnWidth(1) self.prefix_rules_tw.setColumnWidth(1, min(225,int(current_width * scale))) @@ -719,6 +732,7 @@ class PluginWidget(QWidget,Ui_Form): values_combo = PrefixRulesComboBox(self, values, pattern) self.prefix_rules_tw.setCellWidget(row, 4, values_combo) + def read_source_field_changed(self,new_index): ''' Process changes in the read_source_field combo box @@ -873,5 +887,3 @@ class PrefixRulesComboBox(NoWheelComboBox): self.setCurrentIndex(idx) else: self.setCurrentIndex(0) - - diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 31c5063a12..eb39415a81 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -107,10 +107,11 @@ - <p>Default pattern -\[.+\] -excludes tags of the form [tag], -e.g., [Project Gutenberg]</p> + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html> Excluded genres @@ -333,6 +334,9 @@ Default: ~,Catalog 0 + + The first matching rule will be used to add a prefix to book listings in the generated catalog. + Prefix rules diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index edb9567d93..6cc94c0e08 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -3117,32 +3117,24 @@ Author '{0}': def discoverPrefix(self, record): ''' - Given a field:pattern spec, discover if this book marked as read - - if field == tag, scan tags for pattern - if custom field, try regex match for pattern - This allows maximum flexibility with fields of type - datatype bool: #field_name:True - datatype text: #field_name: - datatype datetime: #field_name:.* - + Evaluate conditions for including prefixes in various listings ''' - if self.opts.verbose: - self.opts.log.info("\tevaluating %s (%s)" % (record['title'], record['authors'])) + def log_prefix_rule_match_info(rule, record): + self.opts.log.info(" %s %s by %s (Prefix rule '%s': %s:%s)" % + (rule['prefix'],record['title'], + record['authors'][0], rule['name'], + rule['field'],rule['pattern'])) + # Compare the record to each rule looking for a match for rule in self.prefixRules: - if self.opts.verbose: - self.opts.log.info("\t prefix_rule '%s': %s" % (rule['name'], rule)) - # Literal comparison for Tags field if rule['field'].lower() == 'tags': if rule['pattern'].lower() in map(unicode.lower,record['tags']): if self.opts.verbose: - self.opts.log.info("\t '%s' found in '%s' (%s)" % - (rule['pattern'], rule['field'], rule['name'])) + log_prefix_rule_match_info(rule, record) return rule['prefix'] - # Regex comparison for custom field + # Regex match for custom field elif rule['field'].startswith('#'): field_contents = self.__db.get_field(record['id'], rule['field'], @@ -3150,30 +3142,23 @@ Author '{0}': if field_contents == '': field_contents = None - if self.opts.verbose: - self.opts.log.info("\t field_contents: %s" % repr(field_contents)) - if field_contents is not None: try: if re.search(rule['pattern'], unicode(field_contents), re.IGNORECASE) is not None: if self.opts.verbose: - self.opts.log.info("\t '%s' matches '%s' value (%s)" % - (rule['pattern'], rule['field'], rule['name'])) + log_prefix_rule_match_info(rule, record) return rule['prefix'] except: # Compiling of pat failed, ignore it if self.opts.verbose: - self.opts.log.info("pattern failed to compile: %s" % rule['pattern']) + self.opts.log.error("pattern failed to compile: %s" % rule['pattern']) pass elif field_contents is None and rule['pattern'] == 'None': if self.opts.verbose: - self.opts.log.info("\t '%s' found in '%s' (%s)" % - (rule['pattern'], rule['field'], rule['name'])) + log_prefix_rule_match_info(rule, record) return rule['prefix'] - if False and self.opts.verbose: - self.opts.log.info("\t No prefix match found") return None def filterDbTags(self, tags): From a3a599372ae909d23de2d7423b66feec0c4a2759 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 5 Aug 2012 14:48:02 +0200 Subject: [PATCH 08/27] Make setting pubdate using search/replace more robust. Prevent setting "timestamp". --- src/calibre/gui2/dialogs/metadata_bulk.py | 2 +- src/calibre/library/database2.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 2a18c27a87..5c90e5dddd 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -414,7 +414,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): and fm[f].get('search_terms', None) and f not in ['formats', 'ondevice']) or (fm[f]['datatype'] in ['int', 'float', 'bool', 'datetime'] and - f not in ['id'])): + f not in ['id', 'timestamp'])): self.all_fields.append(f) self.writable_fields.append(f) if fm[f]['datatype'] == 'composite': diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 8a9152eb48..03f7034793 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -31,7 +31,7 @@ from calibre.ptempfile import (PersistentTemporaryFile, from calibre.customize.ui import run_plugins_on_import from calibre import isbytestring from calibre.utils.filenames import ascii_filename -from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp +from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp, parse_date from calibre.utils.config import prefs, tweaks, from_json, to_json from calibre.utils.icu import sort_key, strcmp, lower from calibre.utils.search_query_parser import saved_searches, set_saved_searches @@ -2479,6 +2479,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def set_pubdate(self, id, dt, notify=True, commit=True): if dt: + if isinstance(dt, (str, unicode, bytes)): + dt = parse_date(dt) self.conn.execute('UPDATE books SET pubdate=? WHERE id=?', (dt, id)) self.data.set(id, self.FIELD_MAP['pubdate'], dt, row_is_id=True) self.dirtied([id], commit=False) From 91b700bd6f52366ea27f6d04e89d098b7c345768 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 5 Aug 2012 19:28:09 +0530 Subject: [PATCH 09/27] Start work on a linux MTP driver --- setup/extensions.py | 14 + src/calibre/constants.py | 2 + src/calibre/devices/interface.py | 2 +- src/calibre/devices/mtp/__init__.py | 11 + src/calibre/devices/mtp/base.py | 34 + src/calibre/devices/mtp/unix/__init__.py | 14 + src/calibre/devices/mtp/unix/detect.py | 71 + src/calibre/devices/mtp/unix/devices.c | 16 + src/calibre/devices/mtp/unix/devices.h | 22 + src/calibre/devices/mtp/unix/driver.py | 81 + src/calibre/devices/mtp/unix/libmtp.c | 318 +++ .../devices/mtp/unix/upstream/device-flags.h | 329 +++ .../devices/mtp/unix/upstream/music-players.h | 1792 +++++++++++++++++ .../devices/mtp/unix/upstream/update.py | 20 + src/calibre/devices/scanner.py | 49 +- 15 files changed, 2764 insertions(+), 11 deletions(-) create mode 100644 src/calibre/devices/mtp/__init__.py create mode 100644 src/calibre/devices/mtp/base.py create mode 100644 src/calibre/devices/mtp/unix/__init__.py create mode 100644 src/calibre/devices/mtp/unix/detect.py create mode 100644 src/calibre/devices/mtp/unix/devices.c create mode 100644 src/calibre/devices/mtp/unix/devices.h create mode 100644 src/calibre/devices/mtp/unix/driver.py create mode 100644 src/calibre/devices/mtp/unix/libmtp.c create mode 100644 src/calibre/devices/mtp/unix/upstream/device-flags.h create mode 100644 src/calibre/devices/mtp/unix/upstream/music-players.h create mode 100644 src/calibre/devices/mtp/unix/upstream/update.py diff --git a/setup/extensions.py b/setup/extensions.py index 4dd76be3a6..964d4d9839 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -174,6 +174,20 @@ if isosx: ldflags=['-framework', 'IOKit']) ) +if islinux: + extensions.append(Extension('libmtp', + [ + 'calibre/devices/mtp/unix/devices.c', + 'calibre/devices/mtp/unix/libmtp.c' + ], + headers=[ + 'calibre/devices/mtp/unix/devices.h', + 'calibre/devices/mtp/unix/upstream/music-players.h', + 'calibre/devices/mtp/unix/upstream/device-flags.h', + ], + libraries=['mtp'] + )) + if isunix: cc = os.environ.get('CC', 'gcc') cxx = os.environ.get('CXX', 'g++') diff --git a/src/calibre/constants.py b/src/calibre/constants.py index b626406f84..63119c5363 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -93,6 +93,8 @@ class Plugins(collections.Mapping): plugins.append('winutil') if isosx: plugins.append('usbobserver') + if islinux: + plugins.append('libmtp') self.plugins = frozenset(plugins) def load_plugin(self, name): diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index c9e62bcfca..fc4e5a2a60 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -199,7 +199,7 @@ class DevicePlugin(Plugin): # }}} def reset(self, key='-1', log_packets=False, report_progress=None, - detected_device=None) : + detected_device=None): """ :param key: The key to unlock the device :param log_packets: If true the packet stream to/from the device is logged diff --git a/src/calibre/devices/mtp/__init__.py b/src/calibre/devices/mtp/__init__.py new file mode 100644 index 0000000000..dd9615356c --- /dev/null +++ b/src/calibre/devices/mtp/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + + diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py new file mode 100644 index 0000000000..04dd2a0034 --- /dev/null +++ b/src/calibre/devices/mtp/base.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from calibre.devices.interface import DevicePlugin +from calibre.devices.usbms.deviceconfig import DeviceConfig + +class MTPDeviceBase(DeviceConfig, DevicePlugin): + name = 'SmartDevice App Interface' + gui_name = _('MTP Device') + icon = I('devices/galaxy_s3.png') + description = _('Communicate with MTP devices') + author = 'Kovid Goyal' + version = (1, 0, 0) + + # Invalid USB vendor information so the scanner will never match + VENDOR_ID = [0xffff] + PRODUCT_ID = [0xffff] + BCD = [0xffff] + + THUMBNAIL_HEIGHT = 128 + CAN_SET_METADATA = [] + + BACKLOADING_ERROR_MESSAGE = None + + def reset(self, key='-1', log_packets=False, report_progress=None, + detected_device=None): + pass + diff --git a/src/calibre/devices/mtp/unix/__init__.py b/src/calibre/devices/mtp/unix/__init__.py new file mode 100644 index 0000000000..211ff58986 --- /dev/null +++ b/src/calibre/devices/mtp/unix/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +''' +libmtp based drivers for MTP devices on Unix like platforms. +''' + diff --git a/src/calibre/devices/mtp/unix/detect.py b/src/calibre/devices/mtp/unix/detect.py new file mode 100644 index 0000000000..9e913dd9cf --- /dev/null +++ b/src/calibre/devices/mtp/unix/detect.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from calibre.constants import plugins + +class MTPDetect(object): + + def __init__(self): + p = plugins['libmtp'] + self.libmtp = p[0] + if self.libmtp is None: + print ('Failed to load libmtp, MTP device detection disabled') + print (p[1]) + self.cache = {} + + def __call__(self, devices): + ''' + Given a list of devices as returned by LinuxScanner, return the set of + devices that are likely to be MTP devices. This class maintains a cache + to minimize USB polling. Note that detection is partially based on a + list of known vendor and product ids. This is because polling some + older devices causes problems. Therefore, if this method identifies a + device as MTP, it is not actually guaranteed that it will be a working + MTP device. + ''' + # First drop devices that have been disconnected from the cache + connected_devices = {(d.busnum, d.devnum, d.vendor_id, d.product_id, + d.bcd, d.serial) for d in devices} + for d in tuple(self.cache.iterkeys()): + if d not in connected_devices: + del self.cache[d] + + # Since is_mtp_device() can cause USB traffic by probing the device, we + # cache its result + mtp_devices = set() + if self.libmtp is None: + return mtp_devices + + for d in devices: + ans = self.cache.get((d.busnum, d.devnum, d.vendor_id, d.product_id, + d.bcd, d.serial), None) + if ans is None: + ans = self.libmtp.is_mtp_device(d.busnum, d.devnum, + d.vendor_id, d.product_id) + self.cache[(d.busnum, d.devnum, d.vendor_id, d.product_id, + d.bcd, d.serial)] = ans + if ans: + mtp_devices.add(d) + return mtp_devices + + def create_device(self, connected_device): + d = connected_device + return self.libmtp.Device(d.busnum, d.devnum, d.vendor_id, + d.product_id, d.manufacturer, d.product, d.serial) + +if __name__ == '__main__': + from calibre.devices.scanner import linux_scanner + mtp_detect = MTPDetect() + devs = mtp_detect(linux_scanner()) + print ('Found %d MTP devices:'%len(devs)) + for dev in devs: + print (dev, 'at busnum=%d and devnum=%d'%(dev.busnum, dev.devnum)) + print() + + diff --git a/src/calibre/devices/mtp/unix/devices.c b/src/calibre/devices/mtp/unix/devices.c new file mode 100644 index 0000000000..3e461fae42 --- /dev/null +++ b/src/calibre/devices/mtp/unix/devices.c @@ -0,0 +1,16 @@ +/* + * devices.c + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the MIT license. + */ + +#include "upstream/device-flags.h" +#include "devices.h" + +const calibre_device_entry_t calibre_mtp_device_table[] = { +#include "upstream/music-players.h" + + , { NULL, 0xffff, NULL, 0xffff, DEVICE_FLAG_NONE } +}; + diff --git a/src/calibre/devices/mtp/unix/devices.h b/src/calibre/devices/mtp/unix/devices.h new file mode 100644 index 0000000000..4618bbae06 --- /dev/null +++ b/src/calibre/devices/mtp/unix/devices.h @@ -0,0 +1,22 @@ +#pragma once +/* + * devices.h + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the MIT license. + */ +#include +#include + +struct calibre_device_entry_struct { + char *vendor; /**< The vendor of this device */ + uint16_t vendor_id; /**< Vendor ID for this device */ + char *product; /**< The product name of this device */ + uint16_t product_id; /**< Product ID for this device */ + uint32_t device_flags; /**< Bugs, device specifics etc */ +}; + +typedef struct calibre_device_entry_struct calibre_device_entry_t; + +extern const calibre_device_entry_t calibre_mtp_device_table[]; + diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py new file mode 100644 index 0000000000..8336be9fc2 --- /dev/null +++ b/src/calibre/devices/mtp/unix/driver.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import time +from threading import RLock +from functools import wraps + +from calibre.devices.errors import OpenFailed +from calibre.devices.mtp.base import MTPDeviceBase +from calibre.devices.mtp.unix.detect import MTPDetect + +def synchronous(func): + @wraps(func) + def synchronizer(self, *args, **kwargs): + with self.lock: + return func(self, *args, **kwargs) + return synchronizer + +class MTP_DEVICE(MTPDeviceBase): + + supported_platforms = ['linux'] + + def __init__(self, *args, **kwargs): + MTPDeviceBase.__init__(self, *args, **kwargs) + self.detect = MTPDetect() + self.dev = None + self.lock = RLock() + self.blacklisted_devices = set() + + @synchronous + def is_usb_connected(self, devices_on_system, debug=False, + only_presence=False): + + # First remove blacklisted devices. + devs = [] + for d in devices_on_system: + if (d.busnum, d.devnum, d.vendor_id, + d.product_id, d.bcd, d.serial) not in self.blacklisted_devices: + devs.append(d) + + devs = self.detect(devs) + if self.dev is not None: + # Check if the currently opened device is still connected + ids = self.dev.ids + found = False + for d in devs: + if ( (d.busnum, d.devnum, d.vendor_id, d.product_id, d.serial) + == ids ): + found = True + break + return found + # Check if any MTP capable device is present + return len(devs) > 0 + + @synchronous + def post_yank_cleanup(self): + self.dev = None + + @synchronous + def open(self, connected_device, library_uuid): + try: + self.detect.create_device(connected_device) + except ValueError: + # Give the device some time to settle + time.sleep(2) + try: + self.detect.create_device(connected_device) + except ValueError: + # Black list this device so that it is ignored for the + # remainder of this session. + d = connected_device + self.blacklisted_devices.add((d.busnum, d.devnum, d.vendor_id, + d.product_id, d.bcd, d.serial)) + raise OpenFailed('%s is not a MTP device'%connected_device) + diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c new file mode 100644 index 0000000000..0a818cfc66 --- /dev/null +++ b/src/calibre/devices/mtp/unix/libmtp.c @@ -0,0 +1,318 @@ +#define UNICODE +#include + +#include +#include + +#include "devices.h" + +// Device object definition {{{ +typedef struct { + PyObject_HEAD + // Type-specific fields go here. + LIBMTP_mtpdevice_t *device; + PyObject *ids; + PyObject *friendly_name; + PyObject *manufacturer_name; + PyObject *model_name; + PyObject *serial_number; + PyObject *device_version; + +} libmtp_Device; + +static void +libmtp_Device_dealloc(libmtp_Device* self) +{ + if (self->device != NULL) LIBMTP_Release_Device(self->device); + self->device = NULL; + + Py_XDECREF(self->ids); self->ids = NULL; + Py_XDECREF(self->friendly_name); self->friendly_name = NULL; + Py_XDECREF(self->manufacturer_name); self->manufacturer_name = NULL; + Py_XDECREF(self->model_name); self->model_name = NULL; + Py_XDECREF(self->serial_number); self->serial_number = NULL; + Py_XDECREF(self->device_version); self->device_version = NULL; + + self->ob_type->tp_free((PyObject*)self); +} + +static int +libmtp_Device_init(libmtp_Device *self, PyObject *args, PyObject *kwds) +{ + int busnum, devnum, vendor_id, product_id; + PyObject *usb_serialnum; + char *vendor, *product, *friendly_name, *manufacturer_name, *model_name, *serial_number, *device_version; + LIBMTP_raw_device_t rawdev; + LIBMTP_mtpdevice_t *dev; + size_t i; + + if (!PyArg_ParseTuple(args, "iiiissO", &busnum, &devnum, &vendor_id, &product_id, &vendor, &product, &usb_serialnum)) return -1; + + if (devnum < 0 || devnum > 255 || busnum < 0) { PyErr_SetString(PyExc_TypeError, "Invalid busnum/devnum"); return -1; } + + self->ids = Py_BuildValue("iiiiO", busnum, devnum, vendor_id, product_id, usb_serialnum); + if (self->ids == NULL) return -1; + + rawdev.bus_location = (uint32_t)busnum; + rawdev.devnum = (uint8_t)devnum; + rawdev.device_entry.vendor = vendor; + rawdev.device_entry.product = product; + rawdev.device_entry.vendor_id = vendor_id; + rawdev.device_entry.product_id = product_id; + rawdev.device_entry.device_flags = 0x00000000U; + + Py_BEGIN_ALLOW_THREADS; + for (i = 0; ; i++) { + if (calibre_mtp_device_table[i].vendor == NULL && calibre_mtp_device_table[i].product == NULL && calibre_mtp_device_table[i].vendor_id == 0xffff) break; + if (calibre_mtp_device_table[i].vendor_id == vendor_id && calibre_mtp_device_table[i].product_id == product_id) { + rawdev.device_entry.device_flags = calibre_mtp_device_table[i].device_flags; + } + } + + dev = LIBMTP_Open_Raw_Device_Uncached(&rawdev); + Py_END_ALLOW_THREADS; + + if (dev == NULL) { + PyErr_SetString(PyExc_ValueError, "Unable to open raw device."); + return -1; + } + + self->device = dev; + + Py_BEGIN_ALLOW_THREADS; + friendly_name = LIBMTP_Get_Friendlyname(self->device); + manufacturer_name = LIBMTP_Get_Manufacturername(self->device); + model_name = LIBMTP_Get_Modelname(self->device); + serial_number = LIBMTP_Get_Serialnumber(self->device); + device_version = LIBMTP_Get_Deviceversion(self->device); + Py_END_ALLOW_THREADS; + + if (friendly_name != NULL) { + self->friendly_name = PyUnicode_FromString(friendly_name); + free(friendly_name); + } + if (self->friendly_name == NULL) { self->friendly_name = Py_None; Py_INCREF(Py_None); } + + if (manufacturer_name != NULL) { + self->manufacturer_name = PyUnicode_FromString(manufacturer_name); + free(manufacturer_name); + } + if (self->manufacturer_name == NULL) { self->manufacturer_name = Py_None; Py_INCREF(Py_None); } + + if (model_name != NULL) { + self->model_name = PyUnicode_FromString(model_name); + free(model_name); + } + if (self->model_name == NULL) { self->model_name = Py_None; Py_INCREF(Py_None); } + + if (serial_number != NULL) { + self->serial_number = PyUnicode_FromString(serial_number); + free(serial_number); + } + if (self->serial_number == NULL) { self->serial_number = Py_None; Py_INCREF(Py_None); } + + if (device_version != NULL) { + self->device_version = PyUnicode_FromString(device_version); + free(device_version); + } + if (self->device_version == NULL) { self->device_version = Py_None; Py_INCREF(Py_None); } + + return 0; +} + +// Collator.friendly_name {{{ +static PyObject * +libmtp_Device_friendly_name(libmtp_Device *self, void *closure) { + return Py_BuildValue("O", self->friendly_name); +} // }}} + +// Collator.manufacturer_name {{{ +static PyObject * +libmtp_Device_manufacturer_name(libmtp_Device *self, void *closure) { + return Py_BuildValue("O", self->manufacturer_name); +} // }}} + +// Collator.model_name {{{ +static PyObject * +libmtp_Device_model_name(libmtp_Device *self, void *closure) { + return Py_BuildValue("O", self->model_name); +} // }}} + +// Collator.serial_number {{{ +static PyObject * +libmtp_Device_serial_number(libmtp_Device *self, void *closure) { + return Py_BuildValue("O", self->serial_number); +} // }}} + +// Collator.device_version {{{ +static PyObject * +libmtp_Device_device_version(libmtp_Device *self, void *closure) { + return Py_BuildValue("O", self->device_version); +} // }}} + +// Collator.ids {{{ +static PyObject * +libmtp_Device_ids(libmtp_Device *self, void *closure) { + return Py_BuildValue("O", self->ids); +} // }}} + +static PyMethodDef libmtp_Device_methods[] = { + {NULL} /* Sentinel */ +}; + +static PyGetSetDef libmtp_Device_getsetters[] = { + {(char *)"friendly_name", + (getter)libmtp_Device_friendly_name, NULL, + (char *)"The friendly name of this device, can be None.", + NULL}, + + {(char *)"manufacturer_name", + (getter)libmtp_Device_manufacturer_name, NULL, + (char *)"The manufacturer name of this device, can be None.", + NULL}, + + {(char *)"model_name", + (getter)libmtp_Device_model_name, NULL, + (char *)"The model name of this device, can be None.", + NULL}, + + {(char *)"serial_number", + (getter)libmtp_Device_serial_number, NULL, + (char *)"The serial number of this device, can be None.", + NULL}, + + {(char *)"device_version", + (getter)libmtp_Device_device_version, NULL, + (char *)"The device version of this device, can be None.", + NULL}, + + {(char *)"ids", + (getter)libmtp_Device_ids, NULL, + (char *)"The ids of the device (busnum, devnum, vendor_id, product_id, usb_serialnum)", + NULL}, + + {NULL} /* Sentinel */ +}; + +static PyTypeObject libmtp_DeviceType = { // {{{ + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "libmtp.Device", /*tp_name*/ + sizeof(libmtp_Device), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)libmtp_Device_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "Device", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + libmtp_Device_methods, /* tp_methods */ + 0, /* tp_members */ + libmtp_Device_getsetters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)libmtp_Device_init, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ +}; // }}} + +// }}} End Device object definition + +static PyObject * +libmtp_set_debug_level(PyObject *self, PyObject *args) { + int level; + if (!PyArg_ParseTuple(args, "i", &level)) return NULL; + LIBMTP_Set_Debug(level); + Py_RETURN_NONE; +} + + +static PyObject * +libmtp_is_mtp_device(PyObject *self, PyObject *args) { + int busnum, devnum, vendor_id, prod_id, ans = 0; + size_t i; + + if (!PyArg_ParseTuple(args, "iiii", &busnum, &devnum, &vendor_id, &prod_id)) return NULL; + + for (i = 0; ; i++) { + if (calibre_mtp_device_table[i].vendor == NULL && calibre_mtp_device_table[i].product == NULL && calibre_mtp_device_table[i].vendor_id == 0xffff) break; + if (calibre_mtp_device_table[i].vendor_id == vendor_id && calibre_mtp_device_table[i].product_id == prod_id) { + Py_RETURN_TRUE; + } + } + + /* + * LIBMTP_Check_Specific_Device does not seem to work at least on my linux + * system. Need to investigate why later. Most devices are in the device + * table so this is not terribly important. + */ + /* LIBMTP_Set_Debug(LIBMTP_DEBUG_ALL); */ + /* printf("Calling check: %d %d\n", busnum, devnum); */ + Py_BEGIN_ALLOW_THREADS; + ans = LIBMTP_Check_Specific_Device(busnum, devnum); + Py_END_ALLOW_THREADS; + + if (ans) Py_RETURN_TRUE; + + Py_RETURN_FALSE; + +} + +static PyMethodDef libmtp_methods[] = { + {"set_debug_level", libmtp_set_debug_level, METH_VARARGS, + "set_debug_level(level)\n\nSet the debug level bit mask, see LIBMTP_DEBUG_* constants." + }, + + {"is_mtp_device", libmtp_is_mtp_device, METH_VARARGS, + "is_mtp_device(busnum, devnum, vendor_id, prod_id)\n\nReturn True if the device is recognized as an MTP device by its vendor/product ids. If it is not recognized a probe is done and True returned if the probe succeeds. Note that probing can cause some devices to malfunction, and it is not very reliable, which is why we prefer to use the device database." + }, + + {NULL, NULL, 0, NULL} +}; + + +PyMODINIT_FUNC +initlibmtp(void) { + PyObject *m; + + libmtp_DeviceType.tp_new = PyType_GenericNew; + if (PyType_Ready(&libmtp_DeviceType) < 0) + return; + + m = Py_InitModule3("libmtp", libmtp_methods, "Interface to libmtp."); + if (m == NULL) return; + + LIBMTP_Init(); + LIBMTP_Set_Debug(LIBMTP_DEBUG_NONE); + + Py_INCREF(&libmtp_DeviceType); + PyModule_AddObject(m, "Device", (PyObject *)&libmtp_DeviceType); + + PyModule_AddStringMacro(m, LIBMTP_VERSION_STRING); + PyModule_AddIntMacro(m, LIBMTP_DEBUG_NONE); + PyModule_AddIntMacro(m, LIBMTP_DEBUG_PTP); + PyModule_AddIntMacro(m, LIBMTP_DEBUG_PLST); + PyModule_AddIntMacro(m, LIBMTP_DEBUG_USB); + PyModule_AddIntMacro(m, LIBMTP_DEBUG_DATA); + PyModule_AddIntMacro(m, LIBMTP_DEBUG_ALL); +} diff --git a/src/calibre/devices/mtp/unix/upstream/device-flags.h b/src/calibre/devices/mtp/unix/upstream/device-flags.h new file mode 100644 index 0000000000..06ef62aa1c --- /dev/null +++ b/src/calibre/devices/mtp/unix/upstream/device-flags.h @@ -0,0 +1,329 @@ +/** + * \file device-flags.h + * Special device flags to deal with bugs in specific devices. + * + * Copyright (C) 2005-2007 Richard A. Low + * Copyright (C) 2005-2012 Linus Walleij + * Copyright (C) 2006-2007 Marcus Meissner + * Copyright (C) 2007 Ted Bullock + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * This file is supposed to be included by both libmtp and libgphoto2. + */ + +/** + * These flags are used to indicate if some or other + * device need special treatment. These should be possible + * to concatenate using logical OR so please use one bit per + * feature and lets pray we don't need more than 32 bits... + */ +#define DEVICE_FLAG_NONE 0x00000000 +/** + * This means that the PTP_OC_MTP_GetObjPropList is broken + * in the sense that it won't return properly formatted metadata + * for ALL files on the device when you request an object + * property list for object 0xFFFFFFFF with parameter 3 likewise + * set to 0xFFFFFFFF. Compare to + * DEVICE_FLAG_BROKEN_MTPGETOBJECTPROPLIST which only signify + * that it's broken when getting metadata for a SINGLE object. + * A typical way the implementation may be broken is that it + * may not return a proper count of the objects, and sometimes + * (like on the ZENs) objects are simply missing from the list + * if you use this. Sometimes it has been used incorrectly to + * mask bugs in the code (like handling transactions of data + * with size given to -1 (0xFFFFFFFFU), in that case please + * help us remove it now the code is fixed. Sometimes this is + * used because getting all the objects is just too slow and + * the USB transaction will time out if you use this command. + */ +#define DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL 0x00000001 +/** + * This means that under Linux, another kernel module may + * be using this device's USB interface, so we need to detach + * it if it is. Typically this is on dual-mode devices that + * will present both an MTP compliant interface and device + * descriptor *and* a USB mass storage interface. If the USB + * mass storage interface is in use, other apps (like our + * userspace libmtp through libusb access path) cannot get in + * and get cosy with it. So we can remove the offending + * application. Typically this means you have to run the program + * as root as well. + */ +#define DEVICE_FLAG_UNLOAD_DRIVER 0x00000002 +/** + * This means that the PTP_OC_MTP_GetObjPropList (9805) + * is broken in some way, either it doesn't work at all + * (as for Android devices) or it won't properly return all + * object properties if parameter 3 is set to 0xFFFFFFFFU. + */ +#define DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST 0x00000004 +/** + * This means the device doesn't send zero packets to indicate + * end of transfer when the transfer boundary occurs at a + * multiple of 64 bytes (the USB 1.1 endpoint size). Instead, + * exactly one extra byte is sent at the end of the transfer + * if the size is an integer multiple of USB 1.1 endpoint size + * (64 bytes). + * + * This behaviour is most probably a workaround due to the fact + * that the hardware USB slave controller in the device cannot + * handle zero writes at all, and the usage of the USB 1.1 + * endpoint size is due to the fact that the device will "gear + * down" on a USB 1.1 hub, and since 64 bytes is a multiple of + * 512 bytes, it will work with USB 1.1 and USB 2.0 alike. + */ +#define DEVICE_FLAG_NO_ZERO_READS 0x00000008 +/** + * This flag means that the device is prone to forgetting the + * OGG container file type, so that libmtp must look at the + * filename extensions in order to determine that a file is + * actually OGG. This is a clear and present firmware bug, and + * while firmware bugs should be fixed in firmware, we like + * OGG so much that we back it by introducing this flag. + * The error has only been seen on iriver devices. Turning this + * flag on won't hurt anything, just that the check against + * filename extension will be done for files of "unknown" type. + * If the player does not even know (reports) that it supports + * ogg even though it does, please use the stronger + * OGG_IS_UNKNOWN flag, which will forcedly support ogg on + * anything with the .ogg filename extension. + */ +#define DEVICE_FLAG_IRIVER_OGG_ALZHEIMER 0x00000010 +/** + * This flag indicates a limitation in the filenames a device + * can accept - they must be 7 bit (all chars <= 127/0x7F). + * It was found first on the Philips Shoqbox, and is a deviation + * from the PTP standard which mandates that any unicode chars + * may be used for filenames. I guess this is caused by a 7bit-only + * filesystem being used intrinsically on the device. + */ +#define DEVICE_FLAG_ONLY_7BIT_FILENAMES 0x00000020 +/** + * This flag indicates that the device will lock up if you + * try to get status of endpoints and/or release the interface + * when closing the device. This fixes problems with SanDisk + * Sansa devices especially. It may be a side-effect of a + * Windows behaviour of never releasing interfaces. + */ +#define DEVICE_FLAG_NO_RELEASE_INTERFACE 0x00000040 +/** + * This flag was introduced with the advent of Creative ZEN + * 8GB. The device sometimes return a broken PTP header + * like this: < 1502 0000 0200 01d1 02d1 01d2 > + * the latter 6 bytes (representing "code" and "transaction ID") + * contain junk. This is breaking the PTP/MTP spec but works + * on Windows anyway, probably because the Windows implementation + * does not check that these bytes are valid. To interoperate + * with devices like this, we need this flag to emulate the + * Windows bug. Broken headers has also been found in the + * Aricent MTP stack. + */ +#define DEVICE_FLAG_IGNORE_HEADER_ERRORS 0x00000080 +/** + * The Motorola RAZR2 V8 (others?) has broken set object + * proplist causing the metadata setting to fail. (The + * set object prop to set individual properties work on + * this device, but the metadata is plain ignored on + * tracks, though e.g. playlist names can be set.) + */ +#define DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST 0x00000100 +/** + * The Samsung YP-T10 think Ogg files shall be sent with + * the "unknown" (PTP_OFC_Undefined) file type, this gives a + * side effect that is a combination of the iRiver Ogg Alzheimer + * problem (have to recognized Ogg files on file extension) + * and a need to report the Ogg support (the device itself does + * not properly claim to support it) and need to set filetype + * to unknown when storing Ogg files, even though they're not + * actually unknown. Later iRivers seem to need this flag since + * they do not report to support OGG even though they actually + * do. Often the device supports OGG in USB mass storage mode, + * then the firmware simply miss to declare metadata support + * for OGG properly. + */ +#define DEVICE_FLAG_OGG_IS_UNKNOWN 0x00000200 +/** + * The Creative Zen is quite unstable in libmtp but seems to + * be better with later firmware versions. However, it still + * frequently crashes when setting album art dimensions. This + * flag disables setting the dimensions (which seems to make + * no difference to how the graphic is displayed). + */ +#define DEVICE_FLAG_BROKEN_SET_SAMPLE_DIMENSIONS 0x00000400 +/** + * Some devices, particularly SanDisk Sansas, need to always + * have their "OS Descriptor" probed in order to work correctly. + * This flag provides that extra massage. + */ +#define DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR 0x00000800 +/** + * Samsung has implimented its own playlist format as a .spl file + * stored in the normal file system, rather than a proper mtp + * playlist. There are multiple versions of the .spl format + * identified by a line in the file: VERSION X.XX + * Version 1.00 is just a simple playlist. + */ +#define DEVICE_FLAG_PLAYLIST_SPL_V1 0x00001000 +/** + * Samsung has implimented its own playlist format as a .spl file + * stored in the normal file system, rather than a proper mtp + * playlist. There are multiple versions of the .spl format + * identified by a line in the file: VERSION X.XX + * Version 2.00 is playlist but allows DNSe sound settings + * to be stored, per playlist. + */ +#define DEVICE_FLAG_PLAYLIST_SPL_V2 0x00002000 +/** + * The Sansa E250 is know to have this problem which is actually + * that the device claims that property PTP_OPC_DateModified + * is read/write but will still fail to update it. It can only + * be set properly the first time a file is sent. + */ +#define DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED 0x00004000 +/** + * This avoids use of the send object proplist which + * is used when creating new objects (not just updating) + * The DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST is related + * but only concerns the case where the object proplist + * is sent in to update an existing object. The Toshiba + * Gigabeat MEU202 for example has this problem. + */ +#define DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST 0x00008000 +/** + * Devices that cannot support reading out battery + * level. + */ +#define DEVICE_FLAG_BROKEN_BATTERY_LEVEL 0x00010000 + +/** + * Devices that send "ObjectDeleted" events after deletion + * of images. (libgphoto2) + */ +#define DEVICE_FLAG_DELETE_SENDS_EVENT 0x00020000 + +/** + * Cameras that can capture images. (libgphoto2) + */ +#define DEVICE_FLAG_CAPTURE 0x00040000 + +/** + * Cameras that can capture images. (libgphoto2) + */ +#define DEVICE_FLAG_CAPTURE_PREVIEW 0x00080000 + +/** + * Nikon broken capture support without proper ObjectAdded events. + * (libgphoto2) + */ +#define DEVICE_FLAG_NIKON_BROKEN_CAPTURE 0x00100000 + +/** + * Broken capture support where cameras do not send CaptureComplete events. + * (libgphoto2) + */ +#define DEVICE_FLAG_NO_CAPTURE_COMPLETE 0x00400000 + +/** + * Direct PTP match required. + * (libgphoto2) + */ +#define DEVICE_FLAG_MATCH_PTP_INTERFACE 0x00800000 +/** + * This flag is like DEVICE_FLAG_OGG_IS_UNKNOWN but for FLAC + * files instead. Using the unknown filetype for FLAC files. + */ +#define DEVICE_FLAG_FLAC_IS_UNKNOWN 0x01000000 +/** + * Device needs unique filenames, no two files can be + * named the same string. + */ +#define DEVICE_FLAG_UNIQUE_FILENAMES 0x02000000 +/** + * This flag performs some random magic on the BlackBerry + * device to switch from USB mass storage to MTP mode we think. + */ +#define DEVICE_FLAG_SWITCH_MODE_BLACKBERRY 0x04000000 +/** + * This flag indicates that the device need an extra long + * timeout on some operations. + */ +#define DEVICE_FLAG_LONG_TIMEOUT 0x08000000 +/** + * This flag indicates that the device need an explicit + * USB reset after each connection. Some devices don't + * like this, so it's not done by default. + */ +#define DEVICE_FLAG_FORCE_RESET_ON_CLOSE 0x10000000 +/** + * Early Creative Zen (etc) models actually only support + * command 9805 (Get object property list) and will hang + * if you try to get individual properties of an object. + */ +#define DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL 0x20000000 +/** + * It seems that some devices return an bad data when + * using the GetObjectInfo operation. So in these cases + * we prefer to override the PTP-compatible object infos + * with the MTP property list. + * + * For example Some Samsung Galaxy S devices contain an MTP + * stack that present the ObjectInfo in 64 bit instead of + * 32 bit. + */ +#define DEVICE_FLAG_PROPLIST_OVERRIDES_OI 0x40000000 + +/** + * All these bug flags need to be set on SONY NWZ Walkman + * players, and will be autodetected on unknown devices + * by detecting the vendor extension descriptor "sony.net" + */ +#define DEVICE_FLAGS_SONY_NWZ_BUGS \ + (DEVICE_FLAG_UNLOAD_DRIVER | \ + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | \ + DEVICE_FLAG_UNIQUE_FILENAMES | \ + DEVICE_FLAG_FORCE_RESET_ON_CLOSE ) +/** + * All these bug flags need to be set on Android devices, + * they claim to support MTP operations they actually + * cannot handle, especially 9805 (Get object property list). + * These are auto-assigned to devices reporting + * "android.com" in their device extension descriptor. + */ +#define DEVICE_FLAGS_ANDROID_BUGS \ + (DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | \ + DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST | \ + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST | \ + DEVICE_FLAG_UNLOAD_DRIVER | \ + DEVICE_FLAG_LONG_TIMEOUT ) +/** + * All these bug flags appear on a number of SonyEricsson + * devices including Android devices not using the stock + * Android 4.0+ (Ice Cream Sandwich) MTP stack. It is highly + * supected that these bugs comes from an MTP implementation + * from Aricent, so it is called the Aricent bug flags as a + * shorthand. Especially the header errors that need to be + * ignored is typical for this stack. + * + * After some guesswork we auto-assign these bug flags to + * devices that present the "microsoft.com/WPDNA", and + * "sonyericsson.com/SE" but NOT the "android.com" + * descriptor. + */ +#define DEVICE_FLAGS_ARICENT_BUGS \ + (DEVICE_FLAG_IGNORE_HEADER_ERRORS | \ + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST | \ + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST ) diff --git a/src/calibre/devices/mtp/unix/upstream/music-players.h b/src/calibre/devices/mtp/unix/upstream/music-players.h new file mode 100644 index 0000000000..f8f756df59 --- /dev/null +++ b/src/calibre/devices/mtp/unix/upstream/music-players.h @@ -0,0 +1,1792 @@ +/** + * \file music-players.h + * List of music players as USB ids. + * + * Copyright (C) 2005-2007 Richard A. Low + * Copyright (C) 2005-2012 Linus Walleij + * Copyright (C) 2006-2007 Marcus Meissner + * Copyright (C) 2007 Ted Bullock + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * This file is supposed to be included within a struct from both libmtp + * and libgphoto2. + * + * Information can be harvested from Windows driver .INF files, see: + * http://msdn.microsoft.com/en-us/library/aa973606.aspx + */ +/* + * MTP device list, trying real bad to get all devices into + * this list by stealing from everyone I know. + * Some devices taken from the Rockbox device listing: + * http://www.rockbox.org/twiki/bin/view/Main/DeviceDetection + */ + + /* + * Creative Technology and ZiiLABS + * Initially the Creative devices was all we supported so these are + * the most thoroughly tested devices. Presumably only the devices + * with older firmware (the ones that have 32bit object size) will + * need the DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL flag. This bug + * manifest itself when you have a lot of folders on the device, + * some of the folders will start to disappear when getting all objects + * and properties. + */ + { "Creative", 0x041e, "ZEN Vision", 0x411f, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "Portable Media Center", 0x4123, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "ZEN Xtra (MTP mode)", 0x4128, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Dell", 0x041e, "DJ (2nd generation)", 0x412f, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "ZEN Micro (MTP mode)", 0x4130, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "ZEN Touch (MTP mode)", 0x4131, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Dell", 0x041e, "Dell Pocket DJ (MTP mode)", 0x4132, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "ZEN MicroPhoto (alternate version)", 0x4133, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "ZEN Sleek (MTP mode)", 0x4137, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "ZEN MicroPhoto", 0x413c, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "ZEN Sleek Photo", 0x413d, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "ZEN Vision:M", 0x413e, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + // Reported by marazm@o2.pl + { "Creative", 0x041e, "ZEN V", 0x4150, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + // Reported by danielw@iinet.net.au + // This version of the Vision:M needs the no release interface flag, + // unclear whether the other version above need it too or not. + { "Creative", 0x041e, "ZEN Vision:M (DVP-HD0004)", 0x4151, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + // Reported by Darel on the XNJB forums + { "Creative", 0x041e, "ZEN V Plus", 0x4152, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + { "Creative", 0x041e, "ZEN Vision W", 0x4153, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + // Don't add 0x4155: this is a Zen Stone device which is not MTP + // Reported by Paul Kurczaba + { "Creative", 0x041e, "ZEN", 0x4157, + DEVICE_FLAG_IGNORE_HEADER_ERRORS | + DEVICE_FLAG_BROKEN_SET_SAMPLE_DIMENSIONS | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + // Reported by Ringofan + { "Creative", 0x041e, "ZEN V 2GB", 0x4158, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + // Reported by j norment + { "Creative", 0x041e, "ZEN Mozaic", 0x4161, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + // Reported by Aaron F. Gonzalez + { "Creative", 0x041e, "ZEN X-Fi", 0x4162, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + // Reported by farmerstimuli + { "Creative", 0x041e, "ZEN X-Fi 3", 0x4169, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_GET_OBJECT_PROPVAL }, + // Reported by Todor Gyumyushev + { "ZiiLABS", 0x041e, "Zii EGG", 0x6000, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR | + DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + + /* + * Samsung + * We suspect that more of these are dual mode. + * We suspect more of these might need DEVICE_FLAG_NO_ZERO_READS + * We suspect more of these might need DEVICE_FLAG_PLAYLIST_SPL_V1 + * or DEVICE_FLAG_PLAYLIST_SPL_V2 to get playlists working. + * YP-NEU, YP-NDU, YP-20, YP-800, YP-MF Series, YP-100, YP-30 + * YP-700 and YP-90 are NOT MTP, but use a Samsung custom protocol. + * See: http://wiki.xiph.org/index.php/PortablePlayers for Ogg + * status. + */ + // From anonymous SourceForge user, not verified + { "Samsung", 0x04e8, "YP-900", 0x0409, DEVICE_FLAG_NONE }, + // From MItch + { "Samsung", 0x04e8, "I550W Phone", 0x04a4, DEVICE_FLAG_NONE }, + // From Manfred Enning + { "Samsung", 0x04e8, "Jet S8000", 0x4f1f, DEVICE_FLAG_NONE }, + // From Gabriel Nunes + { "Samsung", 0x04e8, "YH-920 (501d)", 0x501d, DEVICE_FLAG_UNLOAD_DRIVER }, + // From Soren O'Neill + { "Samsung", 0x04e8, "YH-920 (5022)", 0x5022, DEVICE_FLAG_UNLOAD_DRIVER }, + // Contributed by aronvanammers on SourceForge + { "Samsung", 0x04e8, "YH-925GS", 0x5024, DEVICE_FLAG_NONE }, + // From libgphoto2, according to tests by Stephan Fabel it cannot + // get all objects with the getobjectproplist command.. + { "Samsung", 0x04e8, "YH-820", 0x502e, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // Contributed by polux2001@users.sourceforge.net + { "Samsung", 0x04e8, "YH-925(-GS)", 0x502f, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // Contributed by anonymous person on SourceForge + { "Samsung", 0x04e8, "YH-J70J", 0x5033, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From XNJB user + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-Z5", 0x503c, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // Don't add 0x5041 as this is YP-Z5 in USB mode + // Contributed by anonymous person on SourceForge + { "Samsung", 0x04e8, "YP-T7J", 0x5047, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + // Reported by cstrickler@gmail.com + { "Samsung", 0x04e8, "YP-U2J (YP-U2JXB/XAA)", 0x5054, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + // Reported by Andrew Benson + { "Samsung", 0x04e8, "YP-F2J", 0x5057, + DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Patrick + // Just guessing but looks like .spl v1 http://www.anythingbutipod.com/forum/showthread.php?t=14160 + { "Samsung", 0x04e8, "YP-K5", 0x505a, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From dev.local@gmail.com - 0x4e8/0x507c is the UMS mode, apparently + // do not add that device. + // From m.eik michalke + // This device does NOT use the special SPL playlist according to sypqgjxu@gmx.de. + { "Samsung", 0x04e8, "YP-U3", 0x507d, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + // Reported by Matthew Wilcox + // Sergio reports this device need the BROKEN ALL flag. + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-T9", 0x507f, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From Paul Clinch + // Just guessing but looks like .spl v1 http://www.anythingbutipod.com/forum/showthread.php?t=14160 + // Some versions of the firmware reportedly support OGG, reportedly only the + // UMS versions, so MTP+OGG is not possible on this device. + { "Samsung", 0x04e8, "YP-K3", 0x5081, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From XNJB user + // From Alistair Boyle, .spl v2 required for playlists + // According to the device log it properly supports OGG + { "Samsung", 0x04e8, "YP-P2", 0x5083, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_PLAYLIST_SPL_V2 }, + // From Paul Clinch + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-T10", 0x508a, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_PLAYLIST_SPL_V1 | + DEVICE_FLAG_NO_ZERO_READS }, + // From Wim Verwimp + // Not sure about the Ogg and broken proplist flags here. Just guessing. + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-S5", 0x508b, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From Ludovic Danigo + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-S3", 0x5091, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From Adrian Levi + // Guessing on .spl flag + // This one supports OGG properly through the correct MTP type. + { "Samsung", 0x04e8, "YP-U4", 0x5093, DEVICE_FLAG_UNLOAD_DRIVER }, + // From Chris Le Sueur + // Guessing on .spl flag + // This one supports OGG properly through the correct MTP type. + { "Samsung", 0x04e8, "YP-R1", 0x510f, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_UNIQUE_FILENAMES | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // From Anonymous SourceForge user + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-Q1", 0x5115, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From Holger + { "Samsung", 0x04e8, "YP-M1", 0x5118, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_PLAYLIST_SPL_V2 }, + // From Anonymous SourceForge user + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-P3", 0x511a, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From Anonymous SourceForge user + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-Q2", 0x511d, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From Marco Pizzocaro + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-U5", 0x5121, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_PLAYLIST_SPL_V1 | + DEVICE_FLAG_UNIQUE_FILENAMES | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // From Leonardo Accorsi + // Guessing on .spl flag + { "Samsung", 0x04e8, "YP-R0", 0x5125, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_PLAYLIST_SPL_V1 | + DEVICE_FLAG_UNIQUE_FILENAMES | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // From Manuel Carro + // Copied from Q2 + { "Samsung", 0x04e8, "YP-Q3", 0x5130, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // Reported by: traaf + // Guessing on the playlist type! + // Appears to present itself properly as a PTP device with MTP extensions! + { "Samsung", 0x04e8, "YP-Z3", 0x5137, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_OGG_IS_UNKNOWN | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From a rouge .INF file + // this device ID seems to have been recycled for: + // the Samsung SGH-A707 Cingular cellphone + // the Samsung L760-V cellphone + // the Samsung SGH-U900 cellphone + // the Samsung Fascinate player + { "Samsung", 0x04e8, + "YH-999 Portable Media Center/SGH-A707/SGH-L760V/SGH-U900/Verizon Intensity/Fascinate", + 0x5a0f, DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // From Santi Béjar - not sure this is MTP... + // { "Samsung", 0x04e8, "Z170 Mobile Phone", 0x6601, DEVICE_FLAG_UNLOAD_DRIVER }, + // From Santi Béjar - not sure this is MTP... + // { "Samsung", 0x04e8, "E250 Mobile Phone", 0x663e, DEVICE_FLAG_UNLOAD_DRIVER }, + // From an anonymous SF user + { "Samsung", 0x04e8, "M7600 Beat/GT-S8300T/SGH-F490/S8300", 0x6642, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_BROKEN_BATTERY_LEVEL }, + // From Lionel Bouton + { "Samsung", 0x04e8, "X830 Mobile Phone", 0x6702, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From James + { "Samsung", 0x04e8, "U600 Mobile Phone", 0x6709, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From Cesar Cardoso + // No confirmation that this is really MTP. + { "Samsung", 0x04e8, "F250 Mobile Phone", 0x6727, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From Charlie Todd 2007-10-31 + { "Samsung", 0x04e8, "Juke (SCH-U470)", 0x6734, + DEVICE_FLAG_UNLOAD_DRIVER}, + // Reported by Tenn + { "Samsung", 0x04e8, "GT-B2700", 0x6752, + DEVICE_FLAG_UNLOAD_DRIVER }, + // Added by Greg Fitzgerald + { "Samsung", 0x04e8, "SAMSUNG Trance", 0x6763, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From anonymous sourceforge user + // Guessing on .spl flag, maybe needs NO_ZERO_READS, whatdoIknow + { "Samsung", 0x04e8, "GT-S8500", 0x6819, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From Harrison Metzger + { "Samsung", 0x04e8, + "Galaxy Nexus/Galaxy S i9000/i9250, Android 4.0 updates", 0x685c, + DEVICE_FLAGS_ANDROID_BUGS | + DEVICE_FLAG_PLAYLIST_SPL_V2 }, + // Reported by David Goodenough + // Guessing on flags. + { "Samsung", 0x04e8, "Galaxy Y", 0x685e, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_LONG_TIMEOUT | + DEVICE_FLAG_PROPLIST_OVERRIDES_OI }, + /* + * This entry (device 0x6860) seems to be used on a *lot* of Samsung + * Android (gingerbread, 2.3) phones. It is *not* the Android MTP stack + * but an internal Samsung stack. + * + * Popular devices: Galaxy S2 and S3. + * + * - It seems that some PTP commands are broken. + * - Devices seem to have a connection timeout, the session must be + * open in about 3 seconds since the device is plugged in, after + * that time it will not respond. Thus GUI programs work fine. + * - Seems also to be used with Galaxy Nexus debug mode and on + * US markets for some weird reason. + * + * From: Ignacio Martínez and others + */ + { "Samsung", 0x04e8, + "GT P7310/P7510/N7000/I9070/I9100/I9300 Galaxy Tab 7.7/10.1/S2/S3/Nexus/Note/Y", 0x6860, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_LONG_TIMEOUT | + DEVICE_FLAG_PROPLIST_OVERRIDES_OI }, + // Note: ID 0x6865 is some PTP mode! Don't add it. + // From: Erik Berglund + // Logs indicate this needs DEVICE_FLAG_NO_ZERO_READS + // No Samsung platlists on this device. + // https://sourceforge.net/tracker/?func=detail&atid=809061&aid=3026337&group_id=158745 + // i5800 duplicate reported by igel + // Guessing this has the same problematic MTP stack as the device + // above. + { "Samsung", 0x04e8, "Galaxy S GT-I9000/Galaxy 3 i5800/Kies mode", 0x6877, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_LONG_TIMEOUT | + DEVICE_FLAG_PROPLIST_OVERRIDES_OI }, + // From: John Gorkos and + // Akos Maroy + { "Samsung", 0x04e8, "Vibrant SGH-T959/Captivate/Media player mode", 0x68a9, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // Reported by Sleep.Walker + { "Samsung", 0x04e8, "GT-B2710/Xcover 271", 0x68af, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_PLAYLIST_SPL_V1 }, + // From anonymous Sourceforge user + { "Samsung", 0x04e8, "GT-S5230", 0xe20c, DEVICE_FLAG_NONE }, + + + /* + * Microsoft + * All except the first probably need MTPZ to work + */ + { "Microsoft/Intel", 0x045e, "Bandon Portable Media Center", 0x00c9, + DEVICE_FLAG_NONE }, + // Reported by anonymous sourceforge user + // HTC Mozart is using the PID, as is Nokia Lumia 800 + // May need MTPZ to work + { "Microsoft", 0x045e, "Windows Phone", 0x04ec, DEVICE_FLAG_NONE }, + // Reported by Tadimarri Sarath + // No idea why this use an Intel PID, perhaps a leftover from + // the early PMC development days when Intel and Microsoft were + // partnering. + { "Microsoft", 0x045e, "Windows MTP Simulator", 0x0622, DEVICE_FLAG_NONE }, + // Reported by Edward Hutchins (used for Zune HDs) + { "Microsoft", 0x045e, "Zune HD", 0x063e, DEVICE_FLAG_NONE }, + // Reported by anonymous sourceforge user + { "Microsoft", 0x045e, "Kin 1", 0x0640, DEVICE_FLAG_NONE }, + // Reported by anonymous sourceforge user + { "Microsoft/Sharp/nVidia", 0x045e, "Kin TwoM", 0x0641, DEVICE_FLAG_NONE }, + // Reported by Farooq Zaman (used for all Zunes) + { "Microsoft", 0x045e, "Zune", 0x0710, DEVICE_FLAG_NONE }, + + /* + * JVC + */ + // From Mark Veinot + { "JVC", 0x04f1, "Alneo XA-HD500", 0x6105, DEVICE_FLAG_NONE }, + + /* + * Philips + */ + { "Philips", 0x0471, "HDD6320/00 or HDD6330/17", 0x014b, DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // Anonymous SourceForge user + { "Philips", 0x0471, "HDD14XX,HDD1620 or HDD1630/17", 0x014c, DEVICE_FLAG_NONE }, + // from discussion forum + { "Philips", 0x0471, "HDD085/00 or HDD082/17", 0x014d, DEVICE_FLAG_NONE }, + // from XNJB forum + { "Philips", 0x0471, "GoGear SA9200", 0x014f, DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // From John Coppens + { "Philips", 0x0471, "SA1115/55", 0x0164, DEVICE_FLAG_NONE }, + // From Gerhard Mekenkamp + { "Philips", 0x0471, "GoGear Audio", 0x0165, DEVICE_FLAG_NONE }, + // from David Holm + { "Philips", 0x0471, "Shoqbox", 0x0172, DEVICE_FLAG_ONLY_7BIT_FILENAMES }, + // from npedrosa + { "Philips", 0x0471, "PSA610", 0x0181, DEVICE_FLAG_NONE }, + // From libgphoto2 source + { "Philips", 0x0471, "HDD6320", 0x01eb, DEVICE_FLAG_NONE }, + // From Detlef Meier + { "Philips", 0x0471, "GoGear SA6014/SA6015/SA6024/SA6025/SA6044/SA6045", 0x084e, DEVICE_FLAG_UNLOAD_DRIVER }, + // From anonymous Sourceforge user SA5145/02 + { "Philips", 0x0471, "GoGear SA5145", 0x0857, DEVICE_FLAG_UNLOAD_DRIVER }, + // From a + { "Philips", 0x0471, "GoGear SA6125/SA6145/SA6185", 0x2002, DEVICE_FLAG_UNLOAD_DRIVER }, + // From anonymous Sourceforge user, not verified to be MTP! + { "Philips", 0x0471, "GoGear SA3345", 0x2004, DEVICE_FLAG_UNLOAD_DRIVER }, + // From Roberto Vidmar + { "Philips", 0x0471, "SA5285", 0x2022, DEVICE_FLAG_UNLOAD_DRIVER }, + // From Elie De Brauwer + { "Philips", 0x0471, "GoGear ViBE SA1VBE04", 0x2075, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From Anonymous SourceForge user + { "Philips", 0x0471, "GoGear Muse", 0x2077, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From Elie De Brauwer + { "Philips", 0x0471, "GoGear ViBE SA1VBE04/08", 0x207b, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From josmtx + { "Philips", 0x0471, "GoGear Aria", 0x207c, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From epklein + { "Philips", 0x0471, "GoGear SA1VBE08KX/78", 0x208e, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From Anonymous SourceForge User + { "Philips", 0x0471, "GoGear VIBE SA2VBE[08|16]K/02", 0x20b7, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From Anonymous SourceForge User + { "Philips", 0x0471, "GoGear Ariaz", 0x20b9, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From Anonymous SourceForge User + { "Philips", 0x0471, "GoGear Vibe/02", 0x20e5, + DEVICE_FLAG_UNLOAD_DRIVER }, + // from XNJB user + { "Philips", 0x0471, "PSA235", 0x7e01, DEVICE_FLAG_NONE }, + + /* + * Acer + */ + // Reported by anonymous sourceforge user + { "Acer", 0x0502, "Iconia TAB A500 v1", 0x3325, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by: Franck VDL + { "Acer", 0x0502, "Iconia TAB A500 v2", 0x3341, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by: Matthias Arndt + { "Acer", 0x0502, "Iconia TAB A501", 0x3344, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by: anonymous sourceforge user + { "Acer", 0x0502, "Iconia TAB A100", 0x3348, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by: Arvin Schnell + { "Acer", 0x0502, "Iconia TAB A100 ID2", 0x3349, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous sourceforge user + { "Acer", 0x0502, "Iconia TAB A200", 0x337c, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * SanDisk + * several devices (c150 for sure) are definately dual-mode and must + * have the USB mass storage driver that hooks them unloaded first. + * They all have problematic dual-mode making the device unload effect + * uncertain on these devices. + * + * All older devices seem to need DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL. + * Old chipsets: e200/c200 use PP5024 from Nvidia (formerly PortalPlayer). + * m200 use TCC770 from Telechips. + * + * The newer Sansa v2 chipset, AD3525 from Austriamicrosystems (AMS) found + * in e280 v2 c200 v2, Clip, Fuze etc require + * DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST + * and DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR to work properly. + * + * For more info see: http://daniel.haxx.se/sansa/v2.html + */ + // Reported by Brian Robison + { "SanDisk", 0x0781, "Sansa m230/m240", 0x7400, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // From Rockbox device listing + { "SanDisk", 0x0781, "Sansa m200-tcc (MTP mode)", 0x7401, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Reported by tangent_@users.sourceforge.net + { "SanDisk", 0x0781, "Sansa c150", 0x7410, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // From libgphoto2 source + // Reported by + // Reported by Mike Owen + { "SanDisk", 0x0781, "Sansa e200/e250/e260/e270/e280", 0x7420, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Don't add 0x7421 as this is e280 in MSC mode + // Reported by XNJB user + { "SanDisk", 0x0781, "Sansa e260/e280 v2", 0x7422, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR | + DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Reported by XNJB user + { "SanDisk", 0x0781, "Sansa m240/m250", 0x7430, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Reported by Eugene Brevdo + { "SanDisk", 0x0781, "Sansa Clip", 0x7432, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR | + DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED}, + // Reported by HackAR + { "SanDisk", 0x0781, "Sansa Clip v2", 0x7434, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR | + DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED}, + // Reported by anonymous user at sourceforge.net + { "SanDisk", 0x0781, "Sansa c240/c250", 0x7450, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Reported by anonymous SourceForge user + { "SanDisk", 0x0781, "Sansa c250 v2", 0x7452, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Reported by Troy Curtis Jr. + { "SanDisk", 0x0781, "Sansa Express", 0x7460, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Reported by XNJB user, and Miguel de Icaza + // This has no dual-mode so no need to unload any driver. + // This is a Linux based device! + { "SanDisk", 0x0781, "Sansa Connect", 0x7480, DEVICE_FLAG_NONE }, + // Reported by anonymous SourceForge user + { "SanDisk", 0x0781, "Sansa View", 0x74b0, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Reported by Patrick + // There are apparently problems with this device. + { "SanDisk", 0x0781, "Sansa Fuze", 0x74c0, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR | + DEVICE_FLAG_BROKEN_SET_SAMPLE_DIMENSIONS | + DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Harry Phillips + { "SanDisk", 0x0781, "Sansa Fuze v2", 0x74c2, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR | + DEVICE_FLAG_BROKEN_SET_SAMPLE_DIMENSIONS | + DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED }, + // Reported by anonymous SourceForge user + // Need BROKEN_SET_SAMPLE_DIMENSIONS accordning to + // Michael + { "SanDisk", 0x0781, "Sansa Clip+", 0x74d0, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR | + DEVICE_FLAG_BROKEN_SET_SAMPLE_DIMENSIONS | + DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED}, + // Reported by anonymous SourceForge user + { "SanDisk", 0x0781, "Sansa Fuze+", 0x74e0, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR | + DEVICE_FLAG_BROKEN_SET_SAMPLE_DIMENSIONS | + DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED}, + // Reported by mattyj2001@users.sourceforge.net + { "SanDisk", 0x0781, "Sansa Clip Zip", 0x74e4, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE | DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR | + DEVICE_FLAG_BROKEN_SET_SAMPLE_DIMENSIONS | + DEVICE_FLAG_CANNOT_HANDLE_DATEMODIFIED}, + + /* + * iRiver + * we assume that PTP_OC_MTP_GetObjPropList is essentially + * broken on all iRiver devices, meaning it simply won't return + * all properties for a file when asking for metadata 0xffffffff. + * Please test on your device if you believe it isn't broken! + */ + { "iRiver", 0x1006, "H300 Series MTP", 0x3004, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + { "iRiver", 0x1006, "Portable Media Center", 0x4002, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + { "iRiver", 0x1006, "Portable Media Center", 0x4003, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // From [st]anislav + { "iRiver", 0x1042, "T7 Volcano", 0x1143, DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // From an anonymous person at SourceForge, uncertain about this one + { "iRiver", 0x4102, "iFP-880", 0x1008, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // 0x4102, 0x1042 is a USB mass storage mode for E100 v2/Lplayer + // From libgphoto2 source + { "iRiver", 0x4102, "T10", 0x1113, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + { "iRiver", 0x4102, "T20 FM", 0x1114, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // This appears at the MTP-UMS site + { "iRiver", 0x4102, "T20", 0x1115, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + { "iRiver", 0x4102, "U10", 0x1116, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + { "iRiver", 0x4102, "T10a", 0x1117, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + { "iRiver", 0x4102, "T20", 0x1118, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + { "iRiver", 0x4102, "T30", 0x1119, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // Reported by David Wolpoff + { "iRiver", 0x4102, "T10 2GB", 0x1120, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // Rough guess this is the MTP device ID... + { "iRiver", 0x4102, "N12", 0x1122, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // Reported by Philip Antoniades + // Newer iriver devices seem to have shaped-up firmware without any + // of the annoying bugs. + { "iRiver", 0x4102, "Clix2", 0x1126, DEVICE_FLAG_NONE }, + // Reported by Adam Torgerson + { "iRiver", 0x4102, "Clix", 0x112a, + DEVICE_FLAG_NO_ZERO_READS | DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // Reported by Douglas Roth + { "iRiver", 0x4102, "X20", 0x1132, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // Reported by Robert Ugo + { "iRiver", 0x4102, "T60", 0x1134, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_IRIVER_OGG_ALZHEIMER }, + // Reported by two anonymous SourceForge users + // Needs the stronger OGG_IS_UNKNOWN flag to support OGG properly, + // be aware of newer players that may be needing this too. + { "iRiver", 0x4102, "E100", 0x1141, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + // Reported by anonymous SourceForge user + // Need verification of whether this firmware really need all these flags + { "iRiver", 0x4102, "E100 v2/Lplayer", 0x1142, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + // Reported by Richard Vennemann + // In USB Mass Storage mode it is 0x4102/0x1047 + // Seems to use the new shaped-up firmware. + { "iRiver", 0x4102, "Spinn", 0x1147, DEVICE_FLAG_NONE }, + // Reported by Tony Janssen + { "iRiver", 0x4102, "E50", 0x1151, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + // Reported by Jakub Matraszek + { "iRiver", 0x4102, "T5", 0x1153, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_NO_ZERO_READS | DEVICE_FLAG_OGG_IS_UNKNOWN }, + // Reported by pyalex@users.sourceforge.net + // Guessing that this needs the FLAG_NO_ZERO_READS... + { "iRiver", 0x4102, "E30", 0x1167, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + // Reported by Scott Call + // Assume this actually supports OGG though it reports it doesn't. + { "iRiver", 0x4102, "H10 20GB", 0x2101, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + { "iRiver", 0x4102, "H10 5GB", 0x2102, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + // From Rockbox device listing + { "iRiver", 0x4102, "H10 5.6GB", 0x2105, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | DEVICE_FLAG_NO_ZERO_READS | + DEVICE_FLAG_OGG_IS_UNKNOWN }, + + + /* + * Dell + */ + { "Dell, Inc", 0x413c, "DJ Itty", 0x4500, DEVICE_FLAG_NONE }, + /* Reported by: JR */ + { "Dell, Inc", 0x413c, "Dell Streak 7", 0xb10b, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * Toshiba + * Tentatively flagged all Toshiba devices with + * DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST after one of them + * showed erroneous behaviour. + */ + { "Toshiba", 0x0930, "Gigabeat MEGF-40", 0x0009, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + { "Toshiba", 0x0930, "Gigabeat", 0x000c, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // Reported by Nicholas Tripp + { "Toshiba", 0x0930, "Gigabeat P20", 0x000f, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // From libgphoto2 + { "Toshiba", 0x0930, "Gigabeat S", 0x0010, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // Reported by Rob Brown + { "Toshiba", 0x0930, "Gigabeat P10", 0x0011, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // Reported by solanum@users.sourceforge.net + { "Toshiba", 0x0930, "Gigabeat V30", 0x0014, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // Reported by Michael Davis + { "Toshiba", 0x0930, "Gigabeat U", 0x0016, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // Reported by Devon Jacobs + { "Toshiba", 0x0930, "Gigabeat MEU202", 0x0018, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // Reported by Rolf + { "Toshiba", 0x0930, "Gigabeat T", 0x0019, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // Reported by Phil Ingram + // Tentatively added - no real reports of this device ID being MTP, + // reports as USB Mass Storage currently. + { "Toshiba", 0x0930, "Gigabeat MEU201", 0x001a, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // Reported by anonymous SourceForge user + { "Toshiba", 0x0930, "Gigabeat MET401", 0x001d, + DEVICE_FLAG_NO_RELEASE_INTERFACE | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + // Reported by Andree Jacobson + { "Toshiba", 0x0930, "Excite AT300", 0x0963, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Nigel Cunningham + // Guessing on Android bugs + { "Toshiba", 0x0930, "Thrive AT100/AT105", 0x7100, + DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * Archos + * These devices have some dual-mode interfaces which will really + * respect the driver unloading, so DEVICE_FLAG_UNLOAD_DRIVER + * really work on these devices! + */ + // Reported by Alexander Haertig + { "Archos", 0x0e79, "Gmini XS100", 0x1207, DEVICE_FLAG_UNLOAD_DRIVER }, + // Added by Jan Binder + { "Archos", 0x0e79, "XS202 (MTP mode)", 0x1208, DEVICE_FLAG_NONE }, + // Reported by gudul1@users.sourceforge.net + { "Archos", 0x0e79, "104 (MTP mode)", 0x120a, DEVICE_FLAG_NONE }, + // Reported by Archos + { "Archos", 0x0e79, "204 (MTP mode)", 0x120c, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by anonymous Sourceforge user. + { "Archos", 0x0e79, "404 (MTP mode)", 0x1301, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Archos + { "Archos", 0x0e79, "404CAM (MTP mode)", 0x1303, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Etienne Chauchot + { "Archos", 0x0e79, "504 (MTP mode)", 0x1307, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Archos + { "Archos", 0x0e79, "604 (MTP mode)", 0x1309, DEVICE_FLAG_UNLOAD_DRIVER }, + { "Archos", 0x0e79, "604WIFI (MTP mode)", 0x130b, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Kay McCormick + { "Archos", 0x0e79, "704 mobile dvr", 0x130d, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Archos + { "Archos", 0x0e79, "704TV (MTP mode)", 0x130f, DEVICE_FLAG_UNLOAD_DRIVER }, + { "Archos", 0x0e79, "405 (MTP mode)", 0x1311, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Joe Rabinoff + { "Archos", 0x0e79, "605 (MTP mode)", 0x1313, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Archos + { "Archos", 0x0e79, "605F (MTP mode)", 0x1315, DEVICE_FLAG_UNLOAD_DRIVER }, + { "Archos", 0x0e79, "705 (MTP mode)", 0x1319, DEVICE_FLAG_UNLOAD_DRIVER }, + { "Archos", 0x0e79, "TV+ (MTP mode)", 0x131b, DEVICE_FLAG_UNLOAD_DRIVER }, + { "Archos", 0x0e79, "105 (MTP mode)", 0x131d, DEVICE_FLAG_UNLOAD_DRIVER }, + { "Archos", 0x0e79, "405HDD (MTP mode)", 0x1321, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Jim Krehl + { "Archos", 0x0e79, "5 (MTP mode)", 0x1331, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Adrien Guichard + { "Archos", 0x0e79, "5 (MTP mode)", 0x1333, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Archos + { "Archos", 0x0e79, "7 (MTP mode)", 0x1335, DEVICE_FLAG_UNLOAD_DRIVER }, + { "Archos", 0x0e79, "SPOD (MTP mode)", 0x1341, DEVICE_FLAG_UNLOAD_DRIVER }, + { "Archos", 0x0e79, "5S IT (MTP mode)", 0x1351, DEVICE_FLAG_UNLOAD_DRIVER }, + { "Archos", 0x0e79, "5H IT (MTP mode)", 0x1357, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by anonymous Sourceforge user + { "Archos", 0x0e79, "8o G9 (MTP mode)", 0x1508, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Clément + { "Archos", 0x0e79, "8o G9 Turbo (MTP mode)", 0x1509, + DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Thackert + { "Archos", 0x0e79, "80G9", 0x1518, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Till + { "Archos", 0x0e79, "101 G9", 0x1528, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous sourceforge user + { "Archos", 0x0e79, "101 G9 (v2)", 0x1529, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous sourceforge user + { "Archos", 0x0e79, "101 G9 Turbo 250 HD", 0x1538, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous sourceforge user + { "Archos", 0x0e79, "101 G9 Turbo", 0x1539, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous sourceforge user + { "Archos", 0x0e79, "70it2 (mode 1)", 0x1568, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Sebastien ROHAUT + { "Archos", 0x0e79, "70it2 (mode 2)", 0x1569, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * Dunlop (OEM of EGOMAN ltd?) reported by Nanomad + * This unit is falsely detected as USB mass storage in Linux + * prior to kernel 2.6.19 (fixed by patch from Alan Stern) + * so on older kernels special care is needed to remove the + * USB mass storage driver that erroneously binds to the device + * interface. + * + * More problematic, this manufacturer+device ID seems to be + * reused in a USB Mass Storage device named "Zipy Fox 8GB", + * which means libmtp may mistreat it. + */ + { "Dunlop", 0x10d6, "MP3 player 1GB / EGOMAN MD223AFD", 0x2200, DEVICE_FLAG_UNLOAD_DRIVER}, + // Reported by Steven Black + // Obviously this company goes by many names. + // This device is USB 2.0 only. Broken pipe on closing. + // A later report indicates that this is also used by the iRiver E200 + { "Memorex or iRiver", 0x10d6, "MMP 8585/8586 or iRiver E200", 0x2300, + DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_NO_RELEASE_INTERFACE}, + + /* + * Sirius + */ + { "Sirius", 0x18f6, "Stiletto", 0x0102, DEVICE_FLAG_NONE }, + // Reported by Chris Bagwell + { "Sirius", 0x18f6, "Stiletto 2", 0x0110, DEVICE_FLAG_NONE }, + + /* + * Canon + * These are actually cameras, but they have a Microsoft device descriptor + * and reports themselves as supporting the MTP extension. + */ + { "Canon", 0x04a9, "Ixus Digital 700 (PTP/MTP mode)", 0x30f2, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + { "Canon", 0x04a9, "PowerShot A640 (PTP/MTP mode)", 0x3139, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // From Peter + { "Canon", 0x04a9, "PowerShot SX20IS (PTP/MTP mode)", 0x31e4, + DEVICE_FLAG_NONE }, + + /* + * Nokia + * Please verify the low device IDs here, I suspect these might be for + * things like USB storage or modem mode actually, whereas the higher + * range (0x04nn) could be for MTP. Some of the devices were gathered + * from the Nokia WMP drivers: + * http://nds2.nokia.com/files/support/global/phones/software/ + * Address was gathered from going to: + * nseries.com + * -> support + * -> select supported device + * -> PC software + * -> Music software + * -> Windows Media Player 10 driver + */ + // From: DoomHammer + { "Nokia", 0x0421, "N81 Mobile Phone", 0x000a, DEVICE_FLAG_NONE }, + // From an anonymous SourceForge user + { "Nokia", 0x0421, "6120c Classic Mobile Phone", 0x002e, DEVICE_FLAG_NONE }, + // From Stefano + { "Nokia", 0x0421, "N96 Mobile Phone", 0x0039, DEVICE_FLAG_NONE }, + // From Martijn van de Streek + { "Nokia", 0x0421, "6500c Classic Mobile Phone", 0x003c, DEVICE_FLAG_NONE }, + // From: DoomHammer + { "Nokia", 0x0421, "3110c Mobile Phone", 0x005f, DEVICE_FLAG_NONE }, + // From: Vasily + { "Nokia", 0x0421, "3109c Mobile Phone", 0x0065, DEVICE_FLAG_NONE }, + // From: + { "Nokia", 0x0421, "5310 XpressMusic", 0x006c, DEVICE_FLAG_NONE }, + // From: robin (AT) headbank D0Tco DOTuk + { "Nokia", 0x0421, "N95 Mobile Phone 8GB", 0x006e, DEVICE_FLAG_NONE }, + // From Bastien Nocera + { "Nokia", 0x0421, "N82 Mobile Phone", 0x0074, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From Martijn van de Streek + { "Nokia", 0x0421, "N78 Mobile Phone", 0x0079, DEVICE_FLAG_NONE }, + // From William Pettersson + { "Nokia", 0x0421, "6220 Classic", 0x008d, DEVICE_FLAG_NONE }, + // From kellerkev@gmail.com + { "Nokia", 0x0421, "N85 Mobile Phone", 0x0092, DEVICE_FLAG_NONE }, + // From Alexandre LISSY + { "Nokia", 0x0421, "6210 Navigator", 0x0098, DEVICE_FLAG_NONE }, + // From: danielw + { "Nokia", 0x0421, "E71", 0x00e4, DEVICE_FLAG_NONE }, + // From: Laurent Bigonville + { "Nokia", 0x0421, "E66", 0x00e5, DEVICE_FLAG_NONE }, + // From: Pier + { "Nokia", 0x0421, "5320 XpressMusic", 0x00ea, DEVICE_FLAG_NONE }, + // From: Gausie + { "Nokia", 0x0421, "5800 XpressMusic", 0x0154, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // From: Willy Gardiol (web) + // Spurious errors for getting all objects, lead me to believe + // this flag atleast is needed + { "Nokia", 0x0421, "5800 XpressMusic v2", 0x0155, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // Yet another version... I think + { "Nokia", 0x0421, "5800 XpressMusic v3", 0x0159, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // From an anonymous SourceForge user + // Not verified to be MTP + { "Nokia", 0x0421, "E63", 0x0179, DEVICE_FLAG_NONE }, + // Reported by: max g + // Reported by: oswillios + { "Nokia", 0x0421, "N79", 0x0186, DEVICE_FLAG_NONE }, + // From an anonymous SourceForge user + { "Nokia", 0x0421, "E71x", 0x01a1, DEVICE_FLAG_NONE }, + // From Ser + { "Nokia", 0x0421, "E52", 0x01cf, DEVICE_FLAG_NONE }, + // From Marcus Meissner + { "Nokia", 0x0421, "3710", 0x01ee, DEVICE_FLAG_NONE }, + // From: AxeL + { "Nokia", 0x0421, "N97-1", 0x01f4, DEVICE_FLAG_NONE }, + // From: FunkyPenguin + { "Nokia", 0x0421, "N97", 0x01f5, DEVICE_FLAG_NONE }, + // From: Anonymous SourceForge user + { "Nokia", 0x0421, "5130 XpressMusic", 0x0209, DEVICE_FLAG_NONE }, + // From: Anonymous SourceForge user + { "Nokia", 0x0421, "E72", 0x0221, DEVICE_FLAG_NONE }, + // From: Anonymous SourceForge user + { "Nokia", 0x0421, "5530", 0x0229, DEVICE_FLAG_NONE }, + // From: Anonymous SourceForge user + { "Nokia", 0x0421, "N97 mini", 0x026b, DEVICE_FLAG_NONE }, + // From: Anonymous SourceForge user + { "Nokia", 0x0421, "X6", 0x0274, DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // From: Alexander Kojevnikov + { "Nokia", 0x0421, "6600i", 0x0297, DEVICE_FLAG_NONE }, + // From: Karthik Paithankar + { "Nokia", 0x0421, "2710", 0x02c1, DEVICE_FLAG_NONE }, + // From: Mick Stephenson + { "Nokia", 0x0421, "5230", 0x02e2, DEVICE_FLAG_NONE }, + // From: Lan Liu at Nokia + { "Nokia", 0x0421, "N8", 0x02fe, DEVICE_FLAG_NONE }, + // From: Lan Liu at Nokia + { "Nokia", 0x0421, "N8 (Ovi mode)", 0x0302, DEVICE_FLAG_NONE }, + // From: Martijn Hoogendoorn + { "Nokia", 0x0421, "E7", 0x0334, DEVICE_FLAG_NONE }, + // From: Raul Metsma + { "Nokia", 0x0421, "E7 (Ovi mode)", 0x0335, DEVICE_FLAG_NONE }, + // Reported by Serg + // Symbian phone + { "Nokia", 0x0421, "C7", 0x03c1, DEVICE_FLAG_NONE }, + // Reported by Anonymous SourceForge user + { "Nokia", 0x0421, "C7 (ID2)", 0x03cd, DEVICE_FLAG_NONE }, + // Reported by Anonymous SourceForge user + { "Nokia", 0x0421, "N950", 0x03d2, DEVICE_FLAG_NONE }, + // From: http://nds2.nokia.com/files/support/global/phones/software/Nokia_3250_WMP10_driver.inf + { "Nokia", 0x0421, "3250 Mobile Phone", 0x0462, DEVICE_FLAG_NONE }, + // From http://nds2.nokia.com/files/support/global/phones/software/Nokia_N93_WMP10_Driver.inf + { "Nokia", 0x0421, "N93 Mobile Phone", 0x0478, DEVICE_FLAG_NONE }, + // From: http://nds2.nokia.com/files/support/global/phones/software/Nokia_5500_Sport_WMP10_driver.inf + { "Nokia", 0x0421, "5500 Sport Mobile Phone", 0x047e, DEVICE_FLAG_NONE }, + // From http://nds2.nokia.com/files/support/global/phones/software/Nokia_N91_WMP10_Driver.inf + { "Nokia", 0x0421, "N91 Mobile Phone", 0x0485, DEVICE_FLAG_NONE }, + // From: Christian Rusa + { "Nokia", 0x0421, "5700 XpressMusic Mobile Phone", 0x04b4, DEVICE_FLAG_NONE }, + // From: Mitchell Hicks + { "Nokia", 0x0421, "5300 Mobile Phone", 0x04ba, DEVICE_FLAG_NONE }, + // https://sourceforge.net/tracker/index.php?func=detail&aid=2692473&group_id=8874&atid=358874 + // From: Tiburce + { "Nokia", 0x0421, "5200 Mobile Phone", 0x04be, + DEVICE_FLAG_BROKEN_BATTERY_LEVEL }, + // From Christian Arnold + { "Nokia", 0x0421, "N73 Mobile Phone", 0x04d1, DEVICE_FLAG_UNLOAD_DRIVER }, + // From Swapan + { "Nokia", 0x0421, "N75 Mobile Phone", 0x04e1, DEVICE_FLAG_NONE }, + // From: http://nds2.nokia.com/files/support/global/phones/software/Nokia_N93i_WMP10_driver.inf + { "Nokia", 0x0421, "N93i Mobile Phone", 0x04e5, DEVICE_FLAG_NONE }, + // From Anonymous Sourceforge User + { "Nokia", 0x0421, "N95 Mobile Phone", 0x04ef, DEVICE_FLAG_NONE }, + // From: Pat Nicholls + { "Nokia", 0x0421, "N80 Internet Edition (Media Player)", 0x04f1, + DEVICE_FLAG_UNLOAD_DRIVER }, + // From: Maxin B. John + { "Nokia", 0x0421, "N9", 0x051a, DEVICE_FLAG_NONE }, + // Reported by Richard Wall + { "Nokia", 0x05c6, "5530 Xpressmusic", 0x0229, DEVICE_FLAG_NONE }, + // Reported by anonymous SourceForge user + // One thing stated by reporter (Nokia model) another by the detect log... + { "Nokia/Verizon", 0x05c6, "6205 Balboa/Verizon Music Phone", 0x3196, DEVICE_FLAG_NONE }, + + + /* + * Vendor ID 0x13d1 is some offshoring company in China, + * in one source named "A-Max Technology Macao Commercial + * Offshore Co. Ltd." sometime "CCTech". + */ + // Logik brand + { "Logik", 0x13d1, "LOG DAX MP3 and DAB Player", 0x7002, DEVICE_FLAG_UNLOAD_DRIVER }, + // Technika brand + // Reported by + { "Technika", 0x13d1, "MP-709", 0x7017, DEVICE_FLAG_UNLOAD_DRIVER }, + + + /* + * RCA / Thomson + */ + // From kiki + { "Thomson", 0x069b, "EM28 Series", 0x0774, DEVICE_FLAG_NONE }, + { "Thomson / RCA", 0x069b, "Opal / Lyra MC4002", 0x0777, DEVICE_FLAG_NONE }, + { "Thomson", 0x069b, "Lyra MC5104B (M51 Series)", 0x077c, DEVICE_FLAG_NONE }, + { "Thomson", 0x069b, "RCA H106", 0x301a, DEVICE_FLAG_UNLOAD_DRIVER }, + // From Svenna + // Not confirmed to be MTP. + { "Thomson", 0x069b, "scenium E308", 0x3028, DEVICE_FLAG_NONE }, + // From XNJB user + { "Thomson / RCA", 0x069b, "Lyra HC308A", 0x3035, DEVICE_FLAG_NONE }, + + /* + * NTT DoCoMo + */ + { "FOMA", 0x04c5, "F903iX HIGH-SPEED", 0x1140, DEVICE_FLAG_NONE }, + + /* + * Palm device userland program named Pocket Tunes + * Reported by Peter Gyongyosi + */ + { "NormSoft, Inc.", 0x1703, "Pocket Tunes", 0x0001, DEVICE_FLAG_NONE }, + // Reported by anonymous submission + { "NormSoft, Inc.", 0x1703, "Pocket Tunes 4", 0x0002, DEVICE_FLAG_NONE }, + + /* + * TrekStor, Medion and Maxfield devices + * Their datasheet claims their devices are dualmode so probably needs to + * unload the attached drivers here. + */ + // Reported by Stefan Voss + // This is a Sigmatel SoC with a hard disk. + { "TrekStor", 0x066f, "Vibez 8/12GB", 0x842a, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by anonymous SourceForge user. + // This one done for Medion, whatever that is. Error reported so assume + // the same bug flag as its ancestor above. + { "Medion", 0x066f, "MD8333", 0x8550, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by anonymous SourceForge user + { "Medion", 0x066f, "MD8333", 0x8588, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // The vendor ID is "Quanta Computer, Inc." + // same as Olivetti Olipad 110 + // Guessing on device flags + { "Medion", 0x0408, "MD99000 (P9514)/Olivetti Olipad 110", 0xb009, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Richard Eigenmann + { "Medion", 0x0408, "Lifetab P9514", 0xb00a, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous SourceForge user + { "Maxfield", 0x066f, "G-Flash NG 1GB", 0x846c, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by PaoloC + // Apparently SigmaTel has an SDK for MTP players with this ID + { "SigmaTel Inc.", 0x066f, "MTPMSCN Audio Player", 0xa010, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Cristi Magherusan + { "TrekStor", 0x0402, "i.Beat Sweez FM", 0x0611, + DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Fox-ino + // No confirmation that this is really MTP so commented it out. + // { "ALi Corp.", 0x0402, "MPMAN 2GB", 0x5668, + // DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Anonymous SourceForge user + {"TrekStor", 0x1e68, "i.Beat Organix 2.0", 0x0002, + DEVICE_FLAG_UNLOAD_DRIVER }, + + /* + * Disney/Tevion/MyMusix + */ + // Reported by XNJB user + { "Disney", 0x0aa6, "MixMax", 0x6021, DEVICE_FLAG_NONE }, + // Reported by anonymous Sourceforge user + { "Tevion", 0x0aa6, "MD 81488", 0x3011, DEVICE_FLAG_NONE }, + // Reported by Peter Hedlund + { "MyMusix", 0x0aa6, "PD-6070", 0x9601, DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST | + DEVICE_FLAG_NO_RELEASE_INTERFACE }, + + /* + * Cowon Systems, Inc. + * The iAudio audiophile devices don't encourage the use of MTP. + * See: http://wiki.xiph.org/index.php/PortablePlayers for Ogg + * status + */ + // Reported by Patrik Johansson + { "Cowon", 0x0e21, "iAudio U3 (MTP mode)", 0x0701, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // Reported by Kevin Michael Smith + { "Cowon", 0x0e21, "iAudio 6 (MTP mode)", 0x0711, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Roberth Karman + { "Cowon", 0x0e21, "iAudio 7 (MTP mode)", 0x0751, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // Reported by an anonymous SourceForge user + { "Cowon", 0x0e21, "iAudio U5 (MTP mode)", 0x0761, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // Reported by TJ Something + { "Cowon", 0x0e21, "iAudio D2 (MTP mode)", 0x0801, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // Reported by anonymous Sourceforge user + { "Cowon", 0x0e21, "iAudio D2+ FW 2.x (MTP mode)", 0x0861, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // From Rockbox device listing + { "Cowon", 0x0e21, "iAudio D2+ DAB FW 4.x (MTP mode)", 0x0871, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // From Rockbox device listing + { "Cowon", 0x0e21, "iAudio D2+ FW 3.x (MTP mode)", 0x0881, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // From Rockbox device listing + { "Cowon", 0x0e21, "iAudio D2+ DMB FW 1.x (MTP mode)", 0x0891, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // Reported by + { "Cowon", 0x0e21, "iAudio S9 (MTP mode)", 0x0901, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // Reported by Dan Nicholson + { "Cowon", 0x0e21, "iAudio 9 (MTP mode)", 0x0911, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // Reported by Franck VDL + { "Cowon", 0x0e21, "iAudio J3 (MTP mode)", 0x0921, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // Reported by anonymous SourceForge user + { "Cowon", 0x0e21, "iAudio X7 (MTP mode)", 0x0931, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + // Reported by anonymous SourceForge user + { "Cowon", 0x0e21, "iAudio C2 (MTP mode)", 0x0941, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + { "Cowon", 0x0e21, "iAudio 10 (MTP mode)", 0x0952, + DEVICE_FLAG_UNLOAD_DRIVER | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_OGG_IS_UNKNOWN | DEVICE_FLAG_FLAC_IS_UNKNOWN }, + + /* + * Insignia, dual-mode. + */ + { "Insignia", 0x19ff, "NS-DV45", 0x0303, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Rajan Bella + { "Insignia", 0x19ff, "Sport Player", 0x0307, DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by "brad" (anonymous, sourceforge) + { "Insignia", 0x19ff, "Pilot 4GB", 0x0309, DEVICE_FLAG_UNLOAD_DRIVER }, + + /* + * LG Electronics + */ + // From anonymous SourceForge user + // Uncertain if this is really the MTP mode device ID... + { "LG Electronics Inc.", 0x043e, "T54", 0x7040, + DEVICE_FLAG_UNLOAD_DRIVER }, + // Not verified - anonymous submission + { "LG Electronics Inc.", 0x043e, "UP3", 0x70b1, DEVICE_FLAG_NONE }, + // Reported by Joseph Nahmias + { "LG Electronics Inc.", 0x1004, "VX8550 V CAST Mobile Phone", 0x6010, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR }, + // Reported by Cyrille Potereau + { "LG Electronics Inc.", 0x1004, "KC910 Renoir Mobile Phone", 0x608f, + DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by Aaron Slunt + { "LG Electronics Inc.", 0x1004, "GR-500 Music Player", 0x611b, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_ALWAYS_PROBE_DESCRIPTOR }, + // Reported by anonymous sourceforge user + { "LG Electronics Inc.", 0x1004, "KM900", 0x6132, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by anonymous sourceforge user + { "LG Electronics Inc.", 0x1004, "LG8575", 0x619a, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_UNLOAD_DRIVER }, + // Reported by anonymous sourceforge user + { "LG Electronics Inc.", 0x1004, "V909 G-Slate", 0x61f9, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_UNLOAD_DRIVER }, + + /* + * Sony + * It could be that these PIDs are one-per hundred series, so + * NWZ-A8xx is 0325, NWZ-S5xx is 0x326 etc. We need more devices + * reported to see a pattern here. + */ + // Reported by Alessandro Radaelli + { "Sony", 0x054c, "Walkman NWZ-A815/NWZ-A818", 0x0325, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by anonymous Sourceforge user. + { "Sony", 0x054c, "Walkman NWZ-S516", 0x0326, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Endre Oma + { "Sony", 0x054c, "Walkman NWZ-S615F/NWZ-S616F/NWZ-S618F", 0x0327, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Jean-Marc Bourguet + { "Sony", 0x054c, "Walkman NWZ-S716F", 0x035a, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Anon SF User / Anthon van der Neut + { "Sony", 0x054c, "Walkman NWZ-A826/NWZ-A828/NWZ-A829", 0x035b, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Niek Klaverstijn + { "Sony", 0x054c, "Walkman NWZ-A726/NWZ-A728/NWZ-A768", 0x035c, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Mehdi AMINI + { "Sony", 0x054c, "Walkman NWZ-B135", 0x036e, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by + { "Sony", 0x054c, "Walkman NWZ-E436F", 0x0385, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Michael Wilkinson + { "Sony", 0x054c, "Walkman NWZ-W202", 0x0388, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Ondrej Sury + { "Sony", 0x054c, "Walkman NWZ-S739F", 0x038c, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Marco Filipe Nunes Soares Abrantes Pereira + { "Sony", 0x054c, "Walkman NWZ-S638F", 0x038e, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Elliot + { "Sony", 0x054c, "Walkman NWZ-X1050B/NWZ-X1060B", + 0x0397, DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Silvio J. Gutierrez + { "Sony", 0x054c, "Walkman NWZ-X1051/NWZ-X1061", 0x0398, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Gregory Boddin + { "Sony", 0x054c, "Walkman NWZ-B142F", 0x03d8, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Rick Warner + { "Sony", 0x054c, "Walkman NWZ-E344", 0x03fc, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Jonathan Stowe + { "Sony", 0x054c, "Walkman NWZ-E445", 0x03fd, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Anonymous SourceForge user + { "Sony", 0x054c, "Walkman NWZ-S545", 0x03fe, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + { "Sony", 0x054c, "Walkman NWZ-A845", 0x0404, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by anonymous SourceForge user + { "Sony", 0x054c, "Walkman NWZ-W252B", 0x04bb, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Suspect this device has strong DRM features + // See https://answers.launchpad.net/ubuntu/+source/libmtp/+question/149587 + { "Sony", 0x054c, "Walkman NWZ-B153F", 0x04be, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + { "Sony", 0x054c, "Walkman NWZ-E354", 0x04cb, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Toni Burgarello + { "Sony", 0x054c, "Walkman NWZ-S754", 0x04cc, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by dmiceman + { "Sony", 0x054c, "NWZ-B163F", 0x059a, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by anonymous Sourceforge user + // guessing on device flags... + { "Sony", 0x054c, "Walkman NWZ-E464", 0x05a6, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by Jan Rheinlaender + { "Sony", 0x054c, "NWZ-S765", 0x05a8, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + // Reported by ghalambaz + { "Sony", 0x054c, "Sony Tablet S1", 0x05b4, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Anonymous SourceForge user + { "Sony", 0x054c, "DCR-SR75", 0x1294, + DEVICE_FLAGS_SONY_NWZ_BUGS }, + + /* + * SonyEricsson + * These initially seemed to support GetObjPropList but later revisions + * of the firmware seem to have broken it, so all are flagged as broken + * for now. + */ + // Reported by Øyvind Stegard + { "SonyEricsson", 0x0fce, "K850i", 0x0075, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Michael Eriksson + { "SonyEricsson", 0x0fce, "W910", 0x0076, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Zack + { "SonyEricsson", 0x0fce, "W890i", 0x00b3, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by robert dot ahlskog at gmail + { "SonyEricsson", 0x0fce, "W760i", 0x00c6, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Linus Åkesson + { "SonyEricsson", 0x0fce, "C902", 0x00d4, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by an anonymous SourceForge user + { "SonyEricsson", 0x0fce, "C702", 0x00d9, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Christian Zuckschwerdt + { "SonyEricsson", 0x0fce, "W980", 0x00da, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by David Taylor + { "SonyEricsson", 0x0fce, "C905", 0x00ef, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by David House + { "SonyEricsson", 0x0fce, "W595", 0x00f3, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL | + DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST }, + // Reported by Mattias Evensson + { "SonyEricsson", 0x0fce, "W902", 0x00f5, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Sarunas + // Doesn't need any flags according to reporter + { "SonyEricsson", 0x0fce, "T700", 0x00fb, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // Reported by Stéphane Pontier + { "SonyEricsson", 0x0fce, "W705/W715", 0x0105, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Håkan Kvist + { "SonyEricsson", 0x0fce, "W995", 0x0112, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by anonymous SourceForge user + { "SonyEricsson", 0x0fce, "U5", 0x0133, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Flo + { "SonyEricsson", 0x0fce, "U8i", 0x013a, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by xirotyu + { "SonyEricsson", 0x0fce, "j10i2 (Elm)", 0x0144, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Serge Chirik + { "SonyEricsson", 0x0fce, "j108i (Cedar)", 0x014e, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + /* + * SonyEricsson/SONY Android devices usually have three personalities due to + * using composite descriptors and the fact that Windows cannot distinguish + * the device unless each composite descriptor is unique. + * + * 0x0nnn = MTP + * 0x4nnn = MTP + mass storage (for CD-ROM) + * 0x5nnn = MTP + ADB (Android debug bridge) + * + */ + // Reported by Jonas Salling <> + // Erroneous MTP implementation seems to be from Aricent, returns + // broken transaction ID. + { "SonyEricsson", 0x0fce, "LT15i (Xperia arc S)", 0x014f, + DEVICE_FLAGS_ARICENT_BUGS }, + // Reported by Eamonn Webster + // Runtime detect the Aricent or Android stack + { "SonyEricsson", 0x0fce, "MT11i Xperia Neo", 0x0156, + DEVICE_FLAG_NONE }, + // Reported by Alejandro DC + // Runtime detect the Aricent or Android stack + { "SonyEricsson", 0x0fce, "MK16i Xperia", 0x015a, + DEVICE_FLAG_NONE }, + // Reported by + // Runtime detect the Aricent or Android stack + { "SonyEricsson", 0x0fce, "ST18a Xperia Ray", 0x0161, + DEVICE_FLAG_NONE }, + /* + * Reported by StehpanKa + * Android with homebrew MTP stack in one firmware, possibly Aricent + * Android with Android stack in another one, so let the run-time + * detector look up the device bug flags, set to NONE initially. + */ + { "SonyEricsson", 0x0fce, "SK17i Xperia mini pro", 0x0166, + DEVICE_FLAG_NONE }, + // Reported by hdhoang + // Runtime detect the Aricent or Android stack + { "SonyEricsson", 0x0fce, "ST15i Xperia Mini", 0x0167, + DEVICE_FLAG_NONE }, + // Reported by Paul Taylor + { "SONY", 0x0fce, "Xperia S", 0x0169, + DEVICE_FLAG_NO_ZERO_READS | DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by equaeghe + { "SONY", 0x0fce, "ST15i Xperia U", 0x0171, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Ondra Lengal + { "SONY", 0x0fce, "Xperia P", 0x0172, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Jonas Nyrén + { "SonyEricsson", 0x0fce, "W302", 0x10c8, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + /* + * MTP+MSC personalities of MTP devices (see above) + */ + // Reported by equaeghe + { "SONY", 0x0fce, "ST25i Xperia U (MTP+MSC mode)", 0x4171, + DEVICE_FLAGS_ANDROID_BUGS }, + // Guessing on this one + { "SONY", 0x0fce, "Xperia P (MTP+MSC mode)", 0x4172, + DEVICE_FLAGS_ANDROID_BUGS }, + /* + * MTP+ADB personalities of MTP devices (see above) + */ + // Reported by anonymous sourceforge user + // Suspect Aricent stack, guessing on these bug flags + { "SonyEricsson", 0x0fce, "LT15i Xperia Arc (MTP+ADB mode)", 0x514f, + DEVICE_FLAGS_ARICENT_BUGS }, + // Reported by Michael K. + // Runtime detect the Aricent or Android stack + { "SonyEricsson", 0x0fce, "MT11i Xperia Neo (MTP+ADB mode)", 0x5156, + DEVICE_FLAG_NONE }, + // Runtime detect the Aricent or Android stack + { "SonyEricsson", 0x0fce, "MK16i Xperia (MTP+ADB mode)", 0x515a, + DEVICE_FLAG_NONE }, + // Reported by Eduard Bloch + // Xperia Ray (2012), SE Android 2.3.4, flags from ST18a + // Runtime detect the Aricent or Android stack + { "SonyEricsson", 0x0fce, "ST18i Xperia Ray (MTP+ADB mode)", 0x5161, + DEVICE_FLAG_NONE }, + // Reported by StehpanKa + // Android with homebrew MTP stack, possibly Aricent + // Runtime detect the Aricent or Android stack + { "SonyEricsson", 0x0fce, "SK17i Xperia mini pro (MTP+ADB mode)", 0x5166, + DEVICE_FLAG_NONE }, + // Android with homebrew MTP stack, possibly Aricent + // Runtime detect the Aricent or Android stack + { "SonyEricsson", 0x0fce, "ST15i Xperia Mini (MTP+ADB mode)", 0x5167, + DEVICE_FLAG_NONE }, + // Reported by equaeghe + { "SONY", 0x0fce, "ST25i Xperia U (MTP+ADB mode)", 0x5171, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Ondra Lengál + { "SONY", 0x0fce, "Xperia P (MTP+ADB mode)", 0x5172, + DEVICE_FLAGS_ANDROID_BUGS }, + { "SONY", 0x0fce, "MT27i Xperia Sola (MTP+MSC+? mode)", 0xa173, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Anonymous Sourceforge user + { "SonyEricsson", 0x0fce, "j10i (Elm)", 0xd144, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Thomas Schweitzer + { "SonyEricsson", 0x0fce, "K550i", 0xe000, + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + + /* + * Motorola + * Assume DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST on all of these. + */ + // Reported by David Boyd + { "Motorola", 0x22b8, "V3m/V750 verizon", 0x2a65, + DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST | + DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + // Reported by Jader Rodrigues Simoes + { "Motorola", 0x22b8, "Xoom 2 Media Edition (ID2)", 0x41cf, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Steven Roemen + { "Motorola", 0x22b8, "Droid X/MB525 (Defy)", 0x41d6, + DEVICE_FLAG_NONE }, + // Reported by anonymous user + { "Motorola", 0x22b8, "Milestone / Verizon Droid", 0x41dc, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous user + { "Motorola", 0x22b8, "DROID2", 0x42a7, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Motorola", 0x22b8, "Xoom 2 Media Edition", 0x4311, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Motorola", 0x22b8, "XT912", 0x4362, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Marcus Meissner to libptp2 + { "Motorola", 0x22b8, "IdeaPad K1", 0x4811, + DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST }, + // Reported by Hans-Joachim Baader to libptp2 + { "Motorola", 0x22b8, "A1200", 0x60ca, + DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST }, + // http://mark.cdmaforums.com/Files/Motdmmtp.inf + { "Motorola", 0x22b8, "MTP Test Command Interface", 0x6413, + DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST }, + // Reported by anonymous user + { "Motorola", 0x22b8, "RAZR2 V8/U9/Z6", 0x6415, + DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST }, + // Reported by Google Inc's Yavor Goulishev + // Android 3.0 MTP stack seems to announce that it supports the + // list operations, but they do not work? + { "Motorola", 0x22b8, "Xoom (ID 1)", 0x70a8, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous Sourceforge user + // "carried by C Spire and other CDMA US carriers" + { "Motorola", 0x22b8, "Milestone X2", 0x70ca, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * Google + * These guys lend their Vendor ID to anyone who comes down the + * road to produce an Android tablet it seems... The Vendor ID + * was originally used for Nexus phones + */ + { "Google Inc (for Sony)", 0x18d1, "S1", 0x05b3, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous Sourceforge user + { "Google Inc (for Barnes & Noble)", 0x18d1, "Nook Color", 0x2d02, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous Sourceforge user + { "Google Inc (for Asus)", 0x18d1, "TF101 Transformer", 0x4e0f, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Laurent Artaud + { "Google Inc (for Samsung)", 0x18d1, "Nexus S", 0x4e21, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Chris Smith + { "Google Inc (for Asus)", 0x18d1, "Nexus 7 (mode 1)", 0x4e41, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Michael Hess + { "Google Inc (for Asus)", 0x18d1, "Nexus 7 (mode 2)", 0x4e42, + DEVICE_FLAGS_ANDROID_BUGS }, + // WiFi-only version of Xoom + // See: http://bugzilla.gnome.org/show_bug.cgi?id=647506 + { "Google Inc (for Motorola)", 0x18d1, "Xoom (MZ604)", 0x70a8, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Google Inc (for Motorola)", 0x22b8, "Xoom (ID 2)", 0x70a9, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Google Inc (for Toshiba)", 0x18d1, "Thrive 7/AT105", 0x7102, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Google Inc (for Lenovo)", 0x18d1, "Ideapad K1", 0x740a, + DEVICE_FLAGS_ANDROID_BUGS }, + // Another OEM for Medion + { "Google Inc (for Medion)", 0x18d1, "MD99000 (P9514)", 0xb00a, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Frederik Himpe + { "Google Inc (for LG Electronics)", 0x18d1, "P990/Optimus (Cyanogen)", + 0xd109, DEVICE_FLAGS_ANDROID_BUGS }, + { "Google Inc (for LG Electronics)", 0x18d1, "P990/Optimus", 0xd10a, + DEVICE_FLAGS_ANDROID_BUGS }, + + + /* + * Media Keg + */ + // Reported by Rajan Bella + { "Kenwood", 0x0b28, "Media Keg HD10GB7 Sport Player", 0x100c, DEVICE_FLAG_UNLOAD_DRIVER}, + + /* + * Micro-Star International (MSI) + */ + // Reported by anonymous sourceforge user. + { "Micro-Star International", 0x0db0, "P610/Model MS-5557", 0x5572, DEVICE_FLAG_NONE }, + + /* + * FOMA + */ + { "FOMA", 0x06d3, "D905i", 0x21ba, DEVICE_FLAG_NONE }, + + /* + * Haier + */ + // Both reported by an anonymous SourceForge user + // This is the 30 GiB model + { "Haier", 0x1302, "Ibiza Rhapsody", 0x1016, DEVICE_FLAG_NONE }, + // This is the 4/8 GiB model + { "Haier", 0x1302, "Ibiza Rhapsody", 0x1017, DEVICE_FLAG_NONE }, + + /* + * Panasonic + */ + // Reported by dmizer + { "Panasonic", 0x04da, "P905i", 0x2145, DEVICE_FLAG_NONE }, + // Reported by Taku + { "Panasonic", 0x04da, "P906i", 0x2158, DEVICE_FLAG_NONE }, + + /* + * Polaroid + */ + { "Polaroid", 0x0546, "Freescape/MPU-433158", 0x2035, DEVICE_FLAG_NONE }, + + /* + * Pioneer + */ + // Reported by Dan Allen + { "Pioneer", 0x08e4, "XMP3", 0x0148, DEVICE_FLAG_NONE }, + + /* + * Slacker Inc. + * Put in all evilness flags because it looks fragile. + */ + // Reported by Pug Fantus + { "Slacker Inc.", 0x1bdc, "Slacker Portable Media Player", 0xfabf, + DEVICE_FLAG_BROKEN_BATTERY_LEVEL | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST | + DEVICE_FLAG_BROKEN_SET_OBJECT_PROPLIST | DEVICE_FLAG_BROKEN_SEND_OBJECT_PROPLIST }, + + // Reported by anonymous user + { "Conceptronic", 0x1e53, "CMTD2", 0x0005, DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by Demadridsur + { "O2 Sistemas", 0x1e53, "ZoltarTV", 0x0006, DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + // Reported by da-beat + { "Wyplay", 0x1e53, "Wyplayer", 0x0007, DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST }, + + // Reported by Sense Hofstede + { "Perception Digital, Ltd", 0x0aa6, "Gigaware GX400", 0x9702, DEVICE_FLAG_NONE }, + + /* + * RIM's BlackBerry + */ + // Reported by Nicolas VIVIEN + { "RIM", 0x0fca, "BlackBerry Storm/9650", 0x8007, DEVICE_FLAG_UNLOAD_DRIVER | + DEVICE_FLAG_SWITCH_MODE_BLACKBERRY | DEVICE_FLAG_BROKEN_MTPGETOBJPROPLIST_ALL }, + + /* + * Nextar + */ + { "Nextar", 0x0402, "MA715A-8R", 0x5668, DEVICE_FLAG_NONE }, + + /* + * Coby + */ + { "Coby", 0x1e74, "COBY MP705", 0x6512, DEVICE_FLAG_NONE }, + + /* + * Apple devices, which are not MTP natively but can be made to speak MTP + * using PwnTunes (http://www.pwntunes.net/) + */ + { "Apple", 0x05ac, "iPhone", 0x1290, DEVICE_FLAG_NONE }, + { "Apple", 0x05ac, "iPod Touch 1st Gen", 0x1291, DEVICE_FLAG_NONE }, + { "Apple", 0x05ac, "iPhone 3G", 0x1292, DEVICE_FLAG_NONE }, + { "Apple", 0x05ac, "iPod Touch 2nd Gen", 0x1293, DEVICE_FLAG_NONE }, + { "Apple", 0x05ac, "iPhone 3GS", 0x1294, DEVICE_FLAG_NONE }, + { "Apple", 0x05ac, "0x1296", 0x1296, DEVICE_FLAG_NONE }, + { "Apple", 0x05ac, "0x1297", 0x1297, DEVICE_FLAG_NONE }, + { "Apple", 0x05ac, "0x1298", 0x1298, DEVICE_FLAG_NONE }, + { "Apple", 0x05ac, "iPod Touch 3rd Gen", 0x1299, DEVICE_FLAG_NONE }, + { "Apple", 0x05ac, "iPad", 0x129a, DEVICE_FLAG_NONE }, + + // Reported by anonymous SourceForge user, also reported as + // Pantech Crux, claming to be: + // Manufacturer: Qualcomm + // Model: Windows Simulator + // Device version: Qualcomm MTP1.0 + { "Curitel Communications, Inc.", 0x106c, + "Verizon Wireless Device", 0x3215, DEVICE_FLAG_NONE }, + // Reported by: Jim Hanrahan + { "Pantech", 0x106c, "Crux", 0xf003, DEVICE_FLAG_NONE }, + + /* + * Asus + */ + // Reported by Glen Overby + { "Asus", 0x0b05, "TF300 Transformer", 0x4c80, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by jaile + { "Asus", 0x0b05, "TF300 Transformer (USB debug mode)", 0x4c81, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous Sourceforge user + { "Asus", 0x0b05, "TF201 Transformer Prime (keyboard dock)", 0x4d00, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Asus", 0x0b05, "TF201 Transformer Prime (tablet only)", 0x4d01, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Asus", 0x0b05, "TFXXX Transformer Prime (unknown version)", 0x4d04, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous Sourceforge user + { "Asus", 0x0b05, "TF101 Eeepad Slider", 0x4e01, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Asus", 0x0b05, "TF101 Eeepad Transformer", 0x4e0f, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Asus", 0x0b05, "TF101 Eeepad Transformer (debug mode)", 0x4e1f, + DEVICE_FLAGS_ANDROID_BUGS }, + + + /* + * Lenovo + */ + // Reported by Richard Körber + { "Lenovo", 0x17ef, "K1", 0x740a, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous sourceforge user + // Adding Android default bug flags since it appears to be an Android + { "Lenovo", 0x17ef, "ThinkPad Tablet", 0x741c, + DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * Huawei + */ + // Reported by anonymous SourceForge user + { "Huawei", 0x12d1, "Honor U8860", 0x1051, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by anonymous SourceForge user + { "Huawei", 0x12d1, "Mediapad (mode 0)", 0x360f, DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Bearsh + { "Huawei", 0x12d1, "Mediapad (mode 1)", 0x361f, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * ZTE + * Android devices reported by junwang + */ + { "ZTE", 0x19d2, "V55 ID 1", 0x0244, DEVICE_FLAGS_ANDROID_BUGS }, + { "ZTE", 0x19d2, "V55 ID 2", 0x0245, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * HTC (High Tech Computer Corp) + */ + { "HTC", 0x0bb4, "Zopo ZP100", 0x0c02, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Steven Eastland + { "HTC", 0x0bb4, "EVO 4G LTE", 0x0c93, + DEVICE_FLAGS_ANDROID_BUGS }, + // Reported by Steven Eastland + { "HTC", 0x0bb4, "EVO 4G LTE (second ID)", 0x0ca8, + DEVICE_FLAGS_ANDROID_BUGS }, + // These identify themselves as "cm_tenderloin", fun... + // Done by HTC for HP I guess. + { "Hewlett-Packard", 0x0bb4, "HP Touchpad", 0x685c, + DEVICE_FLAGS_ANDROID_BUGS }, + { "Hewlett-Packard", 0x0bb4, "HP Touchpad (debug mode)", + 0x6860, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * NEC + */ + { "NEC", 0x0409, "FOMA N01A", 0x0242, DEVICE_FLAG_NONE }, + + /* + * nVidia + */ + // Found on Internet forum + { "nVidia", 0x0955, "CM9-Adam", 0x70a9, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * Vizio + */ + // Reported by Michael Gurski + { "Vizio", 0x0489, "VTAB1008", 0xe040, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * Viewpia, bq... + * Seems like some multi-branded OEM product. + */ + { "Various", 0x2207, "Viewpia DR/bq Kepler", 0x0001, DEVICE_FLAGS_ANDROID_BUGS }, + + /* + * Other strange stuff. + */ + { "Isabella", 0x0b20, "Her Prototype", 0xddee, DEVICE_FLAG_NONE } diff --git a/src/calibre/devices/mtp/unix/upstream/update.py b/src/calibre/devices/mtp/unix/upstream/update.py new file mode 100644 index 0000000000..20c03d072d --- /dev/null +++ b/src/calibre/devices/mtp/unix/upstream/update.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +MP = 'http://libmtp.git.sourceforge.net/git/gitweb.cgi?p=libmtp/libmtp;a=blob_plain;f=src/music-players.h;hb=HEAD' +DF = 'http://libmtp.git.sourceforge.net/git/gitweb.cgi?p=libmtp/libmtp;a=blob_plain;f=src/device-flags.h;hb=HEAD' + +import urllib, os, shutil + +base = os.path.dirname(os.path.abspath(__file__)) + +for url, fname in [(MP, 'music-players.h'), (DF, 'device-flags.h')]: + with open(os.path.join(base, fname), 'wb') as f: + shutil.copyfileobj(urllib.urlopen(url), f) + diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index 8460ecbdef..89bd4a0cec 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -7,6 +7,7 @@ manner. import sys, os, re from threading import RLock +from collections import namedtuple from calibre import prints, as_unicode from calibre.constants import iswindows, isosx, plugins, islinux, isfreebsd @@ -107,6 +108,15 @@ class WinPNPScanner(object): win_pnp_drives = WinPNPScanner() +_USBDevice = namedtuple('USBDevice', + 'vendor_id product_id bcd manufacturer product serial') + +class USBDevice(_USBDevice): + + def __init__(self, *args, **kwargs): + _USBDevice.__init__(self, *args, **kwargs) + self.busnum = self.devnum = -1 + class LinuxScanner(object): SYSFS_PATH = os.environ.get('SYSFS_PATH', '/sys') @@ -122,6 +132,10 @@ class LinuxScanner(object): if not self.ok: raise RuntimeError('DeviceScanner requires the /sys filesystem to work.') + def read(f): + with open(f, 'rb') as s: + return s.read().strip() + for x in os.listdir(self.base): base = os.path.join(self.base, x) ven = os.path.join(base, 'idVendor') @@ -132,31 +146,46 @@ class LinuxScanner(object): prod_string = os.path.join(base, 'product') dev = [] try: - dev.append(int('0x'+open(ven).read().strip(), 16)) + # Ignore USB HUBs + if read(os.path.join(base, 'bDeviceClass')) == b'09': + continue except: continue try: - dev.append(int('0x'+open(prod).read().strip(), 16)) + dev.append(int(b'0x'+read(ven), 16)) except: continue try: - dev.append(int('0x'+open(bcd).read().strip(), 16)) + dev.append(int(b'0x'+read(prod), 16)) except: continue try: - dev.append(open(man).read().strip()) + dev.append(int(b'0x'+read(bcd), 16)) except: - dev.append('') + continue try: - dev.append(open(prod_string).read().strip()) + dev.append(read(man)) except: - dev.append('') + dev.append(b'') try: - dev.append(open(serial).read().strip()) + dev.append(read(prod_string)) except: - dev.append('') + dev.append(b'') + try: + dev.append(read(serial)) + except: + dev.append(b'') - ans.add(tuple(dev)) + dev = USBDevice(*dev) + try: + dev.busnum = int(read(os.path.join(base, 'busnum'))) + except: + pass + try: + dev.devnum = int(read(os.path.join(base, 'devnum'))) + except: + pass + ans.add(dev) return ans class FreeBSDScanner(object): From 664c28fae7f41065c1667abe0da148876e53a81a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 5 Aug 2012 22:08:51 +0530 Subject: [PATCH 10/27] ... --- src/calibre/devices/mtp/unix/driver.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 8336be9fc2..081b09975f 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -64,6 +64,10 @@ class MTP_DEVICE(MTPDeviceBase): @synchronous def open(self, connected_device, library_uuid): + def blacklist_device(): + d = connected_device + self.blacklisted_devices.add((d.busnum, d.devnum, d.vendor_id, + d.product_id, d.bcd, d.serial)) try: self.detect.create_device(connected_device) except ValueError: @@ -74,8 +78,9 @@ class MTP_DEVICE(MTPDeviceBase): except ValueError: # Black list this device so that it is ignored for the # remainder of this session. - d = connected_device - self.blacklisted_devices.add((d.busnum, d.devnum, d.vendor_id, - d.product_id, d.bcd, d.serial)) + blacklist_device() raise OpenFailed('%s is not a MTP device'%connected_device) + except TypeError: + blacklist_device() + raise OpenFailed('') From 3b5fd30b0a8a06215bc022cff8e8026ea2e715fa Mon Sep 17 00:00:00 2001 From: GRiker Date: Sun, 5 Aug 2012 11:49:11 -0600 Subject: [PATCH 11/27] Changed to GenericRulesTable model --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 858 ++++++++++-------- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 53 +- src/calibre/library/catalogs/epub_mobi.py | 62 +- 3 files changed, 514 insertions(+), 459 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 54e8bf5db9..ac251ff801 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -17,8 +17,9 @@ from PyQt4 import QtGui from PyQt4.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QHBoxLayout, QIcon, QLabel, QLineEdit, - QPlainTextEdit, QRadioButton, QSize, QTableWidget, QTableWidgetItem, - QVBoxLayout, QWidget) + QPlainTextEdit, QRadioButton, QSize, QSizePolicy, + QTableWidget, QTableWidgetItem, + QToolButton, QVBoxLayout, QWidget) class PluginWidget(QWidget,Ui_Form): @@ -190,7 +191,9 @@ class PluginWidget(QWidget,Ui_Form): self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) # Initialize prefix rules - self.prefix_rules_initialize(prefix_rules) + self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, "prefix_rules_tw", + prefix_rules, self.eligible_custom_fields, + self.db) def options(self): # Save/return the current options @@ -212,7 +215,7 @@ class PluginWidget(QWidget,Ui_Form): elif c_type in ['spin_box']: opt_value = unicode(getattr(self, c_name).value()) elif c_type in ['table_widget']: - opt_value = self.prefix_rules_get_data() + opt_value = self.prefix_rules_table.get_data() gprefs.set(self.name + '_' + c_name, opt_value) @@ -346,393 +349,6 @@ class PluginWidget(QWidget,Ui_Form): self.merge_after.setEnabled(False) self.include_hr.setEnabled(False) - - def prefix_rules_add_row(self): - # Called when '+' clicked - self.prefix_rules_tw.setFocus() - row = self.prefix_rules_tw.currentRow() + 1 - self.prefix_rules_tw.insertRow(row) - self.prefix_rules_populate_table_row(row, self.prefix_rules_create_blank_row_data()) - self.prefix_rules_select_and_scroll_to_row(row) - self.prefix_rules_tw.resizeColumnsToContents() - # Just in case table was empty - self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) - - def prefix_rules_convert_row_to_data(self, row): - data = self.prefix_rules_create_blank_row_data() - data['ordinal'] = row - data['enabled'] = self.prefix_rules_tw.item(row,0).checkState() == Qt.Checked - data['name'] = unicode(self.prefix_rules_tw.cellWidget(row,1).text()).strip() - data['prefix'] = unicode(self.prefix_rules_tw.cellWidget(row,2).currentText()).strip() - data['field'] = unicode(self.prefix_rules_tw.cellWidget(row,3).currentText()).strip() - data['pattern'] = unicode(self.prefix_rules_tw.cellWidget(row,4).currentText()).strip() - return data - - def prefix_rules_create_blank_row_data(self): - data = {} - data['ordinal'] = -1 - data['enabled'] = False - data['name'] = 'New rule' - data['field'] = '' - data['pattern'] = '' - data['prefix'] = '' - return data - - def prefix_rules_delete_row(self): - self.prefix_rules_tw.setFocus() - rows = self.prefix_rules_tw.selectionModel().selectedRows() - if len(rows) == 0: - return - message = '

Are you sure you want to delete this rule?' - if len(rows) > 1: - message = '

Are you sure you want to delete the %d selected rules?'%len(rows) - if not question_dialog(self, _('Are you sure?'), message, show_copy_button=False): - return - first_sel_row = self.prefix_rules_tw.currentRow() - for selrow in reversed(rows): - self.prefix_rules_tw.removeRow(selrow.row()) - if first_sel_row < self.prefix_rules_tw.rowCount(): - self.prefix_rules_select_and_scroll_to_row(first_sel_row) - elif self.prefix_rules_tw.rowCount() > 0: - self.prefix_rules_select_and_scroll_to_row(first_sel_row - 1) - - def prefix_rules_generate_prefix_list(self): - def prefix_sorter(item): - key = item - if item[0] == "_": - key = 'zzz' + item - return key - - - # Create a list of prefixes for user selection - raw_prefix_list = [ - ('Ampersand',u'&'), - ('Angle left double',u'\u00ab'), - ('Angle left',u'\u2039'), - ('Angle right double',u'\u00bb'), - ('Angle right',u'\u203a'), - ('Arrow carriage return',u'\u21b5'), - ('Arrow double',u'\u2194'), - ('Arrow down',u'\u2193'), - ('Arrow left',u'\u2190'), - ('Arrow right',u'\u2192'), - ('Arrow up',u'\u2191'), - ('Asterisk',u'*'), - ('At sign',u'@'), - ('Bullet smallest',u'\u22c5'), - ('Bullet small',u'\u00b7'), - ('Bullet',u'\u2022'), - ('Cards clubs',u'\u2663'), - ('Cards diamonds',u'\u2666'), - ('Cards hearts',u'\u2665'), - ('Cards spades',u'\u2660'), - ('Caret',u'^'), - ('Checkmark',u'\u2713'), - ('Copyright circle c',u'\u00a9'), - ('Copyright circle r',u'\u00ae'), - ('Copyright trademark',u'\u2122'), - ('Currency cent',u'\u00a2'), - ('Currency dollar',u'$'), - ('Currency euro',u'\u20ac'), - ('Currency pound',u'\u00a3'), - ('Currency yen',u'\u00a5'), - ('Dagger double',u'\u2021'), - ('Dagger',u'\u2020'), - ('Degree',u'\u00b0'), - ('Dots3',u'\u2234'), - ('Hash',u'#'), - ('Infinity',u'\u221e'), - ('Lozenge',u'\u25ca'), - ('Math divide',u'\u00f7'), - ('Math empty',u'\u2205'), - ('Math equals',u'='), - ('Math minus',u'\u2212'), - ('Math plus circled',u'\u2295'), - ('Math times circled',u'\u2297'), - ('Math times',u'\u00d7'), - ('O slash',u'\u00d8'), - ('Paragraph',u'\u00b6'), - ('Percent',u'%'), - ('Plus-or-minus',u'\u00b1'), - ('Plus',u'+'), - ('Punctuation colon',u':'), - ('Punctuation colon-semi',u';'), - ('Punctuation exclamation',u'!'), - ('Punctuation question',u'?'), - ('Punctuation period',u'.'), - ('Punctuation slash back',u'\\'), - ('Punctuation slash forward',u'/'), - ('Section',u'\u00a7'), - ('Tilde',u'~'), - ('Vertical bar',u'|'), - ('Vertical bar broken',u'\u00a6'), - ('_0',u'0'), - ('_1',u'1'), - ('_2',u'2'), - ('_3',u'3'), - ('_4',u'4'), - ('_5',u'5'), - ('_6',u'6'), - ('_7',u'7'), - ('_8',u'8'), - ('_9',u'9'), - ('_A',u'A'), - ('_B',u'B'), - ('_C',u'C'), - ('_D',u'D'), - ('_E',u'E'), - ('_F',u'F'), - ('_G',u'G'), - ('_H',u'H'), - ('_I',u'I'), - ('_J',u'J'), - ('_K',u'K'), - ('_L',u'L'), - ('_M',u'M'), - ('_N',u'N'), - ('_O',u'O'), - ('_P',u'P'), - ('_Q',u'Q'), - ('_R',u'R'), - ('_S',u'S'), - ('_T',u'T'), - ('_U',u'U'), - ('_V',u'V'), - ('_W',u'W'), - ('_X',u'X'), - ('_Y',u'Y'), - ('_Z',u'Z'), - ('_a',u'a'), - ('_b',u'b'), - ('_c',u'c'), - ('_d',u'd'), - ('_e',u'e'), - ('_f',u'f'), - ('_g',u'g'), - ('_h',u'h'), - ('_i',u'i'), - ('_j',u'j'), - ('_k',u'k'), - ('_l',u'l'), - ('_m',u'm'), - ('_n',u'n'), - ('_o',u'o'), - ('_p',u'p'), - ('_q',u'q'), - ('_r',u'r'), - ('_s',u's'), - ('_t',u't'), - ('_u',u'u'), - ('_v',u'v'), - ('_w',u'w'), - ('_x',u'x'), - ('_y',u'y'), - ('_z',u'z'), - ] - raw_prefix_list = sorted(raw_prefix_list, key=prefix_sorter) - self.prefixes = [x[1] for x in raw_prefix_list] - - def prefix_rules_get_data(self): - data_items = [] - for row in range(self.prefix_rules_tw.rowCount()): - data = self.prefix_rules_convert_row_to_data(row) - data_items.append( - {'ordinal':data['ordinal'], - 'enabled':data['enabled'], - 'name':data['name'], - 'field':data['field'], - 'pattern':data['pattern'], - 'prefix':data['prefix']}) - return data_items - - def prefix_rules_initialize(self, prefix_rules): - # Assign icons to buttons, hook clicks - self.move_rule_up_tb.setToolTip('Move rule up') - self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) - self.move_rule_up_tb.clicked.connect(self.prefix_rules_move_row_up) - - self.add_rule_tb.setToolTip('Add a new rule') - self.add_rule_tb.setIcon(QIcon(I('plus.png'))) - self.add_rule_tb.clicked.connect(self.prefix_rules_add_row) - - self.delete_rule_tb.setToolTip('Delete selected rule') - self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) - self.delete_rule_tb.clicked.connect(self.prefix_rules_delete_row) - - self.move_rule_down_tb.setToolTip('Move rule down') - self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) - self.move_rule_down_tb.clicked.connect(self.prefix_rules_move_row_down) - - # Configure the QTableWidget - # enabled/prefix/rule name/source field/pattern - self.prefix_rules_tw.clear() - header_labels = ['','Name','Prefix','Source','Pattern'] - self.prefix_rules_tw.setColumnCount(len(header_labels)) - self.prefix_rules_tw.setHorizontalHeaderLabels(header_labels) - self.prefix_rules_tw.setSortingEnabled(False) - self.prefix_rules_tw.setSelectionBehavior(QAbstractItemView.SelectRows) - - # Generate the prefix list - self.prefix_rules_generate_prefix_list() - - # Populate the table - self.prefix_rules_populate(prefix_rules) - self.prefix_rules_tw.resizeColumnsToContents() - self.prefix_rules_resize_name(1.5) - self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) - - def prefix_rules_move_row_down(self): - self.prefix_rules_tw.setFocus() - rows = self.prefix_rules_tw.selectionModel().selectedRows() - if len(rows) == 0: - return - last_sel_row = rows[-1].row() - if last_sel_row == self.prefix_rules_tw.rowCount() - 1: - return - - self.prefix_rules_tw.blockSignals(True) - for selrow in reversed(rows): - dest_row = selrow.row() + 1 - src_row = selrow.row() - - # Save the contents of the destination row - saved_data = self.prefix_rules_convert_row_to_data(dest_row) - - # Remove the destination row - self.prefix_rules_tw.removeRow(dest_row) - - # Insert a new row at the original location - self.prefix_rules_tw.insertRow(src_row) - - # Populate it with the saved data - self.prefix_rules_populate_table_row(src_row, saved_data) - self.blockSignals(False) - scroll_to_row = last_sel_row + 1 - if scroll_to_row < self.prefix_rules_tw.rowCount() - 1: - scroll_to_row = scroll_to_row + 1 - self.prefix_rules_tw.scrollToItem(self.prefix_rules_tw.item(scroll_to_row, 0)) - - def prefix_rules_move_row_up(self): - self.prefix_rules_tw.setFocus() - rows = self.prefix_rules_tw.selectionModel().selectedRows() - if len(rows) == 0: - return - first_sel_row = rows[0].row() - if first_sel_row <= 0: - return - self.prefix_rules_tw.blockSignals(True) - for selrow in rows: - # Save the row above - saved_data = self.prefix_rules_convert_row_to_data(selrow.row() - 1) - - # Add a row below us with the source data - self.prefix_rules_tw.insertRow(selrow.row() + 1) - self.prefix_rules_populate_table_row(selrow.row() + 1, saved_data) - - # Delete the row above - self.prefix_rules_tw.removeRow(selrow.row() - 1) - self.blockSignals(False) - - scroll_to_row = first_sel_row - 1 - if scroll_to_row > 0: - scroll_to_row = scroll_to_row - 1 - self.prefix_rules_tw.scrollToItem(self.prefix_rules_tw.item(scroll_to_row, 0)) - - def prefix_rules_populate(self,rules): - # Format of rules list is different if default values vs retrieved JSON - # Hack to normalize list style - if type(rules[0]) is list: - rules = rules[0] - self.prefix_rules_tw.setFocus() - rules = sorted(rules, key=lambda k: k['ordinal']) - for row, rule in enumerate(rules): - self.prefix_rules_tw.insertRow(row) - self.prefix_rules_select_and_scroll_to_row(row) - self.prefix_rules_populate_table_row(row, rule) - self.prefix_rules_tw.selectRow(0) - - def prefix_rules_populate_table_row(self, row, data): - - def set_prefix_field_in_row(row, col, field=''): - prefix_combo = PrefixRulesComboBox(self, self.prefixes, field) - self.prefix_rules_tw.setCellWidget(row, col, prefix_combo) - - def set_rule_name_in_row(row, col, name=''): - rule_name = QLineEdit(name) - rule_name.home(False) - rule_name.editingFinished.connect(self.prefix_rules_rule_name_edited) - self.prefix_rules_tw.setCellWidget(row, col, rule_name) - - def set_source_field_in_row(row, col, field=''): - source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) - source_combo.currentIndexChanged.connect(partial(self.prefix_rules_source_index_changed, source_combo, row)) - #source_combo.currentIndexChanged.connect(self.prefix_rules_source_index_changed, source_combo, row) - self.prefix_rules_tw.setCellWidget(row, col, source_combo) - return source_combo - - - # Entry point - self.prefix_rules_tw.blockSignals(True) - #print("prefix_rules_populate_table_row processing rule:\n%s\n" % data) - - # Column 0: Enabled - self.prefix_rules_tw.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) - - # Column 1: Rule name - #rule_name = QTableWidgetItem(data['name']) - #self.prefix_rules_tw.setItem(row, 1, rule_name) - set_rule_name_in_row(row, 1, name=data['name']) - - # Column 2: Prefix - set_prefix_field_in_row(row, 2, field=data['prefix']) - - # Column 3: Source field - source_combo = set_source_field_in_row(row, 3, field=data['field']) - - # Column 4: Pattern - # The contents of the Pattern field is driven by the Source field - self.prefix_rules_source_index_changed(source_combo, row, 4, pattern=data['pattern']) - - self.prefix_rules_tw.blockSignals(False) - - def prefix_rules_resize_name(self, scale): - current_width = self.prefix_rules_tw.columnWidth(1) - self.prefix_rules_tw.setColumnWidth(1, min(225,int(current_width * scale))) - - def prefix_rules_rule_name_edited(self): - current_row = self.prefix_rules_tw.currentRow() - self.prefix_rules_tw.cellWidget(current_row,1).home(False) - self.prefix_rules_tw.setFocus() - self.prefix_rules_select_and_scroll_to_row(current_row) - - def prefix_rules_select_and_scroll_to_row(self, row): - self.prefix_rules_tw.selectRow(row) - self.prefix_rules_tw.scrollToItem(self.prefix_rules_tw.currentItem()) - - def prefix_rules_source_index_changed(self, combo, row, col, pattern=''): - # Populate the Pattern field based upon the Source field - - source_field = str(combo.currentText()) - if source_field == '': - values = [] - elif source_field == 'Tags': - values = sorted(self.db.all_tags(), key=sort_key) - else: - if self.eligible_custom_fields[source_field]['datatype'] in ['enumeration', 'text']: - values = self.db.all_custom(self.db.field_metadata.key_to_label( - self.eligible_custom_fields[source_field]['field'])) - values = sorted(values, key=sort_key) - elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']: - values = ['True','False','unspecified'] - elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: - values = ['any date','unspecified'] - elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: - values = ['any value','unspecified'] - - values_combo = PrefixRulesComboBox(self, values, pattern) - self.prefix_rules_tw.setCellWidget(row, 4, values_combo) - - def read_source_field_changed(self,new_index): ''' Process changes in the read_source_field combo box @@ -887,3 +503,463 @@ class PrefixRulesComboBox(NoWheelComboBox): self.setCurrentIndex(idx) else: self.setCurrentIndex(0) + +class GenericRulesTable(QTableWidget): + ''' + Generic methods for managing rows + Add QTableWidget, controls to parent QGroupBox + placeholders for basic methods to be overriden + ''' + + def __init__(self, parent_gb_hl, object_name, prefix_rules, eligible_custom_fields, db): + self.prefix_rules = prefix_rules + self.eligible_custom_fields = eligible_custom_fields + self.db = db + QTableWidget.__init__(self) + self.setObjectName(object_name) + self.layout = parent_gb_hl + + # Add ourselves to the layout + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(sizePolicy) + self.setMaximumSize(QSize(16777215, 118)) + self.setColumnCount(0) + self.setRowCount(0) + self.layout.addWidget(self) + + + self._init_table_widget() + self._init_controls() + self._initialize() + + def _init_controls(self): + # Add the control set + vbl = QVBoxLayout() + self.move_rule_up_tb = QToolButton() + self.move_rule_up_tb.setObjectName("move_rule_up_tb") + self.move_rule_up_tb.setToolTip('Move rule up') + self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) + self.move_rule_up_tb.clicked.connect(self.move_row_up) + vbl.addWidget(self.move_rule_up_tb) + + self.add_rule_tb = QToolButton() + self.add_rule_tb.setObjectName("add_rule_tb") + self.add_rule_tb.setToolTip('Add a new rule') + self.add_rule_tb.setIcon(QIcon(I('plus.png'))) + self.add_rule_tb.clicked.connect(self.add_row) + vbl.addWidget(self.add_rule_tb) + + self.delete_rule_tb = QToolButton() + self.delete_rule_tb.setObjectName("delete_rule_tb") + self.delete_rule_tb.setToolTip('Delete selected rule') + self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) + self.delete_rule_tb.clicked.connect(self.delete_row) + vbl.addWidget(self.delete_rule_tb) + + self.move_rule_down_tb = QToolButton() + self.move_rule_down_tb.setObjectName("move_rule_down_tb") + self.move_rule_down_tb.setToolTip('Move rule down') + self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) + self.move_rule_down_tb.clicked.connect(self.move_row_down) + vbl.addWidget(self.move_rule_down_tb) + + self.layout.addLayout(vbl) + + def _init_table_widget(self): + ''' + Override this in the specific instance + ''' + pass + + def _initialize(self): + ''' + Override this in the specific instance + ''' + pass + + def add_row(self): + self.setFocus() + row = self.currentRow() + 1 + self.insertRow(row) + self.populate_table_row(row, self.create_blank_row_data()) + self.select_and_scroll_to_row(row) + self.resizeColumnsToContents() + # Just in case table was empty + self.horizontalHeader().setStretchLastSection(True) + + def convert_row_to_data(self): + ''' + override + ''' + pass + + def create_blank_row_data(self): + ''' + ovverride + ''' + pass + + def delete_row(self): + self.setFocus() + rows = self.selectionModel().selectedRows() + if len(rows) == 0: + return + message = '

Are you sure you want to delete this rule?' + if len(rows) > 1: + message = '

Are you sure you want to delete the %d selected rules?'%len(rows) + if not question_dialog(self, _('Are you sure?'), message, show_copy_button=False): + return + first_sel_row = self.currentRow() + for selrow in reversed(rows): + self.removeRow(selrow.row()) + if first_sel_row < self.rowCount(): + self.select_and_scroll_to_row(first_sel_row) + elif self.rowCount() > 0: + self.select_and_scroll_to_row(first_sel_row - 1) + + def get_data(self): + pass + + def move_row_down(self): + self.setFocus() + rows = self.selectionModel().selectedRows() + if len(rows) == 0: + return + last_sel_row = rows[-1].row() + if last_sel_row == self.rowCount() - 1: + return + + self.blockSignals(True) + for selrow in reversed(rows): + dest_row = selrow.row() + 1 + src_row = selrow.row() + + # Save the contents of the destination row + saved_data = self.convert_row_to_data(dest_row) + + # Remove the destination row + self.removeRow(dest_row) + + # Insert a new row at the original location + self.insertRow(src_row) + + # Populate it with the saved data + self.populate_table_row(src_row, saved_data) + self.blockSignals(False) + scroll_to_row = last_sel_row + 1 + if scroll_to_row < self.rowCount() - 1: + scroll_to_row = scroll_to_row + 1 + self.scrollToItem(self.item(scroll_to_row, 0)) + + def move_row_up(self): + self.setFocus() + rows = self.selectionModel().selectedRows() + if len(rows) == 0: + return + first_sel_row = rows[0].row() + if first_sel_row <= 0: + return + self.blockSignals(True) + for selrow in rows: + # Save the row above + saved_data = self.convert_row_to_data(selrow.row() - 1) + + # Add a row below us with the source data + self.insertRow(selrow.row() + 1) + self.populate_table_row(selrow.row() + 1, saved_data) + + # Delete the row above + self.removeRow(selrow.row() - 1) + self.blockSignals(False) + + scroll_to_row = first_sel_row - 1 + if scroll_to_row > 0: + scroll_to_row = scroll_to_row - 1 + self.scrollToItem(self.item(scroll_to_row, 0)) + + def populate_table_row(self): + ''' + override + ''' + pass + +class PrefixRules(GenericRulesTable): + + def _init_table_widget(self): + header_labels = ['','Name','Prefix','Source','Pattern'] + self.setColumnCount(len(header_labels)) + self.setHorizontalHeaderLabels(header_labels) + self.setSortingEnabled(False) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + + def _initialize(self): + self.generate_prefix_list() + self.populate() + self.resizeColumnsToContents() + self.resize_name(1.5) + self.horizontalHeader().setStretchLastSection(True) + + def convert_row_to_data(self, row): + data = self.create_blank_row_data() + data['ordinal'] = row + data['enabled'] = self.item(row,0).checkState() == Qt.Checked + data['name'] = unicode(self.cellWidget(row,1).text()).strip() + data['prefix'] = unicode(self.cellWidget(row,2).currentText()).strip() + data['field'] = unicode(self.cellWidget(row,3).currentText()).strip() + data['pattern'] = unicode(self.cellWidget(row,4).currentText()).strip() + return data + + def create_blank_row_data(self): + data = {} + data['ordinal'] = -1 + data['enabled'] = False + data['name'] = 'New rule' + data['field'] = '' + data['pattern'] = '' + data['prefix'] = '' + return data + + def generate_prefix_list(self): + def prefix_sorter(item): + key = item + if item[0] == "_": + key = 'zzz' + item + return key + + # Create a list of prefixes for user selection + raw_prefix_list = [ + ('Ampersand',u'&'), + ('Angle left double',u'\u00ab'), + ('Angle left',u'\u2039'), + ('Angle right double',u'\u00bb'), + ('Angle right',u'\u203a'), + ('Arrow carriage return',u'\u21b5'), + ('Arrow double',u'\u2194'), + ('Arrow down',u'\u2193'), + ('Arrow left',u'\u2190'), + ('Arrow right',u'\u2192'), + ('Arrow up',u'\u2191'), + ('Asterisk',u'*'), + ('At sign',u'@'), + ('Bullet smallest',u'\u22c5'), + ('Bullet small',u'\u00b7'), + ('Bullet',u'\u2022'), + ('Cards clubs',u'\u2663'), + ('Cards diamonds',u'\u2666'), + ('Cards hearts',u'\u2665'), + ('Cards spades',u'\u2660'), + ('Caret',u'^'), + ('Checkmark',u'\u2713'), + ('Copyright circle c',u'\u00a9'), + ('Copyright circle r',u'\u00ae'), + ('Copyright trademark',u'\u2122'), + ('Currency cent',u'\u00a2'), + ('Currency dollar',u'$'), + ('Currency euro',u'\u20ac'), + ('Currency pound',u'\u00a3'), + ('Currency yen',u'\u00a5'), + ('Dagger double',u'\u2021'), + ('Dagger',u'\u2020'), + ('Degree',u'\u00b0'), + ('Dots3',u'\u2234'), + ('Hash',u'#'), + ('Infinity',u'\u221e'), + ('Lozenge',u'\u25ca'), + ('Math divide',u'\u00f7'), + ('Math empty',u'\u2205'), + ('Math equals',u'='), + ('Math minus',u'\u2212'), + ('Math plus circled',u'\u2295'), + ('Math times circled',u'\u2297'), + ('Math times',u'\u00d7'), + ('O slash',u'\u00d8'), + ('Paragraph',u'\u00b6'), + ('Percent',u'%'), + ('Plus-or-minus',u'\u00b1'), + ('Plus',u'+'), + ('Punctuation colon',u':'), + ('Punctuation colon-semi',u';'), + ('Punctuation exclamation',u'!'), + ('Punctuation question',u'?'), + ('Punctuation period',u'.'), + ('Punctuation slash back',u'\\'), + ('Punctuation slash forward',u'/'), + ('Section',u'\u00a7'), + ('Tilde',u'~'), + ('Vertical bar',u'|'), + ('Vertical bar broken',u'\u00a6'), + ('_0',u'0'), + ('_1',u'1'), + ('_2',u'2'), + ('_3',u'3'), + ('_4',u'4'), + ('_5',u'5'), + ('_6',u'6'), + ('_7',u'7'), + ('_8',u'8'), + ('_9',u'9'), + ('_A',u'A'), + ('_B',u'B'), + ('_C',u'C'), + ('_D',u'D'), + ('_E',u'E'), + ('_F',u'F'), + ('_G',u'G'), + ('_H',u'H'), + ('_I',u'I'), + ('_J',u'J'), + ('_K',u'K'), + ('_L',u'L'), + ('_M',u'M'), + ('_N',u'N'), + ('_O',u'O'), + ('_P',u'P'), + ('_Q',u'Q'), + ('_R',u'R'), + ('_S',u'S'), + ('_T',u'T'), + ('_U',u'U'), + ('_V',u'V'), + ('_W',u'W'), + ('_X',u'X'), + ('_Y',u'Y'), + ('_Z',u'Z'), + ('_a',u'a'), + ('_b',u'b'), + ('_c',u'c'), + ('_d',u'd'), + ('_e',u'e'), + ('_f',u'f'), + ('_g',u'g'), + ('_h',u'h'), + ('_i',u'i'), + ('_j',u'j'), + ('_k',u'k'), + ('_l',u'l'), + ('_m',u'm'), + ('_n',u'n'), + ('_o',u'o'), + ('_p',u'p'), + ('_q',u'q'), + ('_r',u'r'), + ('_s',u's'), + ('_t',u't'), + ('_u',u'u'), + ('_v',u'v'), + ('_w',u'w'), + ('_x',u'x'), + ('_y',u'y'), + ('_z',u'z'), + ] + raw_prefix_list = sorted(raw_prefix_list, key=prefix_sorter) + self.prefix_list = [x[1] for x in raw_prefix_list] + + def get_data(self): + data_items = [] + for row in range(self.rowCount()): + data = self.convert_row_to_data(row) + data_items.append( + {'ordinal':data['ordinal'], + 'enabled':data['enabled'], + 'name':data['name'], + 'field':data['field'], + 'pattern':data['pattern'], + 'prefix':data['prefix']}) + return data_items + + def populate(self): + # Format of rules list is different if default values vs retrieved JSON + # Hack to normalize list style + rules = self.prefix_rules + if type(rules[0]) is list: + rules = rules[0] + self.setFocus() + rules = sorted(rules, key=lambda k: k['ordinal']) + for row, rule in enumerate(rules): + self.insertRow(row) + self.select_and_scroll_to_row(row) + self.populate_table_row(row, rule) + self.selectRow(0) + + def populate_table_row(self, row, data): + + def set_prefix_field_in_row(row, col, field=''): + prefix_combo = PrefixRulesComboBox(self, self.prefix_list, field) + self.setCellWidget(row, col, prefix_combo) + + def set_rule_name_in_row(row, col, name=''): + rule_name = QLineEdit(name) + rule_name.home(False) + rule_name.editingFinished.connect(self.rule_name_edited) + self.setCellWidget(row, col, rule_name) + + def set_source_field_in_row(row, col, field=''): + source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) + source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row)) + self.setCellWidget(row, col, source_combo) + return source_combo + + + # Entry point + self.blockSignals(True) + #print("prefix_rules_populate_table_row processing rule:\n%s\n" % data) + + # Column 0: Enabled + self.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) + + # Column 1: Rule name + #rule_name = QTableWidgetItem(data['name']) + set_rule_name_in_row(row, 1, name=data['name']) + + # Column 2: Prefix + set_prefix_field_in_row(row, 2, field=data['prefix']) + + # Column 3: Source field + source_combo = set_source_field_in_row(row, 3, field=data['field']) + + # Column 4: Pattern + # The contents of the Pattern field is driven by the Source field + self.source_index_changed(source_combo, row, 4, pattern=data['pattern']) + + self.blockSignals(False) + + def resize_name(self, scale): + current_width = self.columnWidth(1) + self.setColumnWidth(1, min(225,int(current_width * scale))) + + def rule_name_edited(self): + current_row = self.currentRow() + self.cellWidget(current_row,1).home(False) + self.setFocus() + self.select_and_scroll_to_row(current_row) + + def select_and_scroll_to_row(self, row): + self.selectRow(row) + self.scrollToItem(self.currentItem()) + + def source_index_changed(self, combo, row, col, pattern=''): + # Populate the Pattern field based upon the Source field + + source_field = str(combo.currentText()) + if source_field == '': + values = [] + elif source_field == 'Tags': + values = sorted(self.db.all_tags(), key=sort_key) + else: + if self.eligible_custom_fields[source_field]['datatype'] in ['enumeration', 'text']: + values = self.db.all_custom(self.db.field_metadata.key_to_label( + self.eligible_custom_fields[source_field]['field'])) + values = sorted(values, key=sort_key) + elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']: + values = ['True','False','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: + values = ['any date','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: + values = ['any value','unspecified'] + + values_combo = PrefixRulesComboBox(self, values, pattern) + self.setCellWidget(row, 4, values_combo) + diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index eb39415a81..bf10cacd38 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -327,7 +327,7 @@ Default: ~,Catalog - + 0 @@ -342,56 +342,7 @@ Default: ~,Catalog - - - - - - 0 - 0 - - - - - 16777215 - 118 - - - - - - - - - - ... - - - - - - - ... - - - - - - - ... - - - - - - - ... - - - - - - + diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index ee43d21357..a7da345dc2 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -47,28 +47,44 @@ class EPUB_MOBI(CatalogPlugin): "of the conversion process a bug is occurring.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), - Option('--exclude-book-marker', - default=':', - dest='exclude_book_marker', - action = None, - help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n" - "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n" - "Default: '%default'\n" - "Applies to ePub, MOBI output formats")), Option('--exclude-genre', default='\[.+\]', dest='exclude_genre', action = None, help=_("Regex describing tags to exclude as genres.\n" "Default: '%default' excludes bracketed tags, e.g. '[]'\n" "Applies to: ePub, MOBI output formats")), - Option('--exclude-tags', - default=('~,'+_('Catalog')), - dest='exclude_tags', - action = None, - help=_("Comma-separated list of tag words indicating book should be excluded from output. " - "For example: 'skip' will match 'skip this book' and 'Skip will like this'. " - "Default: '%default'\n" - "Applies to: ePub, MOBI output formats")), + +# Option('--exclude-book-marker', +# default=':', +# dest='exclude_book_marker', +# action = None, +# help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n" +# "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n" +# "Default: '%default'\n" +# "Applies to ePub, MOBI output formats")), +# Option('--exclude-tags', +# default=('~,Catalog'), +# dest='exclude_tags', +# action = None, +# help=_("Comma-separated list of tag words indicating book should be excluded from output. " +# "For example: 'skip' will match 'skip this book' and 'Skip will like this'. " +# "Default:'%default'\n" +# "Applies to: ePub, MOBI output formats")), + + Option('--exclusion-rules', + default="(('Excluded tags','Tags','~,Catalog'),)", + dest='exclusion_rules', + action=None, + help=_("Specifies the rules used to exclude books from the generated catalog.\n" + "The model for an exclusion rule is either\n('','Tags','') or\n" + "('','','').\n" + "For example:\n" + "(('Archived books','#status','Archived'),)\n" + "will exclude a book with a value of 'Archived' in the custom column 'status'.\n" + "When multiple rules are defined, all rules will be applied.\n" + "Default: \n" + '"' + '%default' + '"' + "\n" + "Applies to ePub, MOBI output formats")), + Option('--generate-authors', default=False, dest='generate_authors', @@ -142,7 +158,7 @@ class EPUB_MOBI(CatalogPlugin): help=_("Specifies the rules used to include prefixes indicating read books, wishlist items and other user-specifed prefixes.\n" "The model for a prefix rule is ('','','','').\n" "When multiple rules are defined, the first matching rule will be used.\n" - "Default: '%default'\n" + "Default:\n" + '"' + '%default' + '"' + "\n" "Applies to ePub, MOBI output formats")), Option('--thumb-width', default='1.0', @@ -285,6 +301,17 @@ class EPUB_MOBI(CatalogPlugin): if len(rule) != 4: log.error("incorrect number of args for --prefix-rules: %s" % repr(rule)) + # eval exclusion_rules if passed from command line + if type(opts.exclusion_rules) is not tuple: + try: + opts.exclusion_rules = eval(opts.exclusion_rules) + except: + log.error("malformed --exclusion-rules: %s" % opts.exclusion_rules) + raise + for rule in opts.exclusion_rules: + if len(rule) != 3: + log.error("incorrect number of args for --exclusion-rules: %s" % repr(rule)) + # Display opts keys = opts_dict.keys() keys.sort() @@ -292,6 +319,7 @@ class EPUB_MOBI(CatalogPlugin): for key in keys: if key in ['catalog_title','authorClip','connected_kindle','descriptionClip', 'exclude_book_marker','exclude_genre','exclude_tags', + 'exclusion_rules', 'header_note_source_field','merge_comments', 'output_profile','prefix_rules','read_book_marker', 'search_text','sort_by','sort_descriptions_by_author','sync', From c080004682416308769663c289e0487c1922d806 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 5 Aug 2012 19:51:45 +0200 Subject: [PATCH 12/27] Silly bug in smart device flood control --- src/calibre/devices/smart_device_app/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 4fe815dc80..b88c97a279 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -586,7 +586,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): # the device. raise OpenFailed('') try: - peer = self.device_socket.getpeername() + peer = self.device_socket.getpeername()[0] self.connection_attempts[peer] = 0 except: pass From af3f6a62d9d1c27d3cba7a748aad0bab633d6364 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 6 Aug 2012 11:30:15 +0530 Subject: [PATCH 13/27] MTP driver: Get storage information --- src/calibre/devices/interface.py | 2 +- src/calibre/devices/mtp/unix/driver.py | 77 +++++++++++++++++- src/calibre/devices/mtp/unix/libmtp.c | 104 ++++++++++++++++++++++--- 3 files changed, 166 insertions(+), 17 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index fc4e5a2a60..4fd5fea252 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -297,7 +297,7 @@ class DevicePlugin(Plugin): :return: (device name, device version, software version on device, mime type) The tuple can optionally have a fifth element, which is a - drive information diction. See usbms.driver for an example. + drive information dictionary. See usbms.driver for an example. """ raise NotImplementedError() diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 081b09975f..91acae8a9d 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import time +import time, operator from threading import RLock from functools import wraps @@ -33,6 +33,11 @@ class MTP_DEVICE(MTPDeviceBase): self.lock = RLock() self.blacklisted_devices = set() + @synchronous + def get_gui_name(self): + if self.dev is None or not self.dev.friendly_name: return self.name + return self.dev.friendly_name + @synchronous def is_usb_connected(self, devices_on_system, debug=False, only_presence=False): @@ -62,6 +67,10 @@ class MTP_DEVICE(MTPDeviceBase): def post_yank_cleanup(self): self.dev = None + @synchronous + def shutdown(self): + self.dev = None + @synchronous def open(self, connected_device, library_uuid): def blacklist_device(): @@ -69,18 +78,78 @@ class MTP_DEVICE(MTPDeviceBase): self.blacklisted_devices.add((d.busnum, d.devnum, d.vendor_id, d.product_id, d.bcd, d.serial)) try: - self.detect.create_device(connected_device) + self.dev = self.detect.create_device(connected_device) except ValueError: # Give the device some time to settle time.sleep(2) try: - self.detect.create_device(connected_device) + self.dev = self.detect.create_device(connected_device) except ValueError: # Black list this device so that it is ignored for the # remainder of this session. blacklist_device() - raise OpenFailed('%s is not a MTP device'%connected_device) + raise OpenFailed('%s is not a MTP device'%(connected_device,)) except TypeError: blacklist_device() raise OpenFailed('') + storage = sorted(self.dev.storage_info, key=operator.itemgetter('id')) + if not storage: + blacklist_device() + raise OpenFailed('No storage found for device %s'%(connected_device,)) + self._main_id = storage[0]['id'] + self._carda_id = self._cardb_id = None + if len(storage) > 1: + self._carda_id = storage[1]['id'] + if len(storage) > 2: + self._cardb_id = storage[2]['id'] + + @synchronous + def get_device_information(self, end_session=True): + d = self.dev + return (d.friendly_name, d.device_version, d.device_version, '') + + @synchronous + def card_prefix(self, end_session=True): + ans = [None, None] + if self._carda_id is not None: + ans[0] = 'mtp:%d:'%self._carda_id + if self._cardb_id is not None: + ans[1] = 'mtp:%d:'%self._cardb_id + return tuple(ans) + + @synchronous + def total_space(self, end_session=True): + ans = [0, 0, 0] + for s in self.dev.storage_info: + i = {self._main_id:0, self._carda_id:1, + self._cardb_id:2}.get(s['id'], None) + if i is not None: + ans[i] = s['capacity'] + return tuple(ans) + + @synchronous + def free_space(self, end_session=True): + self.dev.update_storage_info() + ans = [0, 0, 0] + for s in self.dev.storage_info: + i = {self._main_id:0, self._carda_id:1, + self._cardb_id:2}.get(s['id'], None) + if i is not None: + ans[i] = s['freespace_bytes'] + return tuple(ans) + + +if __name__ == '__main__': + from pprint import pprint + dev = MTP_DEVICE(None) + from calibre.devices.scanner import linux_scanner + devs = linux_scanner() + mtp_devs = dev.detect(devs) + dev.open(list(mtp_devs)[0], 'xxx') + d = dev.dev + print ("Opened device:", dev.get_gui_name()) + print ("Storage info:") + pprint(d.storage_info) + print("Free space:", dev.free_space()) + diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c index 0a818cfc66..d22f294157 100644 --- a/src/calibre/devices/mtp/unix/libmtp.c +++ b/src/calibre/devices/mtp/unix/libmtp.c @@ -6,6 +6,24 @@ #include "devices.h" +#define ENSURE_DEV(rval) \ + if (self->device == NULL) { \ + PyErr_SetString(PyExc_ValueError, "This device has not been initialized."); \ + return rval; \ + } + +// Storage types +#define ST_Undefined 0x0000 +#define ST_FixedROM 0x0001 +#define ST_RemovableROM 0x0002 +#define ST_FixedRAM 0x0003 +#define ST_RemovableRAM 0x0004 + +// Storage Access capability +#define AC_ReadWrite 0x0000 +#define AC_ReadOnly 0x0001 +#define AC_ReadOnly_with_Object_Deletion 0x0002 + // Device object definition {{{ typedef struct { PyObject_HEAD @@ -20,6 +38,7 @@ typedef struct { } libmtp_Device; +// Device.__init__() {{{ static void libmtp_Device_dealloc(libmtp_Device* self) { @@ -119,44 +138,100 @@ libmtp_Device_init(libmtp_Device *self, PyObject *args, PyObject *kwds) return 0; } +// }}} -// Collator.friendly_name {{{ +// Device.friendly_name {{{ static PyObject * libmtp_Device_friendly_name(libmtp_Device *self, void *closure) { - return Py_BuildValue("O", self->friendly_name); + Py_INCREF(self->friendly_name); return self->friendly_name; } // }}} -// Collator.manufacturer_name {{{ +// Device.manufacturer_name {{{ static PyObject * libmtp_Device_manufacturer_name(libmtp_Device *self, void *closure) { - return Py_BuildValue("O", self->manufacturer_name); + Py_INCREF(self->manufacturer_name); return self->manufacturer_name; } // }}} -// Collator.model_name {{{ +// Device.model_name {{{ static PyObject * libmtp_Device_model_name(libmtp_Device *self, void *closure) { - return Py_BuildValue("O", self->model_name); + Py_INCREF(self->model_name); return self->model_name; } // }}} -// Collator.serial_number {{{ +// Device.serial_number {{{ static PyObject * libmtp_Device_serial_number(libmtp_Device *self, void *closure) { - return Py_BuildValue("O", self->serial_number); + Py_INCREF(self->serial_number); return self->serial_number; } // }}} -// Collator.device_version {{{ +// Device.device_version {{{ static PyObject * libmtp_Device_device_version(libmtp_Device *self, void *closure) { - return Py_BuildValue("O", self->device_version); + Py_INCREF(self->device_version); return self->device_version; } // }}} -// Collator.ids {{{ +// Device.ids {{{ static PyObject * libmtp_Device_ids(libmtp_Device *self, void *closure) { - return Py_BuildValue("O", self->ids); + Py_INCREF(self->ids); return self->ids; +} // }}} + +// Device.update_storage_info() {{{ +static PyObject* +libmtp_Device_update_storage_info(libmtp_Device *self, PyObject *args, PyObject *kwargs) { + ENSURE_DEV(NULL); + if (LIBMTP_Get_Storage(self->device, LIBMTP_STORAGE_SORTBY_NOTSORTED) < 0) { + PyErr_SetString(PyExc_RuntimeError, "Failed to get storage infor for device."); + return NULL; + } + Py_RETURN_NONE; +} +// }}} + +// Device.storage_info {{{ +static PyObject * +libmtp_Device_storage_info(libmtp_Device *self, void *closure) { + PyObject *ans, *loc; + LIBMTP_devicestorage_t *storage; + ENSURE_DEV(NULL); + if (self->device->storage == NULL) { PyErr_SetString(PyExc_RuntimeError, "The device has no storage information."); return NULL; } + + ans = PyList_New(0); + if (ans == NULL) { PyErr_NoMemory(); return NULL; } + + for (storage = self->device->storage; storage != NULL; storage = storage->next) { + // Ignore read only storage + if (storage->StorageType == ST_FixedROM || storage->StorageType == ST_RemovableROM) continue; + // Storage IDs with the lower 16 bits 0x0000 are not supposed to be + // writeable. + if ((storage->id & 0x0000FFFFU) == 0x00000000U) continue; + // Also check the access capability to avoid e.g. deletable only storages + if (storage->AccessCapability == AC_ReadOnly || storage->AccessCapability == AC_ReadOnly_with_Object_Deletion) continue; + + loc = Py_BuildValue("{s:I,s:O,s:K,s:K,s:K,s:s,s:s}", + "id", storage->id, + "removable", ((storage->StorageType == ST_RemovableRAM) ? Py_True : Py_False), + "capacity", storage->MaxCapacity, + "freespace_bytes", storage->FreeSpaceInBytes, + "freespace_objects", storage->FreeSpaceInObjects, + "storage_desc", storage->StorageDescription, + "volume_id", storage->VolumeIdentifier + ); + + if (loc == NULL) return NULL; + if (PyList_Append(ans, loc) != 0) return NULL; + Py_DECREF(loc); + + } + + return ans; } // }}} static PyMethodDef libmtp_Device_methods[] = { + {"update_storage_info", (PyCFunction)libmtp_Device_update_storage_info, METH_VARARGS, + "update_storage_info() -> Reread the storage info from the device (total, space, free space, storage locations, etc.)" + }, + {NULL} /* Sentinel */ }; @@ -191,6 +266,11 @@ static PyGetSetDef libmtp_Device_getsetters[] = { (char *)"The ids of the device (busnum, devnum, vendor_id, product_id, usb_serialnum)", NULL}, + {(char *)"storage_info", + (getter)libmtp_Device_storage_info, NULL, + (char *)"Information about the storage locations on the device. Returns a list of dictionaries where each dictionary corresponds to the LIBMTP_devicestorage_struct.", + NULL}, + {NULL} /* Sentinel */ }; From 4c64613a4dd7a323f75af2c1ee91dc64d28ce12b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 6 Aug 2012 11:52:51 +0530 Subject: [PATCH 14/27] ... --- src/calibre/devices/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 4fd5fea252..64aff3bad2 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -607,7 +607,7 @@ class BookList(list): pass def supports_collections(self): - ''' Return True if the the device supports collections for this book list. ''' + ''' Return True if the device supports collections for this book list. ''' raise NotImplementedError() def add_book(self, book, replace_metadata): From 498c9ba98cd860dc0e829c69fb34c2687b8aa151 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 6 Aug 2012 17:02:43 +0530 Subject: [PATCH 15/27] MTP driver: get files and folders on device --- src/calibre/devices/mtp/base.py | 10 +- src/calibre/devices/mtp/unix/driver.py | 12 ++ src/calibre/devices/mtp/unix/libmtp.c | 169 ++++++++++++++++++++++++- 3 files changed, 185 insertions(+), 6 deletions(-) diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 04dd2a0034..f29f525b30 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -8,9 +8,8 @@ __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' from calibre.devices.interface import DevicePlugin -from calibre.devices.usbms.deviceconfig import DeviceConfig -class MTPDeviceBase(DeviceConfig, DevicePlugin): +class MTPDeviceBase(DevicePlugin): name = 'SmartDevice App Interface' gui_name = _('MTP Device') icon = I('devices/galaxy_s3.png') @@ -28,7 +27,14 @@ class MTPDeviceBase(DeviceConfig, DevicePlugin): BACKLOADING_ERROR_MESSAGE = None + def __init__(self, *args, **kwargs): + DevicePlugin.__init__(self, *args, **kwargs) + self.progress_reporter = None + def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None): pass + def set_progress_reporter(self, report_progress): + self.progress_reporter = report_progress + diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 91acae8a9d..b8d6854fe5 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -33,6 +33,14 @@ class MTP_DEVICE(MTPDeviceBase): self.lock = RLock() self.blacklisted_devices = set() + def report_progress(self, sent, total): + try: + p = int(sent/total * 100) + except ZeroDivisionError: + p = 100 + if self.progress_reporter is not None: + self.progress_reporter(p) + @synchronous def get_gui_name(self): if self.dev is None or not self.dev.friendly_name: return self.name @@ -152,4 +160,8 @@ if __name__ == '__main__': print ("Storage info:") pprint(d.storage_info) print("Free space:", dev.free_space()) + files, errs = d.get_filelist(dev) + pprint((len(files), errs)) + folders, errs = d.get_folderlist() + pprint((len(folders), errs)) diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c index d22f294157..f748954936 100644 --- a/src/calibre/devices/mtp/unix/libmtp.c +++ b/src/calibre/devices/mtp/unix/libmtp.c @@ -6,12 +6,19 @@ #include "devices.h" +// Macros and utilities #define ENSURE_DEV(rval) \ if (self->device == NULL) { \ PyErr_SetString(PyExc_ValueError, "This device has not been initialized."); \ return rval; \ } +#define ENSURE_STORAGE(rval) \ + if (self->device->storage == NULL) { \ + PyErr_SetString(PyExc_RuntimeError, "The device has no storage information."); \ + return rval; \ + } + // Storage types #define ST_Undefined 0x0000 #define ST_FixedROM 0x0001 @@ -24,6 +31,41 @@ #define AC_ReadOnly 0x0001 #define AC_ReadOnly_with_Object_Deletion 0x0002 +typedef struct { + PyObject *obj; + PyThreadState *state; +} ProgressCallback; + +static int report_progress(uint64_t const sent, uint64_t const total, void const *const data) { + PyObject *res; + ProgressCallback *cb; + + cb = (ProgressCallback *)data; + if (cb->obj != NULL) { + PyEval_RestoreThread(cb->state); + res = PyObject_CallMethod(cb->obj, "report_progress", "KK", sent, total); + Py_XDECREF(res); + cb->state = PyEval_SaveThread(); + } + return 0; +} + +static void dump_errorstack(LIBMTP_mtpdevice_t *dev, PyObject *list) { + LIBMTP_error_t *stack; + PyObject *err; + + for(stack = LIBMTP_Get_Errorstack(dev); stack != NULL; stack=stack->next) { + err = Py_BuildValue("Is", stack->errornumber, stack->error_text); + if (err == NULL) break; + PyList_Append(list, err); + Py_DECREF(err); + } + + LIBMTP_Clear_Errorstack(dev); +} + +// }}} + // Device object definition {{{ typedef struct { PyObject_HEAD @@ -88,7 +130,9 @@ libmtp_Device_init(libmtp_Device *self, PyObject *args, PyObject *kwds) } } - dev = LIBMTP_Open_Raw_Device_Uncached(&rawdev); + // Note that contrary to what the libmtp docs imply, we cannot use + // LIBMTP_Open_Raw_Device_Uncached as using it causes file listing to fail + dev = LIBMTP_Open_Raw_Device(&rawdev); Py_END_ALLOW_THREADS; if (dev == NULL) { @@ -193,8 +237,7 @@ static PyObject * libmtp_Device_storage_info(libmtp_Device *self, void *closure) { PyObject *ans, *loc; LIBMTP_devicestorage_t *storage; - ENSURE_DEV(NULL); - if (self->device->storage == NULL) { PyErr_SetString(PyExc_RuntimeError, "The device has no storage information."); return NULL; } + ENSURE_DEV(NULL); ENSURE_STORAGE(NULL); ans = PyList_New(0); if (ans == NULL) { PyErr_NoMemory(); return NULL; } @@ -208,7 +251,7 @@ libmtp_Device_storage_info(libmtp_Device *self, void *closure) { // Also check the access capability to avoid e.g. deletable only storages if (storage->AccessCapability == AC_ReadOnly || storage->AccessCapability == AC_ReadOnly_with_Object_Deletion) continue; - loc = Py_BuildValue("{s:I,s:O,s:K,s:K,s:K,s:s,s:s}", + loc = Py_BuildValue("{s:k,s:O,s:K,s:K,s:K,s:s,s:s}", "id", storage->id, "removable", ((storage->StorageType == ST_RemovableRAM) ? Py_True : Py_False), "capacity", storage->MaxCapacity, @@ -227,11 +270,129 @@ libmtp_Device_storage_info(libmtp_Device *self, void *closure) { return ans; } // }}} +// Device.get_filelist {{{ +static PyObject * +libmtp_Device_get_filelist(libmtp_Device *self, PyObject *args, PyObject *kwargs) { + PyObject *ans, *fo, *callback = NULL, *errs; + ProgressCallback cb; + LIBMTP_file_t *f, *tf; + + ENSURE_DEV(NULL); ENSURE_STORAGE(NULL); + + + if (!PyArg_ParseTuple(args, "|O", &callback)) return NULL; + cb.obj = callback; + + ans = PyList_New(0); + errs = PyList_New(0); + if (ans == NULL || errs == NULL) { PyErr_NoMemory(); return NULL; } + + cb.state = PyEval_SaveThread(); + tf = LIBMTP_Get_Filelisting_With_Callback(self->device, report_progress, &cb); + PyEval_RestoreThread(cb.state); + + if (tf == NULL) { + dump_errorstack(self->device, errs); + return Py_BuildValue("NN", ans, errs); + } + + for (f=tf; f != NULL; f=f->next) { + fo = Py_BuildValue("{s:k,s:k,s:k,s:s,s:K,s:k}", + "id", f->item_id, + "parent_id", f->parent_id, + "storage_id", f->storage_id, + "filename", f->filename, + "size", f->filesize, + "modtime", f->modificationdate + ); + if (fo == NULL || PyList_Append(ans, fo) != 0) break; + Py_DECREF(fo); + } + + // Release memory + f = tf; + while (f != NULL) { + tf = f; f = f->next; LIBMTP_destroy_file_t(tf); + } + + if (callback != NULL) { + // Bug in libmtp where it does not call callback with 100% + fo = PyObject_CallMethod(callback, "report_progress", "KK", PyList_Size(ans), PyList_Size(ans)); + Py_XDECREF(fo); + } + + return Py_BuildValue("NN", ans, errs); +} // }}} + +// Device.get_folderlist {{{ + +int folderiter(LIBMTP_folder_t *f, PyObject *parent) { + PyObject *folder, *children; + + children = PyList_New(0); + if (children == NULL) { PyErr_NoMemory(); return 1;} + + folder = Py_BuildValue("{s:k,s:k,s:k,s:s,s:N}", + "id", f->folder_id, + "parent_d", f->parent_id, + "storage_id", f->storage_id, + "name", f->name, + "children", children); + if (folder == NULL) return 1; + PyList_Append(parent, folder); + Py_DECREF(folder); + + if (f->sibling != NULL) { + if (folderiter(f->sibling, parent)) return 1; + } + + if (f->child != NULL) { + if (folderiter(f->child, children)) return 1; + } + + return 0; +} + +static PyObject * +libmtp_Device_get_folderlist(libmtp_Device *self, PyObject *args, PyObject *kwargs) { + PyObject *ans, *errs; + LIBMTP_folder_t *f; + + ENSURE_DEV(NULL); ENSURE_STORAGE(NULL); + + ans = PyList_New(0); + errs = PyList_New(0); + if (errs == NULL || ans == NULL) { PyErr_NoMemory(); return NULL; } + + Py_BEGIN_ALLOW_THREADS; + f = LIBMTP_Get_Folder_List(self->device); + Py_END_ALLOW_THREADS; + + if (f == NULL) { + dump_errorstack(self->device, errs); + return Py_BuildValue("NN", ans, errs); + } + + if (folderiter(f, ans)) return NULL; + LIBMTP_destroy_folder_t(f); + + return Py_BuildValue("NN", ans, errs); + +} // }}} + static PyMethodDef libmtp_Device_methods[] = { {"update_storage_info", (PyCFunction)libmtp_Device_update_storage_info, METH_VARARGS, "update_storage_info() -> Reread the storage info from the device (total, space, free space, storage locations, etc.)" }, + {"get_filelist", (PyCFunction)libmtp_Device_get_filelist, METH_VARARGS, + "get_filelist(callback=None) -> Get the list of files on the device. callback must be an object that has a method named 'report_progress(current, total)'. Returns files, errors." + }, + + {"get_folderlist", (PyCFunction)libmtp_Device_get_folderlist, METH_VARARGS, + "get_folderlist() -> Get the list of folders on the device. Returns files, erros." + }, + {NULL} /* Sentinel */ }; From 24aab4fb5795e557ab1468a693907e310edfdd25 Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 6 Aug 2012 06:17:18 -0600 Subject: [PATCH 16/27] exclusion_rules_table added --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 249 +++++++++++++++--- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 190 +++---------- src/calibre/library/catalogs/epub_mobi.py | 22 +- .../library/catalogs/epub_mobi_builder.py | 93 +++++-- 4 files changed, 315 insertions(+), 239 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index ac251ff801..8a2035c76f 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -38,7 +38,7 @@ class PluginWidget(QWidget,Ui_Form): self._initControlArrays() def _initControlArrays(self): - + # Default values for controls CheckBoxControls = [] ComboBoxControls = [] DoubleSpinBoxControls = [] @@ -72,13 +72,27 @@ class PluginWidget(QWidget,Ui_Form): # LineEditControls option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit']) - option_fields += zip(['exclude_pattern'],[None],['line_edit']) - option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit']) + #***option_fields += zip(['exclude_pattern'],[None],['line_edit']) + #***option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit']) # SpinBoxControls option_fields += zip(['thumb_width'],[1.00],['spin_box']) - # Prefix rules TableWidget + # Exclusion rules + option_fields += zip(['exclusion_rules_tw','exclusion_rules_tw'], + [{'ordinal':0, + 'enabled':True, + 'name':'Catalogs', + 'field':'Tags', + 'pattern':'Catalog'}, + {'ordinal':1, + 'enabled':False, + 'name':'New rule', + 'field':'', + 'pattern':''}], + ['table_widget','table_widget']) + + # Prefix rules option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'], [{'ordinal':0, 'enabled':True, @@ -123,13 +137,13 @@ class PluginWidget(QWidget,Ui_Form): ['exclude_source_field','header_note_source_field', 'merge_source_field'] LineEditControls (c_type: line_edit): - ['exclude_genre','exclude_pattern','exclude_tags'] + ['exclude_genre'] RadioButtonControls (c_type: radio_button): ['merge_before','merge_after'] SpinBoxControls (c_type: spin_box): ['thumb_width'] TableWidgetControls (c_type: table_widget): - ['prefix_rules_tw'] + ['exclusion_rules_tw','prefix_rules_tw'] ''' self.name = name @@ -139,6 +153,7 @@ class PluginWidget(QWidget,Ui_Form): # Update dialog fields from stored options + exclusion_rules = [] prefix_rules = [] for opt in self.OPTION_FIELDS: c_name, c_def, c_type = opt @@ -159,16 +174,22 @@ class PluginWidget(QWidget,Ui_Form): getattr(self, c_name).setChecked(opt_value) elif c_type in ['spin_box']: getattr(self, c_name).setValue(float(opt_value)) + elif c_type in ['table_widget'] and c_name == 'exclusion_rules_tw': + if opt_value not in exclusion_rules: + exclusion_rules.append(opt_value) elif c_type in ['table_widget'] and c_name == 'prefix_rules_tw': if opt_value not in prefix_rules: prefix_rules.append(opt_value) + ''' + *** # Init self.exclude_source_field_name self.exclude_source_field_name = '' cs = unicode(self.exclude_source_field.currentText()) if cs > '': exclude_source_spec = self.exclude_source_fields[cs] self.exclude_source_field_name = exclude_source_spec['field'] + ''' # Init self.merge_source_field_name self.merge_source_field_name = '' @@ -190,10 +211,13 @@ class PluginWidget(QWidget,Ui_Form): # Hook changes to Description section self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) + # Initialize exclusion rules + self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb_hl, + "exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db) + # Initialize prefix rules - self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, "prefix_rules_tw", - prefix_rules, self.eligible_custom_fields, - self.db) + self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, + "prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db) def options(self): # Save/return the current options @@ -204,6 +228,8 @@ class PluginWidget(QWidget,Ui_Form): opts_dict = {} # Save values to gprefs prefix_rules_processed = False + exclusion_rules_processed = False + for opt in self.OPTION_FIELDS: c_name, c_def, c_type = opt if c_type in ['check_box', 'radio_button']: @@ -215,7 +241,10 @@ class PluginWidget(QWidget,Ui_Form): elif c_type in ['spin_box']: opt_value = unicode(getattr(self, c_name).value()) elif c_type in ['table_widget']: - opt_value = self.prefix_rules_table.get_data() + if c_name == 'prefix_rules_tw': + opt_value = self.prefix_rules_table.get_data() + if c_name == 'exclusion_rules_tw': + opt_value = self.exclusion_rules_table.get_data() gprefs.set(self.name + '_' + c_name, opt_value) @@ -246,19 +275,49 @@ class PluginWidget(QWidget,Ui_Form): pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix']) rule_set.append(pr) - opt_value = tuple(rule_set) opts_dict['prefix_rules'] = opt_value prefix_rules_processed = True + elif c_name == 'exclusion_rules_tw': + if exclusion_rules_processed: + continue + rule_set = [] + for rule in opt_value: + # Test for empty name/field/pattern/prefix, continue + # If pattern = any or unspecified, convert to regex + if not rule['enabled']: + continue + elif not rule['field'] or not rule['pattern']: + continue + else: + if rule['field'] != 'Tags': + # Look up custom column name + #print(self.eligible_custom_fields[rule['field']]['field']) + rule['field'] = self.eligible_custom_fields[rule['field']]['field'] + if rule['pattern'].startswith('any'): + rule['pattern'] = '.*' + elif rule['pattern'] == 'unspecified': + rule['pattern'] = 'None' + + pr = (rule['name'],rule['field'],rule['pattern']) + rule_set.append(pr) + + opt_value = tuple(rule_set) + opts_dict['exclusion_rules'] = opt_value + exclusion_rules_processed = True + else: opts_dict[c_name] = opt_value + ''' + *** # Generate markers for hybrids #opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field_name, # self.read_pattern.text()) opts_dict['exclude_book_marker'] = "%s:%s" % (self.exclude_source_field_name, self.exclude_pattern.text()) + ''' # Generate specs for merge_comments, header_note_source_field checked = '' @@ -306,6 +365,8 @@ class PluginWidget(QWidget,Ui_Form): if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: custom_fields[field_md['name']] = {'field':custom_field, 'datatype':field_md['datatype']} + ''' + *** # Blank field first self.exclude_source_field.addItem('') # Add the sorted eligible fields to the combo box @@ -313,7 +374,7 @@ class PluginWidget(QWidget,Ui_Form): self.exclude_source_field.addItem(cf) self.exclude_source_fields = custom_fields self.exclude_source_field.currentIndexChanged.connect(self.exclude_source_field_changed) - + ''' # Populate the 'Header note' combo box custom_fields = {} @@ -488,7 +549,7 @@ class NoWheelComboBox(QComboBox): # Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid event.ignore() -class PrefixRulesComboBox(NoWheelComboBox): +class ComboBox(NoWheelComboBox): # Caller is responsible for providing the list in the preferred order def __init__(self, parent, items, selected_text,insert_blank=True): NoWheelComboBox.__init__(self, parent) @@ -511,8 +572,8 @@ class GenericRulesTable(QTableWidget): placeholders for basic methods to be overriden ''' - def __init__(self, parent_gb_hl, object_name, prefix_rules, eligible_custom_fields, db): - self.prefix_rules = prefix_rules + def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): + self.rules = rules self.eligible_custom_fields = eligible_custom_fields self.db = db QTableWidget.__init__(self) @@ -525,12 +586,11 @@ class GenericRulesTable(QTableWidget): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) - self.setMaximumSize(QSize(16777215, 118)) + self.setMaximumSize(QSize(16777215, 114)) self.setColumnCount(0) self.setRowCount(0) self.layout.addWidget(self) - self._init_table_widget() self._init_controls() self._initialize() @@ -686,10 +746,138 @@ class GenericRulesTable(QTableWidget): ''' pass + def resize_name(self, scale): + current_width = self.columnWidth(1) + self.setColumnWidth(1, min(225,int(current_width * scale))) + + def rule_name_edited(self): + current_row = self.currentRow() + self.cellWidget(current_row,1).home(False) + self.setFocus() + self.select_and_scroll_to_row(current_row) + + def select_and_scroll_to_row(self, row): + self.selectRow(row) + self.scrollToItem(self.currentItem()) + +class ExclusionRules(GenericRulesTable): + + def _init_table_widget(self): + header_labels = ['','Name','Field','Value'] + self.setColumnCount(len(header_labels)) + self.setHorizontalHeaderLabels(header_labels) + self.setSortingEnabled(False) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + + def _initialize(self): + # Override max size (118) set in GenericRulesTable + self.setMaximumSize(QSize(16777215, 83)) + + self.populate() + self.resizeColumnsToContents() + self.resize_name(1.5) + self.horizontalHeader().setStretchLastSection(True) + + def convert_row_to_data(self, row): + data = self.create_blank_row_data() + data['ordinal'] = row + data['enabled'] = self.item(row,0).checkState() == Qt.Checked + data['name'] = unicode(self.cellWidget(row,1).text()).strip() + data['field'] = unicode(self.cellWidget(row,2).currentText()).strip() + data['pattern'] = unicode(self.cellWidget(row,3).currentText()).strip() + return data + + def create_blank_row_data(self): + data = {} + data['ordinal'] = -1 + data['enabled'] = False + data['name'] = 'New rule' + data['field'] = '' + data['pattern'] = '' + return data + + def get_data(self): + data_items = [] + for row in range(self.rowCount()): + data = self.convert_row_to_data(row) + data_items.append( + {'ordinal':data['ordinal'], + 'enabled':data['enabled'], + 'name':data['name'], + 'field':data['field'], + 'pattern':data['pattern']}) + return data_items + + def populate(self): + # Format of rules list is different if default values vs retrieved JSON + # Hack to normalize list style + rules = self.rules + if rules and type(rules[0]) is list: + rules = rules[0] + self.setFocus() + rules = sorted(rules, key=lambda k: k['ordinal']) + for row, rule in enumerate(rules): + self.insertRow(row) + self.select_and_scroll_to_row(row) + self.populate_table_row(row, rule) + self.selectRow(0) + + def populate_table_row(self, row, data): + + def set_rule_name_in_row(row, col, name=''): + rule_name = QLineEdit(name) + rule_name.home(False) + rule_name.editingFinished.connect(self.rule_name_edited) + self.setCellWidget(row, col, rule_name) + + def set_source_field_in_row(row, col, field=''): + source_combo = ComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) + source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row)) + self.setCellWidget(row, col, source_combo) + return source_combo + + # Entry point + self.blockSignals(True) + + # Column 0: Enabled + self.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) + + # Column 1: Rule name + set_rule_name_in_row(row, 1, name=data['name']) + + # Column 2: Source field + source_combo = set_source_field_in_row(row, 2, field=data['field']) + + # Column 3: Pattern + # The contents of the Pattern field is driven by the Source field + self.source_index_changed(source_combo, row, 3, pattern=data['pattern']) + + self.blockSignals(False) + + def source_index_changed(self, combo, row, col, pattern=''): + # Populate the Pattern field based upon the Source field + source_field = str(combo.currentText()) + if source_field == '': + values = [] + elif source_field == 'Tags': + values = sorted(self.db.all_tags(), key=sort_key) + else: + if self.eligible_custom_fields[source_field]['datatype'] in ['enumeration', 'text']: + values = self.db.all_custom(self.db.field_metadata.key_to_label( + self.eligible_custom_fields[source_field]['field'])) + values = sorted(values, key=sort_key) + elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']: + values = ['True','False','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: + values = ['any value','unspecified'] + + values_combo = ComboBox(self, values, pattern) + self.setCellWidget(row, 3, values_combo) + class PrefixRules(GenericRulesTable): def _init_table_widget(self): - header_labels = ['','Name','Prefix','Source','Pattern'] + header_labels = ['','Name','Prefix','Field','Value'] self.setColumnCount(len(header_labels)) self.setHorizontalHeaderLabels(header_labels) self.setSortingEnabled(False) @@ -873,8 +1061,8 @@ class PrefixRules(GenericRulesTable): def populate(self): # Format of rules list is different if default values vs retrieved JSON # Hack to normalize list style - rules = self.prefix_rules - if type(rules[0]) is list: + rules = self.rules + if rules and type(rules[0]) is list: rules = rules[0] self.setFocus() rules = sorted(rules, key=lambda k: k['ordinal']) @@ -887,7 +1075,7 @@ class PrefixRules(GenericRulesTable): def populate_table_row(self, row, data): def set_prefix_field_in_row(row, col, field=''): - prefix_combo = PrefixRulesComboBox(self, self.prefix_list, field) + prefix_combo = ComboBox(self, self.prefix_list, field) self.setCellWidget(row, col, prefix_combo) def set_rule_name_in_row(row, col, name=''): @@ -897,7 +1085,7 @@ class PrefixRules(GenericRulesTable): self.setCellWidget(row, col, rule_name) def set_source_field_in_row(row, col, field=''): - source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) + source_combo = ComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row)) self.setCellWidget(row, col, source_combo) return source_combo @@ -926,22 +1114,9 @@ class PrefixRules(GenericRulesTable): self.blockSignals(False) - def resize_name(self, scale): - current_width = self.columnWidth(1) - self.setColumnWidth(1, min(225,int(current_width * scale))) - - def rule_name_edited(self): - current_row = self.currentRow() - self.cellWidget(current_row,1).home(False) - self.setFocus() - self.select_and_scroll_to_row(current_row) - - def select_and_scroll_to_row(self, row): - self.selectRow(row) - self.scrollToItem(self.currentItem()) - def source_index_changed(self, combo, row, col, pattern=''): # Populate the Pattern field based upon the Source field + # row, col are the control that changed source_field = str(combo.currentText()) if source_field == '': @@ -960,6 +1135,6 @@ class PrefixRules(GenericRulesTable): elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: values = ['any value','unspecified'] - values_combo = PrefixRulesComboBox(self, values, pattern) + values_combo = ComboBox(self, values, pattern) self.setCellWidget(row, 4, values_combo) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index bf10cacd38..29ca39bf10 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -184,7 +184,7 @@ p, li { white-space: pre-wrap; } - + 0 @@ -198,130 +198,14 @@ p, li { white-space: pre-wrap; } - Books matching either pattern will not be included in generated catalog. + Matching books will not be included in generated catalog. Excluded books - - - QFormLayout::FieldsStayAtSizeHint - - - - - - - - 0 - 0 - - - - - 175 - 0 - - - - - 200 - 16777215 - - - - Tags to &exclude - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - exclude_tags - - - - - - - - 0 - 0 - - - - <p>Comma-separated list of tags to exclude. -Default: ~,Catalog - - - - - - - - - - - - 175 - 0 - - - - - 200 - 16777215 - - - - &Column/value - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - exclude_source_field - - - - - - - - 0 - 0 - - - - Column containing additional exclusion criteria - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 18 - - - - - - - - 150 - 0 - - - - Exclusion pattern - - - - + + + @@ -335,7 +219,7 @@ Default: ~,Catalog - The first matching rule will be used to add a prefix to book listings in the generated catalog. + The earliest enabled matching rule will be used to add a prefix to book listings in the generated catalog. Prefix rules @@ -369,9 +253,9 @@ Default: ~,Catalog QFormLayout::FieldsStayAtSizeHint - + - + 175 @@ -385,16 +269,13 @@ Default: ~,Catalog - &Thumbnail width + &Thumb width Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - true - - thumb_width + merge_source_field @@ -406,6 +287,12 @@ Default: ~,Catalog 0 + + + 137 + 16777215 + + Size hint for Description cover thumbnails @@ -426,38 +313,17 @@ Default: ~,Catalog - - - - - - - - 0 - 0 - - - - - 175 - 0 - - - - - 200 - 16777215 - - - - + + + Qt::Vertical + + + + - &Description note - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + &Extra note header_note_source_field @@ -478,6 +344,12 @@ Default: ~,Catalog 0 + + + 16777215 + 16777215 + + Custom column source for note to include in Description header area @@ -485,7 +357,7 @@ Default: ~,Catalog - + diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index a7da345dc2..385a699c7b 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -48,29 +48,13 @@ class EPUB_MOBI(CatalogPlugin): "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), Option('--exclude-genre', - default='\[.+\]', + default='\[.+\]|\+', dest='exclude_genre', action = None, - help=_("Regex describing tags to exclude as genres.\n" "Default: '%default' excludes bracketed tags, e.g. '[]'\n" + help=_("Regex describing tags to exclude as genres.\n" + "Default: '%default' excludes bracketed tags, e.g. '[Project Gutenberg]', and '+', the default tag for read books.\n" "Applies to: ePub, MOBI output formats")), -# Option('--exclude-book-marker', -# default=':', -# dest='exclude_book_marker', -# action = None, -# help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n" -# "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n" -# "Default: '%default'\n" -# "Applies to ePub, MOBI output formats")), -# Option('--exclude-tags', -# default=('~,Catalog'), -# dest='exclude_tags', -# action = None, -# help=_("Comma-separated list of tag words indicating book should be excluded from output. " -# "For example: 'skip' will match 'skip this book' and 'Skip will like this'. " -# "Default:'%default'\n" -# "Applies to: ePub, MOBI output formats")), - Option('--exclusion-rules', default="(('Excluded tags','Tags','~,Catalog'),)", dest='exclusion_rules', diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 6cc94c0e08..111ceda9fe 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -657,14 +657,36 @@ Author '{0}': # Merge opts.exclude_tags with opts.search_text # Updated to use exact match syntax - empty_exclude_tags = False if len(self.opts.exclude_tags) else True + + exclude_tags = [] + for rule in self.opts.exclusion_rules: + if rule[1].lower() == 'tags': + exclude_tags.extend(rule[2].split(',')) + + # Remove dups + self.exclude_tags = exclude_tags = list(set(exclude_tags)) + + if self.opts.verbose and self.exclude_tags: + #self.opts.log.info(" excluding tag list %s" % exclude_tags) + search_terms = [] + for tag in exclude_tags: + search_terms.append("tag:=%s" % tag) + search_phrase = "%s" % " or ".join(search_terms) + self.opts.search_text = search_phrase + data = self.plugin.search_sort_db(self.db, self.opts) + for record in data: + self.opts.log.info("\t- %s (Exclusion rule %s)" % (record['title'], exclude_tags)) + # Reset the database + self.opts.search_text = '' + data = self.plugin.search_sort_db(self.db, self.opts) + search_phrase = '' - if not empty_exclude_tags: - exclude_tags = self.opts.exclude_tags.split(',') + if exclude_tags: search_terms = [] for tag in exclude_tags: search_terms.append("tag:=%s" % tag) search_phrase = "not (%s)" % " or ".join(search_terms) + # If a list of ids are provided, don't use search_text if self.opts.ids: self.opts.search_text = search_phrase @@ -1672,14 +1694,13 @@ Author '{0}': self.opts.sort_by = 'series' - # Merge opts.exclude_tags with opts.search_text + # Merge self.exclude_tags with opts.search_text # Updated to use exact match syntax - empty_exclude_tags = False if len(self.opts.exclude_tags) else True + search_phrase = 'series:true ' - if not empty_exclude_tags: - exclude_tags = self.opts.exclude_tags.split(',') + if self.exclude_tags: search_terms = [] - for tag in exclude_tags: + for tag in self.exclude_tags: search_terms.append("tag:=%s" % tag) search_phrase += "not (%s)" % " or ".join(search_terms) @@ -3120,7 +3141,7 @@ Author '{0}': Evaluate conditions for including prefixes in various listings ''' def log_prefix_rule_match_info(rule, record): - self.opts.log.info(" %s %s by %s (Prefix rule '%s': %s:%s)" % + self.opts.log.info("\t%s %s by %s (Prefix rule '%s': %s:%s)" % (rule['prefix'],record['title'], record['authors'][0], rule['name'], rule['field'],rule['pattern'])) @@ -3816,9 +3837,14 @@ Author '{0}': return friendly_tag def getMarkerTags(self): - ''' Return a list of special marker tags to be excluded from genre list ''' + ''' + Return a list of special marker tags to be excluded from genre list + exclusion_rules = ('name','Tags|#column','[]|pattern') + ''' markerTags = [] - markerTags.extend(self.opts.exclude_tags.split(',')) + for rule in self.opts.exclusion_rules: + if rule[1].lower() == 'tags': + markerTags.extend(rule[2].split(',')) return markerTags def letter_or_symbol(self,char): @@ -3996,21 +4022,40 @@ Author '{0}': ''' Remove excluded entries ''' - field, pat = self.opts.exclude_book_marker.split(':') - if pat == '': - return data_set filtered_data_set = [] - for record in data_set: - field_contents = self.__db.get_field(record['id'], - field, - index_is_id=True) - if field_contents: - if re.search(pat, unicode(field_contents), - re.IGNORECASE) is not None: - continue - filtered_data_set.append(record) + exclusion_pairs = [] + exclusion_set = [] + for rule in self.opts.exclusion_rules: + if rule[1].startswith('#') and rule[2] != '': + field = rule[1] + pat = rule[2] + exclusion_pairs.append((field,pat)) + else: + continue - return filtered_data_set + if exclusion_pairs: + for record in data_set: + for exclusion_pair in exclusion_pairs: + field,pat = exclusion_pair + field_contents = self.__db.get_field(record['id'], + field, + index_is_id=True) + if field_contents: + if re.search(pat, unicode(field_contents), + re.IGNORECASE) is not None: + if self.opts.verbose: + self.opts.log.info(" excluding '%s' (%s:%s)" % (record['title'], field, pat)) + exclusion_set.append(record) + if record in filtered_data_set: + filtered_data_set.remove(record) + break + else: + if (record not in filtered_data_set and + record not in exclusion_set): + filtered_data_set.append(record) + return filtered_data_set + else: + return data_set def processSpecialTags(self, tags, this_title, opts): From 1f39af8010a304e39356678c5756194a6d82435c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 6 Aug 2012 18:52:00 +0200 Subject: [PATCH 17/27] Add collections to the smartdevice driver --- .../devices/smart_device_app/driver.py | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index b88c97a279..ee16e23a25 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -17,7 +17,7 @@ from calibre.constants import numeric_version, DEBUG from calibre.devices.errors import (OpenFailed, ControlError, TimeoutError, InitialConnectionError) from calibre.devices.interface import DevicePlugin -from calibre.devices.usbms.books import Book, BookList +from calibre.devices.usbms.books import Book, CollectionsBookList from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.driver import USBMS from calibre.ebooks import BOOK_EXTENSIONS @@ -107,8 +107,18 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): } reverse_opcodes = dict([(v, k) for k,v in opcodes.iteritems()]) + ALL_BY_TITLE = _('All by title') + ALL_BY_AUTHOR = _('All by author') EXTRA_CUSTOMIZATION_MESSAGE = [ + _('Comma separated list of metadata fields ' + 'to turn into collections on the device. Possibilities include: ')+\ + 'series, tags, authors' +\ + _('. Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add ' + 'these values to the list to enable them. The collections will be ' + 'given the name provided after the ":" character.')%dict( + abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR), + '', _('Enable connections at startup') + ':::

' + _('Check this box to allow connections when calibre starts') + '

', '', @@ -124,6 +134,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): _('Check this box if requested when reporting problems') + '

', ] EXTRA_CUSTOMIZATION_DEFAULT = [ + 'tags, series', + '', False, '', '', @@ -131,11 +143,12 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): False, '9090', False, ] - OPT_AUTOSTART = 0 - OPT_PASSWORD = 2 - OPT_USE_PORT = 4 - OPT_PORT_NUMBER = 5 - OPT_EXTRA_DEBUG = 6 + OPT_COLLECTIONS = 0 + OPT_AUTOSTART = 2 + OPT_PASSWORD = 4 + OPT_USE_PORT = 6 + OPT_PORT_NUMBER = 7 + OPT_EXTRA_DEBUG = 8 def __init__(self, path): self.sync_lock = threading.RLock() @@ -659,9 +672,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): def books(self, oncard=None, end_session=True): self._debug(oncard) if oncard is not None: - return BookList(None, None, None) + return CollectionsBookList(None, None, None) opcode, result = self._call_client('GET_BOOK_COUNT', {}) - bl = BookList(None, self.PREFIX, self.settings) + bl = CollectionsBookList(None, self.PREFIX, self.settings) if opcode == 'OK': count = result['count'] for i in range(0, count): @@ -681,10 +694,21 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): @synchronous('sync_lock') def sync_booklists(self, booklists, end_session=True): self._debug() + collections = [x.strip() for x in + self.settings().extra_customization[self.OPT_COLLECTIONS].split(',')] + collections = booklists[0].get_collections(collections) + coldict = {} + for k,v in collections.iteritems(): + lpaths = [] + for book in v: + lpaths.append(book.lpath) + coldict[k] = lpaths + self._debug(coldict) # If we ever do device_db plugboards, this is where it will go. We will # probably need to send two booklists, one with calibre's data that is # given back by "books", and one that has been plugboarded. - self._call_client('SEND_BOOKLISTS', { 'count': len(booklists[0]) } ) + self._call_client('SEND_BOOKLISTS', { 'count': len(booklists[0]), + 'collections': coldict} ) for i,book in enumerate(booklists[0]): if not self._metadata_already_on_device(book): self._set_known_metadata(book) From e40bd0164dbf84ab0ceb3f6211591b08cd89687a Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 6 Aug 2012 10:52:07 -0600 Subject: [PATCH 18/27] Removed table selection when lost focus, added reset button to exclude_genres --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 124 +++++++----------- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 13 +- .../library/catalogs/epub_mobi_builder.py | 18 +-- 3 files changed, 69 insertions(+), 86 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 8a2035c76f..57a23211e3 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -18,7 +18,7 @@ from PyQt4.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QHBoxLayout, QIcon, QLabel, QLineEdit, QPlainTextEdit, QRadioButton, QSize, QSizePolicy, - QTableWidget, QTableWidgetItem, + QTableWidget, QTableWidgetItem, QTimer, QToolButton, QVBoxLayout, QWidget) class PluginWidget(QWidget,Ui_Form): @@ -72,8 +72,6 @@ class PluginWidget(QWidget,Ui_Form): # LineEditControls option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit']) - #***option_fields += zip(['exclude_pattern'],[None],['line_edit']) - #***option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit']) # SpinBoxControls option_fields += zip(['thumb_width'],[1.00],['spin_box']) @@ -84,12 +82,7 @@ class PluginWidget(QWidget,Ui_Form): 'enabled':True, 'name':'Catalogs', 'field':'Tags', - 'pattern':'Catalog'}, - {'ordinal':1, - 'enabled':False, - 'name':'New rule', - 'field':'', - 'pattern':''}], + 'pattern':'Catalog'},], ['table_widget','table_widget']) # Prefix rules @@ -105,13 +98,7 @@ class PluginWidget(QWidget,Ui_Form): 'name':'Wishlist item', 'field':'Tags', 'pattern':'Wishlist', - 'prefix':u'\u00d7'}, - {'ordinal':2, - 'enabled':False, - 'name':'New rule', - 'field':'', - 'pattern':'', - 'prefix':''}], + 'prefix':u'\u00d7'},], ['table_widget','table_widget','table_widget']) self.OPTION_FIELDS = option_fields @@ -181,15 +168,9 @@ class PluginWidget(QWidget,Ui_Form): if opt_value not in prefix_rules: prefix_rules.append(opt_value) - ''' - *** - # Init self.exclude_source_field_name - self.exclude_source_field_name = '' - cs = unicode(self.exclude_source_field.currentText()) - if cs > '': - exclude_source_spec = self.exclude_source_fields[cs] - self.exclude_source_field_name = exclude_source_spec['field'] - ''' + # Add icon to the reset button + self.reset_exclude_genres_tb.setIcon(QIcon(I('trash.png'))) + self.reset_exclude_genres_tb.clicked.connect(self.reset_exclude_genres) # Init self.merge_source_field_name self.merge_source_field_name = '' @@ -205,12 +186,6 @@ class PluginWidget(QWidget,Ui_Form): header_note_source_spec = self.header_note_source_fields[cs] self.header_note_source_field_name = header_note_source_spec['field'] - # Hook changes to thumb_width - self.thumb_width.valueChanged.connect(self.thumb_width_changed) - - # Hook changes to Description section - self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) - # Initialize exclusion rules self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb_hl, "exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db) @@ -219,6 +194,13 @@ class PluginWidget(QWidget,Ui_Form): self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, "prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db) + # Hook changes to thumb_width + self.thumb_width.valueChanged.connect(self.thumb_width_changed) + + # Hook changes to Description section + self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) + + def options(self): # Save/return the current options # exclude_genre stores literally @@ -310,15 +292,6 @@ class PluginWidget(QWidget,Ui_Form): else: opts_dict[c_name] = opt_value - ''' - *** - # Generate markers for hybrids - #opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field_name, - # self.read_pattern.text()) - opts_dict['exclude_book_marker'] = "%s:%s" % (self.exclude_source_field_name, - self.exclude_pattern.text()) - ''' - # Generate specs for merge_comments, header_note_source_field checked = '' if self.merge_before.isChecked(): @@ -365,17 +338,6 @@ class PluginWidget(QWidget,Ui_Form): if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: custom_fields[field_md['name']] = {'field':custom_field, 'datatype':field_md['datatype']} - ''' - *** - # Blank field first - self.exclude_source_field.addItem('') - # Add the sorted eligible fields to the combo box - for cf in sorted(custom_fields, key=sort_key): - self.exclude_source_field.addItem(cf) - self.exclude_source_fields = custom_fields - self.exclude_source_field.currentIndexChanged.connect(self.exclude_source_field_changed) - ''' - # Populate the 'Header note' combo box custom_fields = {} for custom_field in self.all_custom_fields: @@ -508,6 +470,12 @@ class PluginWidget(QWidget,Ui_Form): self.merge_after.setEnabled(False) self.include_hr.setEnabled(False) + def reset_exclude_genres(self): + for default in self.OPTION_FIELDS: + if default[0] == 'exclude_genre': + self.exclude_genre.setText(default[1]) + break + def thumb_width_changed(self,new_value): ''' Process changes in the thumb_width spin box @@ -581,19 +549,20 @@ class GenericRulesTable(QTableWidget): self.layout = parent_gb_hl # Add ourselves to the layout - sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) + #print("verticalHeader: %s" % dir(self.verticalHeader())) + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + #sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) - self.setMaximumSize(QSize(16777215, 114)) + self.setMaximumSize(QSize(16777215, 113)) + self.setColumnCount(0) self.setRowCount(0) + self.layout.addWidget(self) - self._init_table_widget() self._init_controls() - self._initialize() def _init_controls(self): # Add the control set @@ -628,18 +597,6 @@ class GenericRulesTable(QTableWidget): self.layout.addLayout(vbl) - def _init_table_widget(self): - ''' - Override this in the specific instance - ''' - pass - - def _initialize(self): - ''' - Override this in the specific instance - ''' - pass - def add_row(self): self.setFocus() row = self.currentRow() + 1 @@ -683,6 +640,10 @@ class GenericRulesTable(QTableWidget): def get_data(self): pass + def focusOutEvent(self,e): + # Override of QTableWidget method + self.clearSelection() + def move_row_down(self): self.setFocus() rows = self.selectionModel().selectedRows() @@ -760,8 +721,21 @@ class GenericRulesTable(QTableWidget): self.selectRow(row) self.scrollToItem(self.currentItem()) + def tweak_height(self, height=4): + for i in range(min(3,self.rowCount())): + height += self.rowHeight(i) + height += self.verticalHeader().sizeHint().height() + print("computed table height for %d rows: %d" % (self.rowCount(),height, )) + self.setMinimumSize(QSize(16777215, height)) + self.setMaximumSize(QSize(16777215, height)) + class ExclusionRules(GenericRulesTable): + def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): + super(ExclusionRules, self).__init__(parent_gb_hl, object_name, rules, eligible_custom_fields, db) + self._init_table_widget() + self._initialize() + def _init_table_widget(self): header_labels = ['','Name','Field','Value'] self.setColumnCount(len(header_labels)) @@ -770,13 +744,11 @@ class ExclusionRules(GenericRulesTable): self.setSelectionBehavior(QAbstractItemView.SelectRows) def _initialize(self): - # Override max size (118) set in GenericRulesTable - self.setMaximumSize(QSize(16777215, 83)) - self.populate() self.resizeColumnsToContents() self.resize_name(1.5) self.horizontalHeader().setStretchLastSection(True) + self.clearSelection() def convert_row_to_data(self, row): data = self.create_blank_row_data() @@ -790,7 +762,7 @@ class ExclusionRules(GenericRulesTable): def create_blank_row_data(self): data = {} data['ordinal'] = -1 - data['enabled'] = False + data['enabled'] = True data['name'] = 'New rule' data['field'] = '' data['pattern'] = '' @@ -876,6 +848,11 @@ class ExclusionRules(GenericRulesTable): class PrefixRules(GenericRulesTable): + def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): + super(PrefixRules, self).__init__(parent_gb_hl, object_name, rules, eligible_custom_fields, db) + self._init_table_widget() + self._initialize() + def _init_table_widget(self): header_labels = ['','Name','Prefix','Field','Value'] self.setColumnCount(len(header_labels)) @@ -889,6 +866,7 @@ class PrefixRules(GenericRulesTable): self.resizeColumnsToContents() self.resize_name(1.5) self.horizontalHeader().setStretchLastSection(True) + self.clearSelection() def convert_row_to_data(self, row): data = self.create_blank_row_data() @@ -903,7 +881,7 @@ class PrefixRules(GenericRulesTable): def create_blank_row_data(self): data = {} data['ordinal'] = -1 - data['enabled'] = False + data['enabled'] = True data['name'] = 'New rule' data['field'] = '' data['pattern'] = '' diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 29ca39bf10..784806c15e 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -111,7 +111,8 @@ <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html>
+<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books.</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html> Excluded genres @@ -178,6 +179,16 @@ p, li { white-space: pre-wrap; }
+ + + + Reset to default + + + ... + + +
diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 111ceda9fe..63fe02d5f1 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -666,19 +666,13 @@ Author '{0}': # Remove dups self.exclude_tags = exclude_tags = list(set(exclude_tags)) + # Report tag exclusions if self.opts.verbose and self.exclude_tags: - #self.opts.log.info(" excluding tag list %s" % exclude_tags) - search_terms = [] - for tag in exclude_tags: - search_terms.append("tag:=%s" % tag) - search_phrase = "%s" % " or ".join(search_terms) - self.opts.search_text = search_phrase - data = self.plugin.search_sort_db(self.db, self.opts) + data = self.db.get_data_as_dict(ids=self.opts.ids) for record in data: - self.opts.log.info("\t- %s (Exclusion rule %s)" % (record['title'], exclude_tags)) - # Reset the database - self.opts.search_text = '' - data = self.plugin.search_sort_db(self.db, self.opts) + matched = list(set(record['tags']) & set(exclude_tags)) + if matched : + self.opts.log.info(" - %s (Exclusion rule %s)" % (record['title'], matched)) search_phrase = '' if exclude_tags: @@ -3141,7 +3135,7 @@ Author '{0}': Evaluate conditions for including prefixes in various listings ''' def log_prefix_rule_match_info(rule, record): - self.opts.log.info("\t%s %s by %s (Prefix rule '%s': %s:%s)" % + self.opts.log.info(" %s %s by %s (Prefix rule '%s': %s:%s)" % (rule['prefix'],record['title'], record['authors'][0], rule['name'], rule['field'],rule['pattern'])) From aa62a13cf0ec321d822f2b4944cfce61f65540cd Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 6 Aug 2012 18:55:28 +0200 Subject: [PATCH 19/27] ... --- src/calibre/devices/smart_device_app/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index ee16e23a25..82ff2e7920 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -703,7 +703,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): for book in v: lpaths.append(book.lpath) coldict[k] = lpaths - self._debug(coldict) + # If we ever do device_db plugboards, this is where it will go. We will # probably need to send two booklists, one with calibre's data that is # given back by "books", and one that has been plugboarded. From a8b5d497fc622959a13a4a427017b9f43b52fb1b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 6 Aug 2012 19:01:15 +0200 Subject: [PATCH 20/27] ... --- src/calibre/devices/smart_device_app/driver.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 82ff2e7920..5fb9761d32 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -693,16 +693,17 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): @synchronous('sync_lock') def sync_booklists(self, booklists, end_session=True): - self._debug() - collections = [x.strip() for x in + colattrs = [x.strip() for x in self.settings().extra_customization[self.OPT_COLLECTIONS].split(',')] - collections = booklists[0].get_collections(collections) + self._debug('collection attributes', colattrs) coldict = {} - for k,v in collections.iteritems(): - lpaths = [] - for book in v: - lpaths.append(book.lpath) - coldict[k] = lpaths + if colattrs: + collections = booklists[0].get_collections(colattrs) + for k,v in collections.iteritems(): + lpaths = [] + for book in v: + lpaths.append(book.lpath) + coldict[k] = lpaths # If we ever do device_db plugboards, this is where it will go. We will # probably need to send two booklists, one with calibre's data that is From 1f2e84181f9d77efaed4152a77b89b188909621d Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 6 Aug 2012 17:34:47 -0600 Subject: [PATCH 21/27] Improved focus handling for tables. Removed obsolete characters in profiles.py. Revised tool tips. --- src/calibre/customize/profiles.py | 8 - src/calibre/gui2/catalog/catalog_epub_mobi.py | 205 ++++++------------ src/calibre/gui2/catalog/catalog_epub_mobi.ui | 36 +-- .../library/catalogs/epub_mobi_builder.py | 7 +- 4 files changed, 84 insertions(+), 172 deletions(-) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 84db12e161..78b61d4345 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -251,10 +251,8 @@ class OutputProfile(Plugin): periodical_date_in_title = True #: Characters used in jackets and catalogs - missing_char = u'x' ratings_char = u'*' empty_ratings_char = u' ' - read_char = u'+' #: Unsupported unicode characters to be replaced during preprocessing unsupported_unicode_chars = [] @@ -292,10 +290,8 @@ class iPadOutput(OutputProfile): } ] - missing_char = u'\u2715\u200a' # stylized 'x' plus hair space ratings_char = u'\u2605' # filled star empty_ratings_char = u'\u2606' # hollow star - read_char = u'\u2713' # check mark touchscreen = True # touchscreen_news_css {{{ @@ -626,10 +622,8 @@ class KindleOutput(OutputProfile): supports_mobi_indexing = True periodical_date_in_title = False - missing_char = u'x\u2009' empty_ratings_char = u'\u2606' ratings_char = u'\u2605' - read_char = u'\u2713' mobi_ems_per_blockquote = 2.0 @@ -651,10 +645,8 @@ class KindleDXOutput(OutputProfile): #comic_screen_size = (741, 1022) supports_mobi_indexing = True periodical_date_in_title = False - missing_char = u'x\u2009' empty_ratings_char = u'\u2606' ratings_char = u'\u2605' - read_char = u'\u2713' mobi_ems_per_blockquote = 2.0 @classmethod diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 57a23211e3..485f84a642 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -6,6 +6,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' +from copy import copy from functools import partial from calibre.ebooks.conversion.config import load_defaults @@ -138,7 +139,6 @@ class PluginWidget(QWidget,Ui_Form): self.fetchEligibleCustomFields() self.populate_combo_boxes() - # Update dialog fields from stored options exclusion_rules = [] prefix_rules = [] @@ -161,12 +161,13 @@ class PluginWidget(QWidget,Ui_Form): getattr(self, c_name).setChecked(opt_value) elif c_type in ['spin_box']: getattr(self, c_name).setValue(float(opt_value)) - elif c_type in ['table_widget'] and c_name == 'exclusion_rules_tw': - if opt_value not in exclusion_rules: - exclusion_rules.append(opt_value) - elif c_type in ['table_widget'] and c_name == 'prefix_rules_tw': - if opt_value not in prefix_rules: - prefix_rules.append(opt_value) + if c_type == 'table_widget': + if c_name == 'exclusion_rules_tw': + if opt_value not in exclusion_rules: + exclusion_rules.append(opt_value) + if c_name == 'prefix_rules_tw': + if opt_value not in prefix_rules: + prefix_rules.append(opt_value) # Add icon to the reset button self.reset_exclude_genres_tb.setIcon(QIcon(I('trash.png'))) @@ -187,20 +188,13 @@ class PluginWidget(QWidget,Ui_Form): self.header_note_source_field_name = header_note_source_spec['field'] # Initialize exclusion rules - self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb_hl, + self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb, "exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db) # Initialize prefix rules - self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, + self.prefix_rules_table = PrefixRules(self.prefix_rules_gb, "prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db) - # Hook changes to thumb_width - self.thumb_width.valueChanged.connect(self.thumb_width_changed) - - # Hook changes to Description section - self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) - - def options(self): # Save/return the current options # exclude_genre stores literally @@ -214,6 +208,11 @@ class PluginWidget(QWidget,Ui_Form): for opt in self.OPTION_FIELDS: c_name, c_def, c_type = opt + if c_name == 'exclusion_rules_tw' and exclusion_rules_processed: + continue + if c_name == 'prefix_rules_tw' and prefix_rules_processed: + continue + if c_type in ['check_box', 'radio_button']: opt_value = getattr(self, c_name).isChecked() elif c_type in ['combo_box']: @@ -225,22 +224,21 @@ class PluginWidget(QWidget,Ui_Form): elif c_type in ['table_widget']: if c_name == 'prefix_rules_tw': opt_value = self.prefix_rules_table.get_data() + prefix_rules_processed = True if c_name == 'exclusion_rules_tw': opt_value = self.exclusion_rules_table.get_data() + exclusion_rules_processed = True + # Store UI values to gui.json in config dir gprefs.set(self.name + '_' + c_name, opt_value) # Construct opts object for catalog builder - if c_name == 'exclude_tags': - # store as list - opts_dict[c_name] = opt_value.split(',') - elif c_name == 'prefix_rules_tw': - if prefix_rules_processed: - continue + if c_name == 'prefix_rules_tw': rule_set = [] - for rule in opt_value: + for stored_rule in opt_value: # Test for empty name/field/pattern/prefix, continue # If pattern = any or unspecified, convert to regex + rule = copy(stored_rule) if not rule['enabled']: continue elif not rule['field'] or not rule['pattern'] or not rule['prefix']: @@ -250,24 +248,22 @@ class PluginWidget(QWidget,Ui_Form): # Look up custom column name #print(self.eligible_custom_fields[rule['field']]['field']) rule['field'] = self.eligible_custom_fields[rule['field']]['field'] - if rule['pattern'].startswith('any'): - rule['pattern'] = '.*' - elif rule['pattern'] == 'unspecified': - rule['pattern'] = 'None' + if rule['pattern'].startswith('any'): + rule['pattern'] = '.*' + elif rule['pattern'] == 'unspecified': + rule['pattern'] = 'None' - pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix']) - rule_set.append(pr) + pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix']) + rule_set.append(pr) opt_value = tuple(rule_set) opts_dict['prefix_rules'] = opt_value - prefix_rules_processed = True elif c_name == 'exclusion_rules_tw': - if exclusion_rules_processed: - continue rule_set = [] - for rule in opt_value: + for stored_rule in opt_value: # Test for empty name/field/pattern/prefix, continue # If pattern = any or unspecified, convert to regex + rule = copy(stored_rule) if not rule['enabled']: continue elif not rule['field'] or not rule['pattern']: @@ -277,17 +273,15 @@ class PluginWidget(QWidget,Ui_Form): # Look up custom column name #print(self.eligible_custom_fields[rule['field']]['field']) rule['field'] = self.eligible_custom_fields[rule['field']]['field'] - if rule['pattern'].startswith('any'): - rule['pattern'] = '.*' - elif rule['pattern'] == 'unspecified': - rule['pattern'] = 'None' - - pr = (rule['name'],rule['field'],rule['pattern']) - rule_set.append(pr) + if rule['pattern'].startswith('any'): + rule['pattern'] = '.*' + elif rule['pattern'] == 'unspecified': + rule['pattern'] = 'None' + pr = (rule['name'],rule['field'],rule['pattern']) + rule_set.append(pr) opt_value = tuple(rule_set) opts_dict['exclusion_rules'] = opt_value - exclusion_rules_processed = True else: opts_dict[c_name] = opt_value @@ -372,74 +366,6 @@ class PluginWidget(QWidget,Ui_Form): self.merge_after.setEnabled(False) self.include_hr.setEnabled(False) - def read_source_field_changed(self,new_index): - ''' - Process changes in the read_source_field combo box - Currently using QLineEdit for all field types - Possible to modify to switch QWidget type - ''' - new_source = unicode(self.read_source_field.currentText()) - read_source_spec = self.read_source_fields[new_source] - self.read_source_field_name = read_source_spec['field'] - - # Change pattern input widget to match the source field datatype - if read_source_spec['datatype'] in ['bool','composite','datetime','text']: - if not isinstance(self.read_pattern, QLineEdit): - self.read_spec_hl.removeWidget(self.read_pattern) - dw = QLineEdit(self) - dw.setObjectName('read_pattern') - dw.setToolTip('Pattern for read book') - self.read_pattern = dw - self.read_spec_hl.addWidget(dw) - - def exclude_source_field_changed(self,new_index): - ''' - Process changes in the exclude_source_field combo box - Currently using QLineEdit for all field types - Possible to modify to switch QWidget type - ''' - new_source = str(self.exclude_source_field.currentText()) - self.exclude_source_field_name = new_source - if new_source > '': - exclude_source_spec = self.exclude_source_fields[unicode(new_source)] - self.exclude_source_field_name = exclude_source_spec['field'] - self.exclude_pattern.setEnabled(True) - - # Change pattern input widget to match the source field datatype - if exclude_source_spec['datatype'] in ['bool','composite','datetime','text']: - if not isinstance(self.exclude_pattern, QLineEdit): - self.exclude_spec_hl.removeWidget(self.exclude_pattern) - dw = QLineEdit(self) - dw.setObjectName('exclude_pattern') - dw.setToolTip('Exclusion pattern') - self.exclude_pattern = dw - self.exclude_spec_hl.addWidget(dw) - else: - self.exclude_pattern.setEnabled(False) - - def generate_descriptions_changed(self,new_state): - ''' - Process changes to Descriptions section - 0: unchecked - 2: checked - ''' - - return - ''' - if new_state == 0: - # unchecked - self.merge_source_field.setEnabled(False) - self.merge_before.setEnabled(False) - self.merge_after.setEnabled(False) - self.include_hr.setEnabled(False) - elif new_state == 2: - # checked - self.merge_source_field.setEnabled(True) - self.merge_before.setEnabled(True) - self.merge_after.setEnabled(True) - self.include_hr.setEnabled(True) - ''' - def header_note_source_field_changed(self,new_index): ''' Process changes in the header_note_source_field combo box @@ -476,12 +402,6 @@ class PluginWidget(QWidget,Ui_Form): self.exclude_genre.setText(default[1]) break - def thumb_width_changed(self,new_value): - ''' - Process changes in the thumb_width spin box - ''' - pass - class CheckableTableWidgetItem(QTableWidgetItem): ''' @@ -540,13 +460,14 @@ class GenericRulesTable(QTableWidget): placeholders for basic methods to be overriden ''' - def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): + def __init__(self, parent_gb, object_name, rules, eligible_custom_fields, db): self.rules = rules self.eligible_custom_fields = eligible_custom_fields self.db = db QTableWidget.__init__(self) self.setObjectName(object_name) - self.layout = parent_gb_hl + self.layout = QHBoxLayout() + parent_gb.setLayout(self.layout) # Add ourselves to the layout #print("verticalHeader: %s" % dir(self.verticalHeader())) @@ -559,9 +480,11 @@ class GenericRulesTable(QTableWidget): self.setColumnCount(0) self.setRowCount(0) - self.layout.addWidget(self) + self.last_row_selected = self.currentRow() + self.last_rows_selected = self.selectionModel().selectedRows() + self._init_controls() def _init_controls(self): @@ -599,12 +522,12 @@ class GenericRulesTable(QTableWidget): def add_row(self): self.setFocus() - row = self.currentRow() + 1 + row = self.last_row_selected + 1 self.insertRow(row) self.populate_table_row(row, self.create_blank_row_data()) self.select_and_scroll_to_row(row) self.resizeColumnsToContents() - # Just in case table was empty + # In case table was empty self.horizontalHeader().setStretchLastSection(True) def convert_row_to_data(self): @@ -621,12 +544,16 @@ class GenericRulesTable(QTableWidget): def delete_row(self): self.setFocus() - rows = self.selectionModel().selectedRows() + rows = self.last_rows_selected if len(rows) == 0: return - message = '

Are you sure you want to delete this rule?' + + first = rows[0].row() + 1 + last = rows[-1].row() + 1 + + message = '

Are you sure you want to delete rule %d?' % first if len(rows) > 1: - message = '

Are you sure you want to delete the %d selected rules?'%len(rows) + message = '

Are you sure you want to delete rules %d-%d?' % (first, last) if not question_dialog(self, _('Are you sure?'), message, show_copy_button=False): return first_sel_row = self.currentRow() @@ -637,16 +564,18 @@ class GenericRulesTable(QTableWidget): elif self.rowCount() > 0: self.select_and_scroll_to_row(first_sel_row - 1) + def focusOutEvent(self,e): + # Override of QTableWidget method - clear selection when table loses focus + self.last_row_selected = self.currentRow() + self.last_rows_selected = self.selectionModel().selectedRows() + self.clearSelection() + def get_data(self): pass - def focusOutEvent(self,e): - # Override of QTableWidget method - self.clearSelection() - def move_row_down(self): self.setFocus() - rows = self.selectionModel().selectedRows() + rows = self.last_rows_selected if len(rows) == 0: return last_sel_row = rows[-1].row() @@ -673,11 +602,11 @@ class GenericRulesTable(QTableWidget): scroll_to_row = last_sel_row + 1 if scroll_to_row < self.rowCount() - 1: scroll_to_row = scroll_to_row + 1 - self.scrollToItem(self.item(scroll_to_row, 0)) + self.select_and_scroll_to_row(scroll_to_row) def move_row_up(self): self.setFocus() - rows = self.selectionModel().selectedRows() + rows = self.last_rows_selected if len(rows) == 0: return first_sel_row = rows[0].row() @@ -699,7 +628,7 @@ class GenericRulesTable(QTableWidget): scroll_to_row = first_sel_row - 1 if scroll_to_row > 0: scroll_to_row = scroll_to_row - 1 - self.scrollToItem(self.item(scroll_to_row, 0)) + self.select_and_scroll_to_row(scroll_to_row) def populate_table_row(self): ''' @@ -744,7 +673,7 @@ class ExclusionRules(GenericRulesTable): self.setSelectionBehavior(QAbstractItemView.SelectRows) def _initialize(self): - self.populate() + self.populate_table() self.resizeColumnsToContents() self.resize_name(1.5) self.horizontalHeader().setStretchLastSection(True) @@ -780,7 +709,7 @@ class ExclusionRules(GenericRulesTable): 'pattern':data['pattern']}) return data_items - def populate(self): + def populate_table(self): # Format of rules list is different if default values vs retrieved JSON # Hack to normalize list style rules = self.rules @@ -842,6 +771,8 @@ class ExclusionRules(GenericRulesTable): values = ['True','False','unspecified'] elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: values = ['any value','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: + values = ['any date','unspecified'] values_combo = ComboBox(self, values, pattern) self.setCellWidget(row, 3, values_combo) @@ -862,7 +793,7 @@ class PrefixRules(GenericRulesTable): def _initialize(self): self.generate_prefix_list() - self.populate() + self.populate_table() self.resizeColumnsToContents() self.resize_name(1.5) self.horizontalHeader().setStretchLastSection(True) @@ -1036,7 +967,7 @@ class PrefixRules(GenericRulesTable): 'prefix':data['prefix']}) return data_items - def populate(self): + def populate_table(self): # Format of rules list is different if default values vs retrieved JSON # Hack to normalize list style rules = self.rules @@ -1108,10 +1039,10 @@ class PrefixRules(GenericRulesTable): values = sorted(values, key=sort_key) elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']: values = ['True','False','unspecified'] - elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: - values = ['any date','unspecified'] elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: values = ['any value','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: + values = ['any date','unspecified'] values_combo = ComboBox(self, values, pattern) self.setCellWidget(row, 4, values_combo) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 784806c15e..bfe94a389f 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -35,7 +35,7 @@ - Sections to include in catalog. + Enabled sections will be included in the generated catalog. Included sections @@ -107,12 +107,8 @@ - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books.</p> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html> + A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books. +The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book], and '+', the default tag for a read book. Excluded genres @@ -209,16 +205,11 @@ p, li { white-space: pre-wrap; } - Matching books will not be included in generated catalog. + Books matching any of the exclusion rules will be excluded from the generated catalog. Excluded books - - - - - @@ -230,16 +221,11 @@ p, li { white-space: pre-wrap; } - The earliest enabled matching rule will be used to add a prefix to book listings in the generated catalog. + The first enabled matching rule will be used to add a prefix to book listings in the generated catalog. Prefix rules - - - - - @@ -305,7 +291,7 @@ p, li { white-space: pre-wrap; } - Size hint for Description cover thumbnails + Size hint for cover thumbnails included in Descriptions section. inch @@ -362,7 +348,7 @@ p, li { white-space: pre-wrap; } - Custom column source for note to include in Description header area + Custom column source for text to include in Description section. @@ -404,7 +390,7 @@ p, li { white-space: pre-wrap; } - Additional content merged with Comments during catalog generation + Custom column containing additional content to be merged with Comments metadata. @@ -418,7 +404,7 @@ p, li { white-space: pre-wrap; } - Merge additional content before Comments + Merge additional content before Comments metadata. &Before @@ -428,7 +414,7 @@ p, li { white-space: pre-wrap; } - Merge additional content after Comments + Merge additional content after Comments metadata. &After @@ -445,7 +431,7 @@ p, li { white-space: pre-wrap; } - Separate Comments and additional content with horizontal rule + Separate Comments metadata and additional content with a horizontal rule. &Separator diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 63fe02d5f1..825778f0d5 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -470,6 +470,7 @@ class CatalogBuilder(object): return self.__output_profile.empty_ratings_char return property(fget=fget) @dynamic_property + def READ_PROGRESS_SYMBOL(self): def fget(self): return "▪" if self.generateForKindle else '+' @@ -672,7 +673,7 @@ Author '{0}': for record in data: matched = list(set(record['tags']) & set(exclude_tags)) if matched : - self.opts.log.info(" - %s (Exclusion rule %s)" % (record['title'], matched)) + self.opts.log.info(" - %s (Exclusion rule Tags: '%s')" % (record['title'], str(matched[0]))) search_phrase = '' if exclude_tags: @@ -4038,7 +4039,9 @@ Author '{0}': if re.search(pat, unicode(field_contents), re.IGNORECASE) is not None: if self.opts.verbose: - self.opts.log.info(" excluding '%s' (%s:%s)" % (record['title'], field, pat)) + field_md = self.db.metadata_for_field(field) + self.opts.log.info(" - %s (Exclusion rule '%s': %s:%s)" % + (record['title'], field_md['name'], field,pat)) exclusion_set.append(record) if record in filtered_data_set: filtered_data_set.remove(record) From b171273f9c508332b51b72d3b1df4eee332223d3 Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 6 Aug 2012 20:19:20 -0400 Subject: [PATCH 22/27] Update Qqick start guide. --- resources/quick_start.epub | Bin 130585 -> 130580 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/quick_start.epub b/resources/quick_start.epub index 6f8f7046cd8d6cfc55ad220c6e0c381fab201f5b..a3f74213a6a374ebb271a570214af6962283c8c4 100644 GIT binary patch delta 734 zcmbRFhkeQ)_6;wj_^RfzIUfA>x9uAv14A|QqnauEHUwF$k=QZ2au+Y$_N7$1l-k4adu^^Lw%k>+R&$8^|zP0LS`&^m&`{nDz zp5NZoP+hWhTiS_58xAx0rcE_Ftr&fI(cd0N-%A>i5zWqNI@bNng=UrW&#c$iS5QB{ ze~*-Zy!wG#bLwBI&hdXHT>s-w*1s~YK#9V-kA18RI_j&~=H#2rF1XCSHu}>~_kDM{ zT`tc1^g&iDNQaNb=q%$)=|l zSIF;K;a9RtNA5k_hhxv!(jxm(yZk=9nx9ZV(JFrZae>aYWq$g_P4Dk7xZP>x|E>DU zo4fzZO|DALk`i6h>%=b9VpI0Z!h^egMb65Cg}kfM7WB=TrS4f>b9Y{EjR~LX%j%XR z)0Uc=_H@kgyZ$8JyLy^;`(mf{0#%9Z>wHpWE2q259!!<_H*Ld%-~UYhelnJ3w`U0O zW@Zs#nEwA4qr~Lv0_x3KvfHy{8BOmh$}x*DFmP}%pr#lG20aD{V4Qy77h@umg6w2R zS%c}{e=-V8PyWp)#gu0IZ{sP%1ejxVbIzRU5)qfea7++0a_LtF_`R_Nz>GIzh#l-@=S=m6g3j?7f MkoELCFc~ud0NQdx>Hq)$ delta 696 zcmV;p0!RIn{0Eu*2e9ZG4zZi#KD0aHcl!YV0Eq*W(GV?vFfMp#bZu;nQ`>ISKoEWR zS4{V@!XCR35(yiZ)Ty{AltNMgs;X*jZ{t<$U28pQ5+Oc_-{8ww+bNd_D*WKf>^W!7 zT+C0mRSEafHp=KRi-M4W)S1bZUXIzC?)+yA8WK7erO|TCv|*>Q$B(A(lEvrCb12C1 zFR#vKuM=Q@KH|qgf;e4F;nSr>EcWo2 zA6+NY$zpPSjGhC&sKrvk7pR0?D($C+$W$YE8id_d)ANTTGWDPFua1t#H#f0Uav6K4 zu5|2c_jqdZ9b~0wT)(ou0jx7uLvb>=q$2SJF=%^nL$4~7YmRq2}C-{M~w-G-&jlqmgq zA*`H~Y9+yO;Y!!WlC!b*!%@V+Z{>4rr`1gb&uwt*{evGJw;vq=d)=3s`2h?Sv^(N= z`vCv|i30!tE&u=k000000GGS^0bv3q9g}ehDVIn40Wkt|G?Q@&DVL-B0Wku#Ig@b- zD3|{G0Wtw;mofYSTLKP2lW_?tlTJYgm%jV~Dgus3lW_?tlg>y7mm2*6Dgrr9lW_?t emv8+6F#+$Fq5T0v0s{A!=KTRE2I}_#0001Ms799n From d703ef3ebef98392903bfaf0b2395d71db4aabb1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Aug 2012 09:22:32 +0530 Subject: [PATCH 23/27] Fix #1033624 (won't reconize HTC one s) --- src/calibre/devices/android/driver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 36dbcfafb7..12e7f40301 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -43,6 +43,7 @@ class ANDROID(USBMS): 0xccf : HTC_BCDS, 0xcd6 : HTC_BCDS, 0xce5 : HTC_BCDS, + 0xcec : HTC_BCDS, 0x2910 : HTC_BCDS, 0xff9 : HTC_BCDS, }, From 08f38df9b9b3f63ba6c652ce09fb184154210e8f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Aug 2012 10:41:48 +0530 Subject: [PATCH 24/27] MOBI Output: Fix ToC at start option having no effect when converting some input documents that have an out-of-spine ToC. Fixes #1033656 (Table of Contents not in Front after converting) --- src/calibre/ebooks/oeb/transforms/htmltoc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/oeb/transforms/htmltoc.py b/src/calibre/ebooks/oeb/transforms/htmltoc.py index 26b2ccf41c..0e1b2b5f10 100644 --- a/src/calibre/ebooks/oeb/transforms/htmltoc.py +++ b/src/calibre/ebooks/oeb/transforms/htmltoc.py @@ -73,7 +73,10 @@ class HTMLTOCAdder(object): if (hasattr(item.data, 'xpath') and XPath('//h:a[@href]')(item.data)): if oeb.spine.index(item) < 0: - oeb.spine.add(item, linear=False) + if self.position == 'end': + oeb.spine.add(item, linear=False) + else: + oeb.spine.insert(0, item, linear=True) return elif has_toc: oeb.guide.remove('toc') From de21068e728e912ac4e27e57fa5df5949b590c4e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Aug 2012 10:47:43 +0530 Subject: [PATCH 25/27] Fix #1033430 (line scrolling do not stop at page break in paged mode) --- src/calibre/gui2/viewer/documentview.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 18a8005ab2..0bae46d717 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -1036,14 +1036,14 @@ class DocumentView(QWebView): # {{{ if not self.handle_key_press(event): return QWebView.keyPressEvent(self, event) - def paged_col_scroll(self, forward=True): + def paged_col_scroll(self, forward=True, scroll_past_end=True): dir = 'next' if forward else 'previous' loc = self.document.javascript( 'paged_display.%s_col_location()'%dir, typ='int') if loc > -1: self.document.scroll_to(x=loc, y=0) self.manager.scrolled(self.document.scroll_fraction) - else: + elif scroll_past_end: (self.manager.next_document() if forward else self.manager.previous_document()) @@ -1059,7 +1059,8 @@ class DocumentView(QWebView): # {{{ self.is_auto_repeat_event = False elif key == 'Down': if self.document.in_paged_mode: - self.paged_col_scroll() + self.paged_col_scroll(scroll_past_end=not + self.document.line_scrolling_stops_on_pagebreaks) else: if (not self.document.line_scrolling_stops_on_pagebreaks and self.document.at_bottom): @@ -1068,7 +1069,8 @@ class DocumentView(QWebView): # {{{ self.scroll_by(y=15) elif key == 'Up': if self.document.in_paged_mode: - self.paged_col_scroll(forward=False) + self.paged_col_scroll(forward=False, scroll_past_end=not + self.document.line_scrolling_stops_on_pagebreaks) else: if (not self.document.line_scrolling_stops_on_pagebreaks and self.document.at_top): From 21022a6c0d337f46ceb0638909bbf05c4d3006d5 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 7 Aug 2012 09:04:46 +0200 Subject: [PATCH 26/27] Move the new collections option to the end to avoid trouble. --- .../devices/smart_device_app/driver.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 5fb9761d32..55b9deeff1 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -111,14 +111,6 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): ALL_BY_AUTHOR = _('All by author') EXTRA_CUSTOMIZATION_MESSAGE = [ - _('Comma separated list of metadata fields ' - 'to turn into collections on the device. Possibilities include: ')+\ - 'series, tags, authors' +\ - _('. Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add ' - 'these values to the list to enable them. The collections will be ' - 'given the name provided after the ":" character.')%dict( - abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR), - '', _('Enable connections at startup') + ':::

' + _('Check this box to allow connections when calibre starts') + '

', '', @@ -132,23 +124,31 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): _('Enter the port number the driver is to use if the "fixed port" box is checked') + '

', _('Print extra debug information') + ':::

' + _('Check this box if requested when reporting problems') + '

', + '', + _('Comma separated list of metadata fields ' + 'to turn into collections on the device. Possibilities include: ')+\ + 'series, tags, authors' +\ + _('. Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add ' + 'these values to the list to enable them. The collections will be ' + 'given the name provided after the ":" character.')%dict( + abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR) ] EXTRA_CUSTOMIZATION_DEFAULT = [ - 'tags, series', - '', False, '', '', '', False, '9090', False, + '', + '' ] - OPT_COLLECTIONS = 0 - OPT_AUTOSTART = 2 - OPT_PASSWORD = 4 - OPT_USE_PORT = 6 - OPT_PORT_NUMBER = 7 - OPT_EXTRA_DEBUG = 8 + OPT_AUTOSTART = 0 + OPT_PASSWORD = 2 + OPT_USE_PORT = 4 + OPT_PORT_NUMBER = 5 + OPT_EXTRA_DEBUG = 6 + OPT_COLLECTIONS = 8 def __init__(self, path): self.sync_lock = threading.RLock() From 89472f78ec2a1bdb9ebe5a99ecc140b1a1585678 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Aug 2012 13:55:58 +0530 Subject: [PATCH 27/27] Generate a PDF version of the User Manual --- manual/Makefile | 2 +- manual/conf.py | 17 +++++++++++++---- manual/custom.py | 2 ++ manual/index.rst | 2 +- manual/latex.py | 25 +++++++++++++++++++++++++ setup/publish.py | 9 +++++++++ 6 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 manual/latex.py diff --git a/manual/Makefile b/manual/Makefile index c1a2279abf..a21de12bed 100644 --- a/manual/Makefile +++ b/manual/Makefile @@ -60,7 +60,7 @@ htmlhelp: latex: mkdir -p .build/latex .build/doctrees - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex + $(SPHINXBUILD) -b mylatex $(ALLSPHINXOPTS) .build/latex @echo @echo "Build finished; the LaTeX files are in .build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ diff --git a/manual/conf.py b/manual/conf.py index 7b24f2f50a..967b6f0c65 100644 --- a/manual/conf.py +++ b/manual/conf.py @@ -14,10 +14,10 @@ import sys, os # If your extensions are in another directory, add it here. -sys.path.append(os.path.abspath('../src')) sys.path.append(os.path.abspath('.')) -__appname__ = os.environ.get('__appname__', 'calibre') -__version__ = os.environ.get('__version__', '0.0.0') +import init_calibre +init_calibre +from calibre.constants import __appname__, __version__ import custom custom # General configuration @@ -154,7 +154,8 @@ latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). -#latex_documents = [] +latex_documents = [('index', 'calibre.tex', 'calibre User Manual', + 'Kovid Goyal', 'manual', False)] # Additional stuff for the LaTeX preamble. #latex_preamble = '' @@ -164,3 +165,11 @@ latex_font_size = '10pt' # If false, no module index is generated. #latex_use_modindex = True + +latex_logo = 'resources/logo.png' +latex_show_pagerefs = True +latex_show_urls = 'footnote' +latex_elements = { +'papersize':'letterpaper', +'fontenc':r'\usepackage[T2A,T1]{fontenc}' +} diff --git a/manual/custom.py b/manual/custom.py index fdfb5711bb..30ca28ec96 100644 --- a/manual/custom.py +++ b/manual/custom.py @@ -14,6 +14,7 @@ from sphinx.util.console import bold sys.path.append(os.path.abspath('../../../')) from calibre.linux import entry_points from epub import EPUBHelpBuilder +from latex import LaTeXHelpBuilder def substitute(app, doctree): pass @@ -251,6 +252,7 @@ def template_docs(app): def setup(app): app.add_config_value('kovid_epub_cover', None, False) app.add_builder(EPUBHelpBuilder) + app.add_builder(LaTeXHelpBuilder) app.connect('doctree-read', substitute) app.connect('builder-inited', generate_docs) app.connect('build-finished', finished) diff --git a/manual/index.rst b/manual/index.rst index fa89dba95f..b8f98a5561 100755 --- a/manual/index.rst +++ b/manual/index.rst @@ -17,7 +17,7 @@ To get started with more advanced usage, you should read about the :ref:`Graphic .. only:: online - **An ebook version of this user manual is available in** `EPUB format `_ and `AZW3 (Kindle Fire) format `_. + **An ebook version of this user manual is available in** `EPUB format `_, `AZW3 (Kindle Fire) format `_ and `PDF format `_. Sections ------------ diff --git a/manual/latex.py b/manual/latex.py new file mode 100644 index 0000000000..95f38eab20 --- /dev/null +++ b/manual/latex.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os + + +from sphinx.builders.latex import LaTeXBuilder + +class LaTeXHelpBuilder(LaTeXBuilder): + name = 'mylatex' + + def finish(self): + LaTeXBuilder.finish(self) + self.info('Fixing Cyrillic characters...') + tex = os.path.join(self.outdir, 'calibre.tex') + with open(tex, 'r+b') as f: + raw = f.read().replace(b'Михаил Горбачёв', + br'{\fontencoding{T2A}\selectfont Михаил Горбачёв}') + f.seek(0) + f.write(raw) diff --git a/setup/publish.py b/setup/publish.py index e43c9fdf7f..fd0dd48900 100644 --- a/setup/publish.py +++ b/setup/publish.py @@ -80,8 +80,17 @@ class Manual(Command): '-d', '.build/doctrees', '.', '.build/html']) subprocess.check_call(['sphinx-build', '-b', 'myepub', '-d', '.build/doctrees', '.', '.build/epub']) + subprocess.check_call(['sphinx-build', '-b', 'mylatex', '-d', + '.build/doctrees', '.', '.build/latex']) + pwd = os.getcwdu() + os.chdir('.build/latex') + subprocess.check_call(['make', 'all-pdf'], stdout=open(os.devnull, + 'wb')) + os.chdir(pwd) epub_dest = self.j('.build', 'html', 'calibre.epub') + pdf_dest = self.j('.build', 'html', 'calibre.pdf') shutil.copyfile(self.j('.build', 'epub', 'calibre.epub'), epub_dest) + shutil.copyfile(self.j('.build', 'latex', 'calibre.pdf'), pdf_dest) subprocess.check_call(['ebook-convert', epub_dest, epub_dest.rpartition('.')[0] + '.azw3', '--page-breaks-before=/', '--disable-font-rescaling',