merge from trunk

This commit is contained in:
ldolse 2011-02-13 09:51:43 +08:00
commit 7053884ea4
144 changed files with 84248 additions and 65704 deletions

View File

@ -19,6 +19,106 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.7.45
date: 2011-02-11
new features:
- title: "Add plugin to download series information from the Kent District Library"
- title: "Kindle driver: When uploading MOBI files to the device, upload page number information as well (used by the not yet released Kindle 3.1 firmware)"
- title: "When automatically sending news to device, send to main memory preferentially, if it has enough space."
tickets: [8877]
- title: "Allow customization of which metadata fields are searched by default (click the preferences icon next to the search box)"
- title: "New format TXTZ: which is a zip file containing the TXT file and associated images + metadata. calibre can convert to and from this format and read/write metadata to it."
- title: "New option to control how automerge handles duplicate formats when adding books to your calibre library. See Preferences->Adding books"
- title: "Driver for Nokia X6, Nexus S, WiBook, MyTouch 4G and Huawei Ideos S7"
- title: "Nicer interface for editing tweaks"
- title: "Add tweak to remove yellow lines from edges of book list"
- title: "Completion: Restore adding of comma at end after completion for tags type fields. Add a tweak to control if an & is added after completion for author type fields"
- title: "Turn search as you type off by default for searching the book list. You can turn it on by clicking the preferences button next to the search bar."
- title: "TXT Input: Add option to remove indents and fix bug where spaces were not retained properly."
bug fixes:
- title: "Fix a regression in 0.7.44 that could cause setting authors to fail in windows when the author name is very long"
tickets: [8797]
- title: "E-book viewer: Fix bug that could cause the bottom of chapters to get cut-off if the topmost element had a large top margin."
tickets: [8791]
- title: "Fix regression that caused a spurious error message after moving a library. Also ensure that the entries in the Copy to Library menu are updated after a library is moved/renamed/deleted."
tickets: [8905]
- title: "PML Input: New handling of t and T tags. T's that do not start the line are ignored. t's that start and end the line use a margin for the text block"
- title: "News download: Remove all invalid ASCII control characters from article descriptions as they cause XML parsing to fail"
- title: "MOBI Output: Fix bug that was discarding non breaking spaces at the start of a paragraph when they were followed immediately by a tag."
tickets: [4887]
- title: "LIT Input: Fix a regression in handling LIT files that contain txt rather than html data"
tickets: [8904]
- title: "Fix bug in search box in the plugins dialog"
tickets: [8882]
- title: "Fix renaming of categories via the Tag Browser"
tickets: [8807]
- title: "Content server: Do not send mobile version to iPad"
tickets: [8820]
- title: "Fix undefined publication date appearing in book jacket as 101"
tickets: [8799]
- title: "Heuristics: Fix issue with invalid markup from italicize patterns."
- title: "TXT Input: De-hyphenate textile and markdown inpu as well. Fix inline toc not showing all items."
- title: "RTF Input: More encoding token splitting fixes."
- title: "Fix regression that broke the convenience Email to xxx entry in the connect share menu."
tickets: [8775]
- title: "Fix editing of series type custom columns in the book list."
tickets: [8765]
improved recipes:
- El periodico de Aragon
- B92
- French Belgian news sources
new recipes:
- title: "ABC.es"
author: "Ricardo Jurado"
- title: "Korespondent and Kopalnia Wiedzy"
author: "Attis"
- title: "Radio Prague"
author: "Francois Pellicaan"
- title: "Europa Press"
author: "Luis Hernandez"
- title: "Interoperability Happens and njuz.net"
author: "Darko Miletic"
- title: "Weblogs SL"
author: "desUBIKado"
- title: "Kompas and Jakarta Post"
author: "Adrian Gunawan"
- version: 0.7.44 - version: 0.7.44
date: 2011-02-04 date: 2011-02-04

69
format_docs/pdb/apnx.txt Normal file
View File

@ -0,0 +1,69 @@
APNX
----
apnx files are used by the Amazon Kindle (firmware revision 3.1+) to
map pages from a print book to the Kindle version. Integers within
the file are big-endian.
Layout
------
bytes content comments
4 00010001 Format identifier. Value of 65537 little-endian.
4 start of next The offset after ending location of the first header.
Starts a new sequence of header info
4 length Length of first header
N first header String containing content header
Starts next sequence
2 unknown Always 1
2 length Length of second header
2 page count Total number of bytes after second header that
represent pages. This total includes bytes that
are ignored by the pageMap.
2 unknown Always 32
N second header String containing the page mapping header
4*N padding The first number given in the page mapping header indicates the number of 0 bytes.
4*N page list
Content Header
--------------
The content header is a string enclosed in {} containing key, value pairs.
content comments
contentGuid Guid.
asin Amazon identifier for the Kindle version of the book.
cdeType MOBI cdeType. Should always be EBOK for ebooks.
fileRevisionId Revision of this file.
Example:
{"contentGuid":"d8c14b0","asin":"B000JML5VM","cdeType":"EBOK","fileRevisionId":"1296874359405"}
Page Mapping Header
-------------------
The page mapping header is a string enclosed in {} containing key, value pairs.
content comments
asin The ISBN 10 for the paper book the pages correspond to
pageMap Three value tuple. Looks like: "(N,N,N)"
1) Number of bytes after header that starts the page numbering sequence
2) unknown
3) unknown
Example:
{"asin":"1906694184","pageMap":"(4,a,1)"}
Page List
---------
The page list is a sequence of offsets in the uncompressed HTML. Each
value is the beginning of a new page. Each entry is a 4 byte big endian
int. The list is ordered lowest to highest.

View File

@ -10,7 +10,7 @@ you know what you are doing. If you delete this file, it will be recreated from
defaults. defaults.
''' '''
#: Auto increment series index
# The algorithm used to assign a new book in an existing series a series number. # The algorithm used to assign a new book in an existing series a series number.
# New series numbers assigned using this tweak are always integer values, except # New series numbers assigned using this tweak are always integer values, except
# if a constant non-integer is specified. # if a constant non-integer is specified.
@ -29,14 +29,16 @@ defaults.
# series_index_auto_increment = 16.5 # series_index_auto_increment = 16.5
series_index_auto_increment = 'next' series_index_auto_increment = 'next'
#: Add separator after completing an author name
# Should the completion separator be append # Should the completion separator be append
# to the end of the completed text to # to the end of the completed text to
# automatically begin a new completion operation. # automatically begin a new completion operation
# for authors.
# Can be either True or False # Can be either True or False
completer_append_separator = False authors_completer_append_separator = False
#: Author sort name algorithm
# The algorithm used to copy author to author_sort # The algorithm used to copy author to author_sort
# Possible values are: # Possible values are:
# invert: use "fn ln" -> "ln, fn" (the default algorithm) # invert: use "fn ln" -> "ln, fn" (the default algorithm)
@ -48,6 +50,7 @@ completer_append_separator = False
# selecting 'manage authors', and pressing 'Recalculate all author sort values'. # selecting 'manage authors', and pressing 'Recalculate all author sort values'.
author_sort_copy_method = 'invert' author_sort_copy_method = 'invert'
#: Use author sort in Tag Browser
# Set which author field to display in the tags pane (the list of authors, # Set which author field to display in the tags pane (the list of authors,
# series, publishers etc on the left hand side). The choices are author and # series, publishers etc on the left hand side). The choices are author and
# author_sort. This tweak affects only what is displayed under the authors # author_sort. This tweak affects only what is displayed under the authors
@ -62,6 +65,7 @@ author_sort_copy_method = 'invert'
# categories_use_field_for_author_name = 'author_sort' # categories_use_field_for_author_name = 'author_sort'
categories_use_field_for_author_name = 'author' categories_use_field_for_author_name = 'author'
#: Control partitioning of Tag Browser
# When partitioning the tags browser, the format of the subcategory label is # When partitioning the tags browser, the format of the subcategory label is
# controlled by a template: categories_collapsed_name_template if sorting by # controlled by a template: categories_collapsed_name_template if sorting by
# name, categories_collapsed_rating_template if sorting by average rating, and # name, categories_collapsed_rating_template if sorting by average rating, and
@ -73,24 +77,25 @@ categories_use_field_for_author_name = 'author'
# author category will be the name of the author. The sub-values available are: # author category will be the name of the author. The sub-values available are:
# name: the printable name of the item # name: the printable name of the item
# count: the number of books that references this item # count: the number of books that references this item
# avg_rating: the averate rating of all the books referencing this item # avg_rating: the average rating of all the books referencing this item
# sort: the sort value. For authors, this is the author_sort for that author # sort: the sort value. For authors, this is the author_sort for that author
# category: the category (e.g., authors, series) that the item is in. # category: the category (e.g., authors, series) that the item is in.
# Note that the "r'" in front of the { is necessary if there are backslashes # Note that the "r'" in front of the { is necessary if there are backslashes
# (\ characters) in the template. It doesn't hurt anything to leave it there # (\ characters) in the template. It doesn't hurt anything to leave it there
# even if there aren't any backslashes. # even if there aren't any backslashes.
categories_collapsed_name_template = r'{first.sort:shorten(4,'',0)} - {last.sort:shorten(4,'',0)}' categories_collapsed_name_template = r'{first.sort:shorten(4,"",0)} - {last.sort:shorten(4,"",0)}'
categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}' categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}' categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}'
#: Set boolean custom columns to be tristate
# Set whether boolean custom columns are two- or three-valued. # Set whether boolean custom columns are two- or three-valued.
# Two-values for true booleans # Two-values for true booleans
# three-values for yes/no/unknown # three-values for yes/no/unknown
# Set to 'yes' for three-values, 'no' for two-values # Set to 'yes' for three-values, 'no' for two-values
bool_custom_columns_are_tristate = 'yes' bool_custom_columns_are_tristate = 'yes'
#: Specify columns to sort the booklist by on startup
# Provide a set of columns to be sorted on when calibre starts # Provide a set of columns to be sorted on when calibre starts
# The argument is None if saved sort history is to be used # The argument is None if saved sort history is to be used
# otherwise it is a list of column,order pairs. Column is the # otherwise it is a list of column,order pairs. Column is the
@ -100,6 +105,7 @@ bool_custom_columns_are_tristate = 'yes'
# title within authors. # title within authors.
sort_columns_at_startup = None sort_columns_at_startup = None
#; Control how dates are displayed
# Format to be used for publication date and the timestamp (date). # Format to be used for publication date and the timestamp (date).
# A string controlling how the publication date is displayed in the GUI # A string controlling how the publication date is displayed in the GUI
# d the day as number without a leading zero (1 to 31) # d the day as number without a leading zero (1 to 31)
@ -120,25 +126,34 @@ sort_columns_at_startup = None
gui_pubdate_display_format = 'MMM yyyy' gui_pubdate_display_format = 'MMM yyyy'
gui_timestamp_display_format = 'dd MMM yyyy' gui_timestamp_display_format = 'dd MMM yyyy'
# Control title and series sorting in the library view. #: Control sorting of titles and series in the library display
# If set to 'library_order', Leading articles such as The and A will be ignored. # Control title and series sorting in the library view. If set to
# If set to 'strictly_alphabetic', the titles will be sorted without processing # 'library_order', the title sort field will be used instead of the title.
# For example, with library_order, The Client will sort under 'C'. With # Unless you have manually edited the title sort field, leading articles such as
# strictly_alphabetic, the book will sort under 'T'. # The and A will be ignored. If set to 'strictly_alphabetic', the titles will be
# sorted as-is (sort by title instead of title sort). For example, with
# library_order, The Client will sort under 'C'. With strictly_alphabetic, the
# book will sort under 'T'.
# This flag affects Calibre's library display. It has no effect on devices. In # This flag affects Calibre's library display. It has no effect on devices. In
# addition, titles for books added before changing the flag will retain their # addition, titles for books added before changing the flag will retain their
# order until the title is edited. Double-clicking on a title and hitting return # order until the title is edited. Double-clicking on a title and hitting return
# without changing anything is sufficient to change the sort. # without changing anything is sufficient to change the sort.
title_series_sorting = 'library_order' title_series_sorting = 'library_order'
#: Control formatting of title and series when used in templates
# Control how title and series names are formatted when saving to disk/sending # Control how title and series names are formatted when saving to disk/sending
# to device. If set to library_order, leading articles such as The and A will # to device. The behavior depends on the field being processed. If processing
# be put at the end # title, then if this tweak is set to 'library_order', the title will be
# If set to 'strictly_alphabetic', the titles will be sorted without processing # replaced with title_sort. If it is set to 'strictly_alphabetic', then the
# For example, with library_order, "The Client" will become "Client, The". With # title will not be changed. If processing series, then if set to
# strictly_alphabetic, it would remain "The Client". # 'library_order', articles such as 'The' and 'An' will be moved to the end. If
# set to 'strictly_alphabetic', the series will be sent without change.
# For example, if the tweak is set to library_order, "The Lord of the Rings"
# will become "Lord of the Rings, The". If the tweak is set to
# strictly_alphabetic, it would remain "The Lord of the Rings".
save_template_title_series_sorting = 'library_order' save_template_title_series_sorting = 'library_order'
#: Set the list of words considered to be "articles" for sort strings
# Set the list of words that are to be considered 'articles' when computing the # Set the list of words that are to be considered 'articles' when computing the
# title sort strings. The list is a regular expression, with the articles # title sort strings. The list is a regular expression, with the articles
# separated by 'or' bars. Comparisons are case insensitive, and that cannot be # separated by 'or' bars. Comparisons are case insensitive, and that cannot be
@ -148,7 +163,7 @@ save_template_title_series_sorting = 'library_order'
# Default: '^(A|The|An)\s+' # Default: '^(A|The|An)\s+'
title_sort_articles=r'^(A|The|An)\s+' title_sort_articles=r'^(A|The|An)\s+'
#: Specify a folder calibre should connect to at startup
# Specify a folder that calibre should connect to at startup using # Specify a folder that calibre should connect to at startup using
# connect_to_folder. This must be a full path to the folder. If the folder does # connect_to_folder. This must be a full path to the folder. If the folder does
# not exist when calibre starts, it is ignored. If there are '\' characters in # not exist when calibre starts, it is ignored. If there are '\' characters in
@ -158,7 +173,7 @@ title_sort_articles=r'^(A|The|An)\s+'
# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library' # auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
auto_connect_to_folder = '' auto_connect_to_folder = ''
#: Specify renaming rules for SONY collections
# Specify renaming rules for sony collections. This tweak is only applicable if # Specify renaming rules for sony collections. This tweak is only applicable if
# metadata management is set to automatic. Collections on Sonys are named # metadata management is set to automatic. Collections on Sonys are named
# depending upon whether the field is standard or custom. A collection derived # depending upon whether the field is standard or custom. A collection derived
@ -211,7 +226,7 @@ auto_connect_to_folder = ''
sony_collection_renaming_rules={} sony_collection_renaming_rules={}
sony_collection_name_template='{value}{category:| (|)}' sony_collection_name_template='{value}{category:| (|)}'
#: Specify how SONY collections are sorted
# Specify how sony collections are sorted. This tweak is only applicable if # Specify how sony collections are sorted. This tweak is only applicable if
# metadata management is set to automatic. You can indicate which metadata is to # metadata management is set to automatic. You can indicate which metadata is to
# be used to sort on a collection-by-collection basis. The format of the tweak # be used to sort on a collection-by-collection basis. The format of the tweak
@ -230,7 +245,7 @@ sony_collection_name_template='{value}{category:| (|)}'
sony_collection_sorting_rules = [] sony_collection_sorting_rules = []
# Create search terms to apply a query across several built-in search terms. #: Create search terms to apply a query across several built-in search terms.
# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...} # Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...}
# Example: create the term 'myseries' that when used as myseries:foo would # Example: create the term 'myseries' that when used as myseries:foo would
# search all of the search categories 'series', '#myseries', and '#myseries2': # search all of the search categories 'series', '#myseries', and '#myseries2':
@ -243,15 +258,17 @@ sony_collection_sorting_rules = []
grouped_search_terms = {} grouped_search_terms = {}
# Set this to True (not 'True') to ensure that tags in 'Tags to add when adding #: Control how tags are applied when copying books to another library
# Set this to True to ensure that tags in 'Tags to add when adding
# a book' are added when copying books to another library # a book' are added when copying books to another library
add_new_book_tags_when_importing_books = False add_new_book_tags_when_importing_books = False
# Set the maximum number of tags to show per book in the content server #: Set the maximum number of tags to show per book in the content server
max_content_server_tags_shown=5 max_content_server_tags_shown=5
# Set custom metadata fields that the content server will or will not display.
#: Set custom metadata fields that the content server will or will not display.
# content_server_will_display is a list of custom fields to be displayed. # content_server_will_display is a list of custom fields to be displayed.
# content_server_wont_display is a list of custom fields not to be displayed. # content_server_wont_display is a list of custom fields not to be displayed.
# wont_display has priority over will_display. # wont_display has priority over will_display.
@ -269,13 +286,27 @@ max_content_server_tags_shown=5
content_server_will_display = ['*'] content_server_will_display = ['*']
content_server_wont_display = [] content_server_wont_display = []
# Same as above (content server) but for the book details pane. Same syntax. #: Set custom metadata fields that the book details panel will or will not display.
# book_details_will_display is a list of custom fields to be displayed.
# book_details_wont_display is a list of custom fields not to be displayed.
# wont_display has priority over will_display.
# The special value '*' means all custom fields. The value [] means no entries.
# Defaults:
# book_details_will_display = ['*']
# book_details_wont_display = []
# Examples:
# To display only the custom fields #mytags and #genre:
# book_details_will_display = ['#mytags', '#genre']
# book_details_wont_display = []
# To display all fields except #mycomments:
# book_details_will_display = ['*']
# book_details_wont_display['#mycomments']
# As above, this tweak affects only display of custom fields. The standard # As above, this tweak affects only display of custom fields. The standard
# fields are not affected # fields are not affected
book_details_will_display = ['*'] book_details_will_display = ['*']
book_details_wont_display = [] book_details_wont_display = []
#: Set the maximum number of sort 'levels'
# Set the maximum number of sort 'levels' that calibre will use to resort the # Set the maximum number of sort 'levels' that calibre will use to resort the
# library after certain operations such as searches or device insertion. Each # library after certain operations such as searches or device insertion. Each
# sort level adds a performance penalty. If the database is large (thousands of # sort level adds a performance penalty. If the database is large (thousands of
@ -283,16 +314,14 @@ book_details_wont_display = []
# level sorts, and if you are seeing a slowdown, reduce the value of this tweak. # level sorts, and if you are seeing a slowdown, reduce the value of this tweak.
maximum_resort_levels = 5 maximum_resort_levels = 5
# Absolute path to a TTF font file to use as the font for the title and author #: Specify which font to use when generating a default cover
# when generating a default cover. Useful if the default font (Liberation # Absolute path to .ttf font files to use as the fonts for the title, author
# and footer when generating a default cover. Useful if the default font (Liberation
# Serif) does not contain glyphs for the language of the books in your library. # Serif) does not contain glyphs for the language of the books in your library.
generate_cover_title_font = None generate_cover_title_font = None
# Absolute path to a TTF font file to use as the font for the footer in the
# default cover
generate_cover_foot_font = None generate_cover_foot_font = None
#: Control behavior of double clicks on the book list
# Behavior of doubleclick on the books list. Choices: open_viewer, do_nothing, # Behavior of doubleclick on the books list. Choices: open_viewer, do_nothing,
# edit_cell, edit_metadata. Selecting edit_metadata has the side effect of # edit_cell, edit_metadata. Selecting edit_metadata has the side effect of
# disabling editing a field using a single click. # disabling editing a field using a single click.
@ -301,7 +330,8 @@ generate_cover_foot_font = None
doubleclick_on_library_view = 'open_viewer' doubleclick_on_library_view = 'open_viewer'
# Language to use when sorting. Setting this tweak will force sorting to use the #: Language to use when sorting.
# Setting this tweak will force sorting to use the
# collating order for the specified language. This might be useful if you run # collating order for the specified language. This might be useful if you run
# calibre in English but want sorting to work in the language where you live. # calibre in English but want sorting to work in the language where you live.
# Set the tweak to the desired ISO 639-1 language code, in lower case. # Set the tweak to the desired ISO 639-1 language code, in lower case.
@ -312,12 +342,13 @@ doubleclick_on_library_view = 'open_viewer'
# Example: locale_for_sorting = 'nb' -- sort using Norwegian rules. # Example: locale_for_sorting = 'nb' -- sort using Norwegian rules.
locale_for_sorting = '' locale_for_sorting = ''
#: Number of columns for custom metadata in the edit metadata dialog
# Set whether to use one or two columns for custom metadata when editing # Set whether to use one or two columns for custom metadata when editing
# metadata one book at a time. If True, then the fields are laid out using two # metadata one book at a time. If True, then the fields are laid out using two
# columns. If False, one column is used. # columns. If False, one column is used.
metadata_single_use_2_cols_for_custom_fields = True metadata_single_use_2_cols_for_custom_fields = True
#: The number of seconds to wait before sending emails
# The number of seconds to wait before sending emails when using a # The number of seconds to wait before sending emails when using a
# public email server like gmail or hotmail. Default is: 5 minutes # public email server like gmail or hotmail. Default is: 5 minutes
# Setting it to lower may cause the server's SPAM controls to kick in, # Setting it to lower may cause the server's SPAM controls to kick in,
@ -325,3 +356,9 @@ metadata_single_use_2_cols_for_custom_fields = True
# calibre. # calibre.
public_smtp_relay_delay = 301 public_smtp_relay_delay = 301
#: Remove the bright yellow lines at the edges of the book list
# Control whether the bright yellow lines at the edges of book list are drawn
# when a section of the user interface is hidden. Changes will take effect
# after a restart of calibre.
draw_hidden_section_indicators = True

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

View File

@ -0,0 +1,68 @@
__license__ = 'GPL v3'
__author__ = 'Ricardo Jurado'
__copyright__ = 'Ricardo Jurado'
__version__ = 'v0.4'
__date__ = '11 February 2011'
'''
http://www.abc.es/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1296604369(BasicNewsRecipe):
title = u'ABC.es'
masthead_url = 'http://www.abc.es/img/logo-abc.gif'
cover_url = 'http://www.abc.es/img/logo-abc.gif'
publisher = u'Grupo VOCENTO'
__author__ = 'Ricardo Jurado'
description = 'Noticias de Spain y el mundo'
category = 'News,Spain,National,International,Economy'
oldest_article = 2
max_articles_per_feed = 10
no_stylesheets = True
use_embedded_content = False
encoding = 'ISO-8859-1'
remove_javascript = True
language = 'es'
extra_css = """
p{text-align: justify; font-size: 100%}
body{ text-align: left; font-size:100% }
h3{font-family: sans-serif; font-size:120%; font-weight:bold; text-align: justify; }
h2{font-family: sans-serif; font-size:100%; font-weight:bold; text-align: justify; }
h1{font-family: sans-serif; font-size:150%; font-weight:bold; text-align: justify; }
"""
keep_only_tags = [
# dict(name='h2', attrs={'class':['logos']}),
dict(name='h3', attrs={'class':['overhead']}),
dict(name='h1', attrs={'class':'headline'}),
dict(name='h3', attrs={'class':['subhead']}),
dict(name='div', attrs={'class':'datosi'}),
dict(name='div', attrs={'class':'photo-alt1'}),
dict(name='div', attrs={'class':'text'})
]
# remove_tags_before = dict(name='div' , attrs={'id':['cabecera2']})
feeds = [
(u'PORTADA', u'http://www.abc.es/rss/feeds/abcPortada.xml')
,(u'ULTIMAS', u'http://www.abc.es/rss/feeds/abc_ultima.xml')
,(u'NACIONAL', u'http://www.abc.es/rss/feeds/abc_EspanaEspana.xml')
,(u'INTERNACIONAL', u'http://www.abc.es/rss/feeds/abc_Internacional.xml')
,(u'OPINION', u'http://www.abc.es/rss/feeds/abc_opinioncompleto.xml')
,(u'BLOGS ABC', u'http://www.abc.es/rss/feeds/blogs-abc.xml')
,(u'ECONOMIA', u'http://www.abc.es/rss/feeds/abc_Economia.xml')
,(u'CIENCIA Y TECNOLOGIA', u'http://www.abc.es/rss/feeds/abc_Ciencia_Tecnologia.xml')
,(u'CULTURA', u'http://www.abc.es/rss/feeds/abc_Cultura.xml')
,(u'LIBROS', u'http://www.abc.es/rss/feeds/abc_Libros.xml')
,(u'MEDIOS Y REDES', u'http://www.abc.es/rss/feeds/ABC_Medios_Redes.xml')
,(u'EVASION', u'http://www.abc.es/rss/feeds/abc_evasion.xml')
,(u'ESPECTACULOS', u'http://www.abc.es/rss/feeds/abc_Espectaculos.xml')
,(u'GENTE', u'http://www.abc.es/rss/feeds/abc_Gente.xml')
,(u'DEPORTES', u'http://www.abc.es/rss/feeds/abc_Deportes.xml')
]

View File

@ -1,6 +1,6 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2008-2011, Darko Miletic <darko.miletic at gmail.com>'
''' '''
b92.net b92.net
''' '''
@ -10,7 +10,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
class B92(BasicNewsRecipe): class B92(BasicNewsRecipe):
title = 'B92' title = 'B92'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'B92 info, najnovije vesti iz Srbije, regiona i sveta' description = 'Najnovije vesti iz Srbije, regiona i sveta, aktuelne teme iz sveta politike, ekonomije, drustva, foto galerija, kolumne'
publisher = 'B92' publisher = 'B92'
category = 'news, politics, Serbia' category = 'news, politics, Serbia'
oldest_article = 2 oldest_article = 2
@ -20,7 +20,14 @@ class B92(BasicNewsRecipe):
encoding = 'cp1250' encoding = 'cp1250'
language = 'sr' language = 'sr'
publication_type = 'newsportal' publication_type = 'newsportal'
extra_css = ' @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: serif1, serif} ' masthead_url = 'http://www.b92.net/images/fp/logo.gif'
extra_css = """
@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
body{font-family: Arial,Helvetica,sans1,sans-serif}
.articledescription{font-family: serif1, serif}
.article-info2,.article-info1{text-transform: uppercase; font-size: small}
"""
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
@ -32,22 +39,25 @@ class B92(BasicNewsRecipe):
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='table', attrs={'class':'maindocument'})] keep_only_tags = [dict(attrs={'class':['article-info1','article-text']})]
remove_attributes = ['width','height','align','hspace','vspace','border']
remove_tags = [ remove_tags = [dict(name=['embed','link','base','meta'])]
dict(name='ul', attrs={'class':'comment-nav'})
,dict(name=['embed','link','base'] )
,dict(name='div', attrs={'class':'udokum'} )
]
feeds = [ feeds = [
(u'Vesti' , u'http://www.b92.net/info/rss/vesti.xml' ) (u'Vesti' , u'http://www.b92.net/info/rss/vesti.xml' )
,(u'Biz' , u'http://www.b92.net/info/rss/biz.xml' ) ,(u'Biz' , u'http://www.b92.net/info/rss/biz.xml' )
,(u'Sport' , u'http://www.b92.net/info/rss/sport.xml' )
,(u'Zivot' , u'http://www.b92.net/info/rss/zivot.xml' )
,(u'Kultura' , u'http://www.b92.net/info/rss/kultura.xml' )
,(u'Automobili' , u'http://www.b92.net/info/rss/automobili.xml')
,(u'Tehnopolis' , u'http://www.b92.net/info/rss/tehnopolis.xml')
] ]
def print_version(self, url):
return url + '&version=print'
def preprocess_html(self, soup): def preprocess_html(self, soup):
return self.adeify_images(soup) for item in soup.findAll(style=True):
del item['style']
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
cinebel.be cinebel.be
''' '''
@ -14,14 +14,14 @@ class Cinebel(BasicNewsRecipe):
description = u'Cinema news from Belgium in French' description = u'Cinema news from Belgium in French'
publisher = u'cinebel.be' publisher = u'cinebel.be'
category = 'news, cinema, movie, Belgium' category = 'news, cinema, movie, Belgium'
oldest_article = 3 oldest_article = 15
encoding = 'utf8' language = 'fr'
language = 'fr_BE'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
timefmt = ' [%d %b %Y]' timefmt = ' [%d %b %Y]'
filterDuplicates = True
keep_only_tags = [ keep_only_tags = [
dict(name = 'span', attrs = {'class': 'movieMainTitle'}) dict(name = 'span', attrs = {'class': 'movieMainTitle'})
@ -35,6 +35,13 @@ class Cinebel(BasicNewsRecipe):
,(u'Top 10' , u'http://www.cinebel.be/Servlets/RssServlet?languageCode=fr&rssType=2' ) ,(u'Top 10' , u'http://www.cinebel.be/Servlets/RssServlet?languageCode=fr&rssType=2' )
] ]
def preprocess_html(self, soup):
for alink in soup.findAll('a'):
if alink.has_key('href'):
tstr = "Site officiel: " + alink['href']
alink.replaceWith(tstr)
return soup
def get_cover_url(self): def get_cover_url(self):
cover_url = 'http://www.cinebel.be/portal/resources/common/logo_index.gif' cover_url = 'http://www.cinebel.be/portal/resources/common/logo_index.gif'
return cover_url return cover_url

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
dhnet.be dhnet.be
''' '''
@ -16,7 +16,8 @@ class DHNetBe(BasicNewsRecipe):
publisher = u'dhnet.be' publisher = u'dhnet.be'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
language = 'fr_BE' language = 'fr'
masthead_url = 'http://www.dhnet.be/images/homepage_logo_dh.gif'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True
@ -34,6 +35,13 @@ class DHNetBe(BasicNewsRecipe):
,(u'La Une Info' , u'http://www.dhnet.be/rss/dhinfos/' ) ,(u'La Une Info' , u'http://www.dhnet.be/rss/dhinfos/' )
] ]
def preprocess_html(self, soup):
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup
def get_cover_url(self): def get_cover_url(self):
cover_url = strftime('http://pdf-online.dhnet.be/pdfonline/image/%Y%m%d/dh_%Y%m%d_nam_infoge_001.pdf.L.jpg') cover_url = strftime('http://pdf-online.dhnet.be/pdfonline/image/%Y%m%d/dh_%Y%m%d_nam_infoge_001.pdf.L.jpg')
return cover_url return cover_url

View File

@ -5,8 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '04 December 2010, desUBIKado' __copyright__ = '04 December 2010, desUBIKado'
__author__ = 'desUBIKado' __author__ = 'desUBIKado'
__description__ = 'Daily newspaper from Aragon' __description__ = 'Daily newspaper from Aragon'
__version__ = 'v0.05' __version__ = 'v0.07'
__date__ = '07, December 2010' __date__ = '06, February 2011'
''' '''
elperiodicodearagon.com elperiodicodearagon.com
''' '''
@ -38,7 +38,8 @@ class elperiodicodearagon(BasicNewsRecipe):
,'publisher' : publisher ,'publisher' : publisher
} }
feeds = [(u'Arag\xf3n', u'http://elperiodicodearagon.com/RSS/2.xml'), feeds = [
(u'Arag\xf3n', u'http://elperiodicodearagon.com/RSS/2.xml'),
(u'Internacional', u'http://elperiodicodearagon.com/RSS/4.xml'), (u'Internacional', u'http://elperiodicodearagon.com/RSS/4.xml'),
(u'Espa\xf1a', u'http://elperiodicodearagon.com/RSS/3.xml'), (u'Espa\xf1a', u'http://elperiodicodearagon.com/RSS/3.xml'),
(u'Econom\xeda', u'http://elperiodicodearagon.com/RSS/5.xml'), (u'Econom\xeda', u'http://elperiodicodearagon.com/RSS/5.xml'),
@ -47,13 +48,16 @@ class elperiodicodearagon(BasicNewsRecipe):
(u'Opini\xf3n', u'http://elperiodicodearagon.com/RSS/103.xml'), (u'Opini\xf3n', u'http://elperiodicodearagon.com/RSS/103.xml'),
(u'Escenarios', u'http://elperiodicodearagon.com/RSS/105.xml'), (u'Escenarios', u'http://elperiodicodearagon.com/RSS/105.xml'),
(u'Sociedad', u'http://elperiodicodearagon.com/RSS/104.xml'), (u'Sociedad', u'http://elperiodicodearagon.com/RSS/104.xml'),
(u'Gente', u'http://elperiodicodearagon.com/RSS/330.xml')] (u'Gente', u'http://elperiodicodearagon.com/RSS/330.xml')
]
extra_css = ''' extra_css = '''
h3{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:xx-large;} h3 {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:30px;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} h2 {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:18px;}
dd{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} h4 {font-family:Arial,Helvetica,sans-serif; font-style:italic; font-weight:normal;font-size:20px;}
.columnaDeRecursosRelacionados {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:14px;}
img{margin-bottom: 0.4em}
''' '''
remove_attributes = ['height','width'] remove_attributes = ['height','width']
@ -82,6 +86,7 @@ class elperiodicodearagon(BasicNewsRecipe):
dict(name='a', attrs={'class':'AvisoComentario'}), dict(name='a', attrs={'class':'AvisoComentario'}),
dict(name='div', attrs={'class':'CajaAvisoComentario'}), dict(name='div', attrs={'class':'CajaAvisoComentario'}),
dict(name='div', attrs={'class':'navegaNoticias'}), dict(name='div', attrs={'class':'navegaNoticias'}),
dict(name='div', attrs={'class':'Mensaje'}),
dict(name='div', attrs={'id':'PaginadorDiCom'}), dict(name='div', attrs={'id':'PaginadorDiCom'}),
dict(name='div', attrs={'id':'CajaAccesoCuentaUsuario'}), dict(name='div', attrs={'id':'CajaAccesoCuentaUsuario'}),
dict(name='div', attrs={'id':'CintilloComentario'}), dict(name='div', attrs={'id':'CintilloComentario'}),
@ -107,3 +112,15 @@ class elperiodicodearagon(BasicNewsRecipe):
(re.compile(r'<p> </p>', re.DOTALL|re.IGNORECASE), lambda match: ''), (re.compile(r'<p> </p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'<p id="">', re.DOTALL|re.IGNORECASE), lambda match: '<p>') (re.compile(r'<p id="">', re.DOTALL|re.IGNORECASE), lambda match: '<p>')
] ]
# Para sustituir el video incrustado de YouTube por una imagen
def preprocess_html(self, soup):
for video_yt in soup.findAll('iframe',{'title':'YouTube video player'}):
if video_yt:
video_yt.name = 'img'
fuente = video_yt['src']
fuente2 = fuente.replace('http://www.youtube.com/embed/','http://img.youtube.com/vi/')
video_yt['src'] = fuente2 + '/0.jpg'
return soup

View File

@ -0,0 +1,67 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2011, Adrian Gunawan <agunawan at adrnalin.com>'
__author__ = 'Adrian Gunawan'
__version__ = 'v1.0'
__date__ = '02 February 2011'
'''
http://www.thejakartapost.com/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class JakartaPost(BasicNewsRecipe):
title = u'Jakarta Post'
masthead_url = 'http://www.thejakartapost.com/images/jakartapost_logo.jpg'
cover_url = 'http://www.thejakartapost.com/images/jakartapost_logo.jpg'
__author__ = u'Adrian Gunawan'
description = u'Indonesian Newspaper in English from Jakarta Post Online Edition'
category = 'breaking news, national, business, international, Indonesia'
language = 'en_ID'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
no_javascript = True
remove_empty_feeds = True
timefmt = ' [%A, %d %B, %Y]'
encoding = 'utf-8'
keep_only_tags = [dict(name='div', attrs ={'id':'news-main'})]
extra_css = '''
h1{font-family:Georgia,"Times New Roman",Times,serif; font-weight:bold; font-size:large;}
.cT-storyDetails{font-family:Arial,Helvetica,sans-serif; color:#666666;font-size:x-small;}
.articleBody{font-family:Arial,Helvetica,sans-serif; color:black;font-size:small;}
.cT-imageLandscape{font-family:Arial,Helvetica,sans-serif; color:#333333 ;font-size:x-small;}
.source{font-family:Arial,Helvetica,sans-serif; color:#333333 ;font-size:xx-small;}
#content{font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
.pageprint{font-family:Arial,Helvetica,sans-serif;font-size:small;}
#bylineDetails{font-family:Arial,Helvetica,sans-serif; color:#666666;font-size:x-small;}
.featurePic-wide{font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
#idfeaturepic{font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
h3{font-family:Georgia,"Times New Roman",Times,serif; font-size:small;}
h2{font-family:Georgia,"Times New Roman",Times,serif; font-size:small;}
h4{font-family:Georgia,"Times New Roman",Times,serif; font-size:small;}
h5{font-family:Georgia,"Times New Roman",Times,serif; font-size:small;}
body{font-family:Arial,Helvetica,sans-serif; font-size:x-small;}
'''
remove_tags = [
dict(name='div', attrs ={'class':['text-size']}),
]
feeds = [
(u'Breaking News', u'http://www.thejakartapost.com/breaking/feed'),
(u'National', u'http://www.thejakartapost.com/channel/national/feed'),
(u'Archipelago', u'http://www.thejakartapost.com/channel/archipelago/feed'),
(u'Business', u'http://www.thejakartapost.com/channel/business/feed'),
(u'Jakarta', u'http://www.thejakartapost.com/channel/jakarta/feed'),
(u'World', u'http://www.thejakartapost.com/channel/world/feed'),
(u'Sports', u'http://www.thejakartapost.com/channel/sports/feed'),
]

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2011, Adrian Gunawan <agunawan at adrnalin.com>'
__author__ = 'Adrian Gunawan'
__version__ = 'v1.0'
__date__ = '02 February 2011'
'''
http://www.kompas.com/
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class Kompas(BasicNewsRecipe):
title = u'Kompas'
masthead_url = 'http://stat.k.kidsklik.com/data/2k10/kompascom2011/images/logo_kompas.png'
cover_url = 'http://stat.k.kidsklik.com/data/2k10/kompascom2011/images/logo_kompas.png'
__author__ = u'Adrian Gunawan'
description = u'Indonesian News from Kompas Online Edition'
category = 'local news, international, business, Indonesia'
language = 'id'
oldest_article = 5
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
no_javascript = True
remove_empty_feeds = True
timefmt = ' [%A, %d %B, %Y]'
encoding = 'utf-8'
keep_only_tags = [dict(name='div', attrs ={'class':'content_kiri_detail'})]
extra_css = '''
h1{font-family:Georgia,"Times New Roman",Times,serif; font-weight:bold; font-size:large;}
.cT-storyDetails{font-family:Arial,Helvetica,sans-serif; color:#666666;font-size:x-small;}
.articleBody{font-family:Arial,Helvetica,sans-serif; color:black;font-size:small;}
.cT-imageLandscape{font-family:Arial,Helvetica,sans-serif; color:#333333 ;font-size:x-small;}
.source{font-family:Arial,Helvetica,sans-serif; color:#333333 ;font-size:xx-small;}
#content{font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
.pageprint{font-family:Arial,Helvetica,sans-serif;font-size:small;}
#bylineDetails{font-family:Arial,Helvetica,sans-serif; color:#666666;font-size:x-small;}
.featurePic-wide{font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
#idfeaturepic{font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
h3{font-family:Georgia,"Times New Roman",Times,serif; font-size:small;}
h2{font-family:Georgia,"Times New Roman",Times,serif; font-size:small;}
h4{font-family:Georgia,"Times New Roman",Times,serif; font-size:small;}
h5{font-family:Georgia,"Times New Roman",Times,serif; font-size:small;}
body{font-family:Arial,Helvetica,sans-serif; font-size:x-small;}
'''
remove_tags = [
dict(name='div', attrs ={'class':['c_biru_kompas2011', 'c_abu01_kompas2011', 'c_abu_01_kompas2011', 'right', 'clearit']}),
dict(name='div', attrs ={'id':['comment_list', 'comment_paging', 'share']}),
dict(name='form'),
dict(name='ul'),
]
preprocess_regexps = [
(re.compile(r'<!--TERKAIT -->.*<!--TERKAIT END -->', re.DOTALL|re.IGNORECASE),lambda match: ''),
(re.compile(r'<strong>Sent Using.*</body>', re.DOTALL|re.IGNORECASE),lambda match: ''),
(re.compile(r'<strong>Kirim Komentar Anda</strong>', re.DOTALL|re.IGNORECASE),lambda match: ''),
(re.compile(r'<a[^>]*>Kembali ke Index Topik Pilihan</a>', re.DOTALL|re.IGNORECASE),lambda match: ''),
]
feeds = [
(u'Nasional', u'http://www.kompas.com/getrss/nasional'),
(u'Regional', u'http://www.kompas.com/getrss/regional'),
(u'Internasional', u'http://www.kompas.com/getrss/internasional'),
(u'Megapolitan', u'http://www.kompas.com/getrss/megapolitan'),
(u'Bisnis Keuangan', u'http://www.kompas.com/getrss/bisniskeuangan'),
(u'Kesehatan', u'http://www.kompas.com/getrss/kesehatan'),
(u'Olahraga', u'http://www.kompas.com/getrss/olahraga'),
]

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
lalibre.be lalibre.be
''' '''
@ -16,18 +16,18 @@ class LaLibre(BasicNewsRecipe):
publisher = u'lalibre.be' publisher = u'lalibre.be'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
language = 'fr_BE' language = 'fr'
masthead_url = 'http://www.lalibre.be/img/logoLaLibre.gif'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
timefmt = ' [%d %b %Y]' timefmt = ' [%d %b %Y]'
keep_only_tags = [ remove_tags_before = dict(name = 'div', attrs = {'class': 'extraMainContent'})
dict(name = 'div', attrs = {'id': 'articleHat'}) remove_tags_after = dict(name = 'div', attrs = {'id': 'articleText'})
,dict(name = 'p', attrs = {'id': 'publicationDate'})
,dict(name = 'div', attrs = {'id': 'articleText'}) remove_tags = [dict(name = 'div', attrs = {'id': 'strongArticleLinks'})]
]
feeds = [ feeds = [
(u'L\'actu' , u'http://www.lalibre.be/rss/?section=10' ) (u'L\'actu' , u'http://www.lalibre.be/rss/?section=10' )
@ -38,6 +38,13 @@ class LaLibre(BasicNewsRecipe):
,(u'Societe' , u'http://www.lalibre.be/rss/?section=12' ) ,(u'Societe' , u'http://www.lalibre.be/rss/?section=12' )
] ]
def preprocess_html(self, soup):
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup
def get_cover_url(self): def get_cover_url(self):
cover_url = strftime('http://pdf-online.lalibre.be/pdfonline/image/%Y%m%d/llb_%Y%m%d_nam_libre_001.pdf.L.jpg') cover_url = strftime('http://pdf-online.lalibre.be/pdfonline/image/%Y%m%d/llb_%Y%m%d_nam_libre_001.pdf.L.jpg')
return cover_url return cover_url

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
lameuse.be lameuse.be
''' '''
@ -16,8 +16,8 @@ class LaMeuse(BasicNewsRecipe):
publisher = u'lameuse.be' publisher = u'lameuse.be'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
encoding = 'utf8' language = 'fr'
language = 'fr_BE' masthead_url = 'http://www.lameuse.be/images/SPV3/logo_header_LM.gif'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True
@ -32,6 +32,11 @@ class LaMeuse(BasicNewsRecipe):
dict(name = 'div', attrs = {'class': 'sb-group'}) dict(name = 'div', attrs = {'class': 'sb-group'})
,dict(name = 'div', attrs = {'id': 'share'}) ,dict(name = 'div', attrs = {'id': 'share'})
,dict(name = 'div', attrs = {'id': 'commentaires'}) ,dict(name = 'div', attrs = {'id': 'commentaires'})
,dict(name = 'ul', attrs = {'class': 'right liensutiles'})
,dict(name = 'ul', attrs = {'class': 'bas liensutiles'})
,dict(name = 'p', attrs = {'class': 'ariane'})
,dict(name = 'div', attrs = {'class': 'inner-bloc'})
,dict(name = 'div', attrs = {'class': 'block-01'})
] ]
feeds = [ feeds = [

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
lavenir.net lavenir.net
''' '''
@ -15,8 +15,7 @@ class LAvenir(BasicNewsRecipe):
publisher = u'lavenir.net' publisher = u'lavenir.net'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
encoding = 'utf8' language = 'fr'
language = 'fr_BE'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True
@ -35,6 +34,13 @@ class LAvenir(BasicNewsRecipe):
,(u'Societe' , u'http://www.lavenir.net/rss.aspx?foto=1&intro=1&section=info&info=12e1a2f4-7e03-4cf1-afec-016869072317' ) ,(u'Societe' , u'http://www.lavenir.net/rss.aspx?foto=1&intro=1&section=info&info=12e1a2f4-7e03-4cf1-afec-016869072317' )
] ]
def preprocess_html(self, soup):
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup
def get_cover_url(self): def get_cover_url(self):
cover_url = 'http://www.lavenir.net/extra/Static/journal/Pdf/1/UNE_Nationale.PDF' cover_url = 'http://www.lavenir.net/extra/Static/journal/Pdf/1/UNE_Nationale.PDF'
return cover_url return cover_url

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
lesoir.be lesoir.be
''' '''
@ -16,7 +16,8 @@ class LeSoirBe(BasicNewsRecipe):
publisher = u'lesoir.be' publisher = u'lesoir.be'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
language = 'fr_BE' language = 'fr'
masthead_url = 'http://pdf.lesoir.be/pdf/images/SOIR//logo.gif'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True

View File

@ -0,0 +1,61 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
'''
njuz.net
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class NjuzNet(BasicNewsRecipe):
title = 'Njuz.net'
__author__ = 'Darko Miletic'
description = 'Iscasene vesti iz Srbije, regiona i sveta'
publisher = 'njuz.net'
category = 'news, politics, humor, Serbia'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'utf8'
language = 'sr'
publication_type = 'newsportal'
masthead_url = 'http://www.njuz.net/njuznet.jpg'
extra_css = """
@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
body{font-family: serif1, serif}
.articledescription{font-family: serif1, serif}
.wp-caption-text{font-size: x-small}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [
dict(attrs={'id':'entryMeta'})
,dict(attrs={'class':'post'})
]
remove_tags = [
dict(name=['embed','link','base','iframe','object','meta','fb:like'])
,dict(name='div', attrs={'id':'tagsandcats'})
]
remove_tags_after= dict(name='div', attrs={'id':'tagsandcats'})
remove_attributes= ['lang']
feeds = [(u'Clanci', u'http://www.njuz.net/feed/')]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup

View File

@ -182,6 +182,10 @@ class NYTimes(BasicNewsRecipe):
'mediaOverlay slideshow', 'mediaOverlay slideshow',
'headlinesOnly multiline flush', 'headlinesOnly multiline flush',
'wideThumb', 'wideThumb',
'video', #added 02-11-2011
'videoHeader',#added 02-11-2011
'articleInlineVideoHolder', #added 02-11-2011
'assetCompanionAd',
re.compile('^subNavigation'), re.compile('^subNavigation'),
re.compile('^leaderboard'), re.compile('^leaderboard'),
re.compile('^module'), re.compile('^module'),
@ -664,7 +668,7 @@ class NYTimes(BasicNewsRecipe):
try: try:
#remove "Related content" bar #remove "Related content" bar
runAroundsFound = soup.findAll('div',{'class':['articleInline runaroundLeft','articleInline doubleRule runaroundLeft','articleInline runaroundLeft firstArticleInline']}) runAroundsFound = soup.findAll('div',{'class':['articleInline runaroundLeft','articleInline doubleRule runaroundLeft','articleInline runaroundLeft firstArticleInline','articleInline runaroundLeft ']})
if runAroundsFound: if runAroundsFound:
for runAround in runAroundsFound: for runAround in runAroundsFound:
#find all section headers #find all section headers
@ -672,6 +676,12 @@ class NYTimes(BasicNewsRecipe):
if hlines: if hlines:
for hline in hlines: for hline in hlines:
hline.extract() hline.extract()
#find all section headers
hlines = runAround.findAll('h6')
if hlines:
for hline in hlines:
hline.extract()
except: except:
self.log("Error removing related content bar") self.log("Error removing related content bar")

View File

@ -27,7 +27,7 @@ class TazDigiabo(BasicNewsRecipe):
} }
def build_index(self): def build_index(self):
domain = "http://www.taz.de" domain = "http://dl.taz.de"
url = domain + "/epub/" url = domain + "/epub/"

View File

@ -0,0 +1,33 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
'''
blogs.tedneward.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class InteroperabilityHappens(BasicNewsRecipe):
title = 'Interoperability Happens'
__author__ = 'Darko Miletic'
description = 'Tech blog by Ted Neward'
oldest_article = 15
max_articles_per_feed = 100
language = 'en'
encoding = 'utf-8'
no_stylesheets = True
use_embedded_content = True
publication_type = 'blog'
extra_css = """
body{font-family: Verdana,Arial,Helvetica,sans-serif}
"""
conversion_options = {
'comment' : description
, 'tags' : 'blog, technology, microsoft, programming, C#, Java'
, 'publisher': 'Ted Neward'
, 'language' : language
}
feeds = [(u'Posts', u'http://blogs.tedneward.com/SyndicationService.asmx/GetRss')]

View File

@ -0,0 +1,104 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '4 February 2011, desUBIKado'
__author__ = 'desUBIKado'
__version__ = 'v0.05'
__date__ = '9, February 2011'
'''
http://www.weblogssl.com/
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class weblogssl(BasicNewsRecipe):
__author__ = 'desUBIKado'
description = u'Weblogs colectivos dedicados a seguir la actualidad sobre tecnologia, entretenimiento, estilos de vida, motor, deportes y economia.'
title = u'Weblogs SL (Xataka, Genbeta, VidaExtra, Blog de Cine y otros)'
publisher = 'Weblogs SL'
category = 'Gadgets, Tech news, Product reviews, mobiles, science, cinema, entertainment, culture, tv, food, recipes, life style, motor, F1, sports, economy'
language = 'es'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 1.5
max_articles_per_feed = 100
encoding = 'utf-8'
use_embedded_content = False
remove_empty_feeds = True
remove_javascript = True
no_stylesheets = True
# Si no se quiere recuperar todos los blogs se puede suprimir la descarga del que se desee poniendo
# un caracter # por delante, es decir, # (u'Applesfera', u'http://feeds.weblogssl.com/applesfera'),
# haría que no se descargase Applesfera. OJO: El último feed no debe llevar la coma al final
feeds = [
(u'Xataka', u'http://feeds.weblogssl.com/xataka2'),
(u'Xataka M\xf3vil', u'http://feeds.weblogssl.com/xatakamovil'),
(u'Xataka Android', u'http://feeds.weblogssl.com/xatakandroid'),
(u'Xataka Foto', u'http://feeds.weblogssl.com/xatakafoto'),
(u'Xataka ON', u'http://feeds.weblogssl.com/xatakaon'),
(u'Xataka Ciencia', u'http://feeds.weblogssl.com/xatakaciencia'),
(u'Genbeta', u'http://feeds.weblogssl.com/genbeta'),
(u'Applesfera', u'http://feeds.weblogssl.com/applesfera'),
(u'Vida Extra', u'http://feeds.weblogssl.com/vidaextra'),
(u'Naci\xf3n Red', u'http://feeds.weblogssl.com/nacionred'),
(u'Blog de Cine', u'http://feeds.weblogssl.com/blogdecine'),
(u'Vaya tele', u'http://feeds.weblogssl.com/vayatele2'),
(u'Hipers\xf3nica', u'http://feeds.weblogssl.com/hipersonica'),
(u'Diario del viajero', u'http://feeds.weblogssl.com/diariodelviajero'),
(u'Papel en blanco', u'http://feeds.weblogssl.com/papelenblanco'),
(u'Pop rosa', u'http://feeds.weblogssl.com/poprosa'),
(u'Zona FandoM', u'http://feeds.weblogssl.com/zonafandom'),
(u'Fandemia', u'http://feeds.weblogssl.com/fandemia'),
(u'Noctamina', u'http://feeds.weblogssl.com/noctamina'),
(u'Tendencias', u'http://feeds.weblogssl.com/trendencias'),
(u'Beb\xe9s y m\xe1s', u'http://feeds.weblogssl.com/bebesymas'),
(u'Directo al paladar', u'http://feeds.weblogssl.com/directoalpaladar'),
(u'Compradicci\xf3n', u'http://feeds.weblogssl.com/compradiccion'),
(u'Decoesfera', u'http://feeds.weblogssl.com/decoesfera'),
(u'Embelezzia', u'http://feeds.weblogssl.com/embelezzia'),
(u'Vit\xf3nica', u'http://feeds.weblogssl.com/vitonica'),
(u'Ambiente G', u'http://feeds.weblogssl.com/ambienteg'),
(u'Arrebatadora', u'http://feeds.weblogssl.com/arrebatadora'),
(u'Mensencia', u'http://feeds.weblogssl.com/mensencia'),
(u'Peques y m\xe1s', u'http://feeds.weblogssl.com/pequesymas'),
(u'Motorpasi\xf3n', u'http://feeds.weblogssl.com/motorpasion'),
(u'Motorpasi\xf3n F1', u'http://feeds.weblogssl.com/motorpasionf1'),
(u'Motorpasi\xf3n Moto', u'http://feeds.weblogssl.com/motorpasionmoto'),
(u'Notas de futbol', u'http://feeds.weblogssl.com/notasdefutbol'),
(u'Fuera de l\xedmites', u'http://feeds.weblogssl.com/fueradelimites'),
(u'Salir a ganar', u'http://feeds.weblogssl.com/saliraganar'),
(u'El blog salm\xf3n', u'http://feeds.weblogssl.com/elblogsalmon2'),
(u'Pymes y aut\xf3nomos', u'http://feeds.weblogssl.com/pymesyautonomos'),
(u'Tecnolog\xeda Pyme', u'http://feeds.weblogssl.com/tecnologiapyme'),
(u'Ahorro diario', u'http://feeds.weblogssl.com/ahorrodiario')
]
keep_only_tags = [dict(name='div', attrs={'id':'infoblock'}),
dict(name='div', attrs={'class':'post'}),
dict(name='div', attrs={'id':'blog-comments'})
]
remove_tags = [dict(name='div', attrs={'id':'comment-nav'})]
def print_version(self, url):
return url.replace('http://www.', 'http://m.')
preprocess_regexps = [
# Para poner una linea en blanco entre un comentario y el siguiente
(re.compile(r'<li id="c', re.DOTALL|re.IGNORECASE), lambda match: '<br><br><li id="c')
]
# Para sustituir el video incrustado de YouTube por una imagen
def preprocess_html(self, soup):
for video_yt in soup.findAll('iframe',{'title':'YouTube video player'}):
if video_yt:
video_yt.name = 'img'
fuente = video_yt['src']
fuente2 = fuente.replace('http://www.youtube.com/embed/','http://img.youtube.com/vi/')
fuente3 = fuente2.replace('?rel=0','')
video_yt['src'] = fuente3 + '/0.jpg'
return soup

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.7.44' __version__ = '0.7.45'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re

View File

@ -497,7 +497,7 @@ from calibre.devices.binatone.driver import README
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
from calibre.devices.edge.driver import EDGE from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \ from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR, \ GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR, \
@ -507,7 +507,7 @@ from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK from calibre.devices.bambook.driver import BAMBOOK
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \ from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
LibraryThing KentDistrictLibrary
from calibre.ebooks.metadata.douban import DoubanBooks from calibre.ebooks.metadata.douban import DoubanBooks
from calibre.ebooks.metadata.nicebooks import NiceBooks, NiceBooksCovers from calibre.ebooks.metadata.nicebooks import NiceBooks, NiceBooksCovers
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \ from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
@ -517,7 +517,7 @@ from calibre.ebooks.epub.fix.unmanifested import Unmanifested
from calibre.ebooks.epub.fix.epubcheck import Epubcheck from calibre.ebooks.epub.fix.epubcheck import Epubcheck
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon, plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
LibraryThing, DoubanBooks, NiceBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested, KentDistrictLibrary, DoubanBooks, NiceBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
Epubcheck, OpenLibraryCovers, LibraryThingCovers, DoubanCovers, Epubcheck, OpenLibraryCovers, LibraryThingCovers, DoubanCovers,
NiceBooksCovers] NiceBooksCovers]
plugins += [ plugins += [
@ -605,9 +605,8 @@ plugins += [
ELONEX, ELONEX,
TECLAST_K3, TECLAST_K3,
NEWSMY, NEWSMY,
PICO, SUNSTECH_EB700, ARCHOS7O, PICO, SUNSTECH_EB700, ARCHOS7O, SOVOS, STASH,
IPAPYRUS, IPAPYRUS,
SOVOS,
EDGE, EDGE,
SNE, SNE,
ALEX, ALEX,
@ -791,6 +790,17 @@ class Toolbar(PreferencesPlugin):
description = _('Customize the toolbars and context menus, changing which' description = _('Customize the toolbars and context menus, changing which'
' actions are available in each') ' actions are available in each')
class Search(PreferencesPlugin):
name = 'Search'
icon = I('search.png')
gui_name = _('Customize searching')
category = 'Interface'
gui_category = _('Interface')
category_order = 1
name_order = 5
config_widget = 'calibre.gui2.preferences.search'
description = _('Customize the way searching for books works in calibre')
class InputOptions(PreferencesPlugin): class InputOptions(PreferencesPlugin):
name = 'Input Options' name = 'Input Options'
icon = I('arrow-down.png') icon = I('arrow-down.png')
@ -941,7 +951,7 @@ class Misc(PreferencesPlugin):
config_widget = 'calibre.gui2.preferences.misc' config_widget = 'calibre.gui2.preferences.misc'
description = _('Miscellaneous advanced configuration') description = _('Miscellaneous advanced configuration')
plugins += [LookAndFeel, Behavior, Columns, Toolbar, InputOptions, plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard, CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions] Email, Server, Plugins, Tweaks, Misc, TemplateFunctions]

View File

@ -62,6 +62,12 @@ class ANDROID(USBMS):
# Archos # Archos
0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216], 0x1422 : [0x0216]}, 0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216], 0x1422 : [0x0216]},
# Huawei
0x45e : { 0x00e1 : [0x007], },
# T-Mobile
0x0408 : { 0x03ba : [0x0109], },
} }
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books'] EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to ' EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
@ -71,12 +77,13 @@ class ANDROID(USBMS):
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS', 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
'TELECHIP'] 'TELECHIP', 'HUAWEI', 'T-MOBILE', ]
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H'] 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT'] 'A70S', 'A101IT']

View File

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2011, John Schember <john at nachtimwald.com>'
__docformat__ = 'restructuredtext en'
'''
Generates and writes an APNX page mapping file.
'''
import struct
import uuid
from calibre.ebooks.pdb.header import PdbHeaderReader
class APNXBuilder(object):
'''
2300 characters of uncompressed text per page. This is
not meant to map 1 to 1 to a print book but to be a
close enough measure.
A test book was chosen and the characters were counted
on one page. This number was round to 2240 then 60
characters of markup were added to the total giving
2300.
Uncompressed text length is used because it's easily
accessible in MOBI files (part of the header). Also,
It's faster to work off of the length then to
decompress and parse the actual text.
A better but much more resource intensive and slower
method to calculate the page length would be to parse
the uncompressed text. For each paragraph we would
want to find how many lines it would occupy in a paper
back book. 70 characters per line and 32 lines per page.
So divide the number of characters (minus markup) in
each paragraph by 70. If there are less than 70
characters in the paragraph then it is 1 line. Then,
count every 32 lines and mark that location as a page.
'''
def write_apnx(self, mobi_file_path, apnx_path):
with open(mobi_file_path, 'rb') as mf:
phead = PdbHeaderReader(mf)
r0 = phead.section_data(0)
text_length = struct.unpack('>I', r0[4:8])[0]
pages = self.get_pages(text_length)
apnx = self.generate_apnx(pages)
with open(apnx_path, 'wb') as apnxf:
apnxf.write(apnx)
def generate_apnx(self, pages):
apnx = ''
content_vals = {
'guid': str(uuid.uuid4()).replace('-', '')[:8],
'isbn': '',
}
content_header = '{"contentGuid":"%(guid)s","asin":"%(isbn)s","cdeType":"EBOK","fileRevisionId":"1"}' % content_vals
page_header = '{"asin":"%(isbn)s","pageMap":"(1,a,1)"}' % content_vals
apnx += struct.pack('>I', 65537)
apnx += struct.pack('>I', 12 + len(content_header))
apnx += struct.pack('>I', len(content_header))
apnx += content_header
apnx += struct.pack('>H', 1)
apnx += struct.pack('>H', len(page_header))
apnx += struct.pack('>H', len(pages))
apnx += struct.pack('>H', 32)
apnx += page_header
# write page values to apnx
for page in pages:
apnx += struct.pack('>L', page)
return apnx
def get_pages(self, text_length):
pages = []
count = 0
while count < text_length:
pages.append(count)
count += 2300
return pages

View File

@ -0,0 +1,315 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
import os
from cStringIO import StringIO
from struct import unpack
class Bookmark(): # {{{
'''
A simple class fetching bookmark data
Kindle-specific
'''
def __init__(self, path, id, book_format, bookmark_extension):
self.book_format = book_format
self.bookmark_extension = bookmark_extension
self.book_length = 0
self.id = id
self.last_read = 0
self.last_read_location = 0
self.path = path
self.timestamp = 0
self.user_notes = None
self.get_bookmark_data()
self.get_book_length()
try:
self.percent_read = min(float(100*self.last_read / self.book_length),100)
except:
self.percent_read = 0
def record(self, n):
from calibre.ebooks.metadata.mobi import StreamSlicer
if n >= self.nrecs:
raise ValueError('non-existent record %r' % n)
offoff = 78 + (8 * n)
start, = unpack('>I', self.data[offoff + 0:offoff + 4])
stop = None
if n < (self.nrecs - 1):
stop, = unpack('>I', self.data[offoff + 8:offoff + 12])
return StreamSlicer(self.stream, start, stop)
def get_bookmark_data(self):
''' Return the timestamp and last_read_location '''
from calibre.ebooks.metadata.mobi import StreamSlicer
user_notes = {}
if self.bookmark_extension == 'mbp':
MAGIC_MOBI_CONSTANT = 150
with open(self.path,'rb') as f:
stream = StringIO(f.read())
data = StreamSlicer(stream)
self.timestamp, = unpack('>I', data[0x24:0x28])
bpar_offset, = unpack('>I', data[0x4e:0x52])
lrlo = bpar_offset + 0x0c
self.last_read = int(unpack('>I', data[lrlo:lrlo+4])[0])
self.last_read_location = self.last_read/MAGIC_MOBI_CONSTANT + 1
entries, = unpack('>I', data[0x4a:0x4e])
# Store the annotations/locations
bpl = bpar_offset + 4
bpar_len, = unpack('>I', data[bpl:bpl+4])
bpar_len += 8
#print "bpar_len: 0x%x" % bpar_len
eo = bpar_offset + bpar_len
# Walk bookmark entries
#print " --- %s --- " % self.path
current_entry = 1
sig = data[eo:eo+4]
previous_block = None
while sig == 'DATA':
text = None
entry_type = None
rec_len, = unpack('>I', data[eo+4:eo+8])
if rec_len == 0:
current_block = "empty_data"
elif data[eo+8:eo+12] == "EBAR":
current_block = "data_header"
#entry_type = "data_header"
location, = unpack('>I', data[eo+0x34:eo+0x38])
#print "data_header location: %d" % location
else:
current_block = "text_block"
if previous_block == 'empty_data':
entry_type = 'Note'
elif previous_block == 'data_header':
entry_type = 'Highlight'
text = data[eo+8:eo+8+rec_len].decode('utf-16-be')
if entry_type:
displayed_location = location/MAGIC_MOBI_CONSTANT + 1
user_notes[location] = dict(id=self.id,
displayed_location=displayed_location,
type=entry_type,
text=text)
eo += rec_len + 8
current_entry += 1
previous_block = current_block
sig = data[eo:eo+4]
while sig == 'BKMK':
# Fix start location for Highlights using BKMK data
end_loc, = unpack('>I', data[eo+0x10:eo+0x14])
if end_loc in user_notes and \
(user_notes[end_loc]['type'] == 'Highlight' or \
user_notes[end_loc]['type'] == 'Note'):
# Switch location to start (0x08:0x0c)
start, = unpack('>I', data[eo+8:eo+12])
user_notes[start] = user_notes[end_loc]
'''
print " %s: swapping 0x%x (%d) to 0x%x (%d)" % (user_notes[end_loc]['type'],
end_loc,
end_loc/MAGIC_MOBI_CONSTANT + 1,
start,
start//MAGIC_MOBI_CONSTANT + 1)
'''
user_notes[start]['displayed_location'] = start/MAGIC_MOBI_CONSTANT + 1
user_notes.pop(end_loc)
else:
# If a bookmark coincides with a user annotation, the locs could
# be the same - cheat by nudging -1
# Skip bookmark for last_read_location
if end_loc != self.last_read:
# print " adding Bookmark at 0x%x (%d)" % (end_loc, end_loc/MAGIC_MOBI_CONSTANT + 1)
displayed_location = end_loc/MAGIC_MOBI_CONSTANT + 1
user_notes[end_loc - 1] = dict(id=self.id,
displayed_location=displayed_location,
type='Bookmark',
text=None)
rec_len, = unpack('>I', data[eo+4:eo+8])
eo += rec_len + 8
sig = data[eo:eo+4]
elif self.bookmark_extension == 'tan':
from calibre.ebooks.metadata.topaz import get_metadata as get_topaz_metadata
def get_topaz_highlight(displayed_location):
# Parse My Clippings.txt for a matching highlight
# Search looks for book title match, highlight match, and location match
# Author is not matched
# This will find the first instance of a clipping only
book_fs = self.path.replace('.%s' % self.bookmark_extension,'.%s' % self.book_format)
with open(book_fs,'rb') as f2:
stream = StringIO(f2.read())
mi = get_topaz_metadata(stream)
my_clippings = self.path
split = my_clippings.find('documents') + len('documents/')
my_clippings = my_clippings[:split] + "My Clippings.txt"
try:
with open(my_clippings, 'r') as f2:
marker_found = 0
text = ''
search_str1 = '%s' % (mi.title)
search_str2 = '- Highlight Loc. %d' % (displayed_location)
for line in f2:
if marker_found == 0:
if line.startswith(search_str1):
marker_found = 1
elif marker_found == 1:
if line.startswith(search_str2):
marker_found = 2
elif marker_found == 2:
if line.startswith('=========='):
break
text += line.strip()
else:
raise Exception('error')
except:
text = '(Unable to extract highlight text from My Clippings.txt)'
return text
MAGIC_TOPAZ_CONSTANT = 33.33
self.timestamp = os.path.getmtime(self.path)
with open(self.path,'rb') as f:
stream = StringIO(f.read())
data = StreamSlicer(stream)
self.last_read = int(unpack('>I', data[5:9])[0])
self.last_read_location = self.last_read/MAGIC_TOPAZ_CONSTANT + 1
entries, = unpack('>I', data[9:13])
current_entry = 0
e_base = 0x0d
while current_entry < entries:
location, = unpack('>I', data[e_base+2:e_base+6])
text = None
text_len, = unpack('>I', data[e_base+0xA:e_base+0xE])
e_type, = unpack('>B', data[e_base+1])
if e_type == 0:
e_type = 'Bookmark'
elif e_type == 1:
e_type = 'Highlight'
text = get_topaz_highlight(location/MAGIC_TOPAZ_CONSTANT + 1)
elif e_type == 2:
e_type = 'Note'
text = data[e_base+0x10:e_base+0x10+text_len]
else:
e_type = 'Unknown annotation type'
displayed_location = location/MAGIC_TOPAZ_CONSTANT + 1
user_notes[location] = dict(id=self.id,
displayed_location=displayed_location,
type=e_type,
text=text)
if text_len == 0xFFFFFFFF:
e_base = e_base + 14
else:
e_base = e_base + 14 + 2 + text_len
current_entry += 1
for location in user_notes:
if location == self.last_read:
user_notes.pop(location)
break
elif self.bookmark_extension == 'pdr':
self.timestamp = os.path.getmtime(self.path)
with open(self.path,'rb') as f:
stream = StringIO(f.read())
data = StreamSlicer(stream)
self.last_read = int(unpack('>I', data[5:9])[0])
entries, = unpack('>I', data[9:13])
current_entry = 0
e_base = 0x0d
self.pdf_page_offset = 0
while current_entry < entries:
'''
location, = unpack('>I', data[e_base+2:e_base+6])
text = None
text_len, = unpack('>I', data[e_base+0xA:e_base+0xE])
e_type, = unpack('>B', data[e_base+1])
if e_type == 0:
e_type = 'Bookmark'
elif e_type == 1:
e_type = 'Highlight'
text = get_topaz_highlight(location/MAGIC_TOPAZ_CONSTANT + 1)
elif e_type == 2:
e_type = 'Note'
text = data[e_base+0x10:e_base+0x10+text_len]
else:
e_type = 'Unknown annotation type'
if self.book_format in ['tpz','azw1']:
displayed_location = location/MAGIC_TOPAZ_CONSTANT + 1
elif self.book_format == 'pdf':
# *** This needs implementation
displayed_location = location
user_notes[location] = dict(id=self.id,
displayed_location=displayed_location,
type=e_type,
text=text)
if text_len == 0xFFFFFFFF:
e_base = e_base + 14
else:
e_base = e_base + 14 + 2 + text_len
current_entry += 1
'''
# Use label as page number
pdf_location, = unpack('>I', data[e_base+1:e_base+5])
label_len, = unpack('>H', data[e_base+5:e_base+7])
location = int(data[e_base+7:e_base+7+label_len])
displayed_location = location
e_type = 'Bookmark'
text = None
user_notes[location] = dict(id=self.id,
displayed_location=displayed_location,
type=e_type,
text=text)
self.pdf_page_offset = pdf_location - location
e_base += (7 + label_len)
current_entry += 1
self.last_read_location = self.last_read - self.pdf_page_offset
else:
print "unsupported bookmark_extension: %s" % self.bookmark_extension
self.user_notes = user_notes
def get_book_length(self):
from calibre.ebooks.metadata.mobi import StreamSlicer
book_fs = self.path.replace('.%s' % self.bookmark_extension,'.%s' % self.book_format)
self.book_length = 0
if self.bookmark_extension == 'mbp':
# Read the book len from the header
try:
with open(book_fs,'rb') as f:
self.stream = StringIO(f.read())
self.data = StreamSlicer(self.stream)
self.nrecs, = unpack('>H', self.data[76:78])
record0 = self.record(0)
self.book_length = int(unpack('>I', record0[0x04:0x08])[0])
except:
pass
elif self.bookmark_extension == 'tan':
# Read bookLength from metadata
from calibre.ebooks.metadata.topaz import MetadataUpdater
try:
with open(book_fs,'rb') as f:
mu = MetadataUpdater(f)
self.book_length = mu.book_length
except:
pass
elif self.bookmark_extension == 'pdr':
from calibre import plugins
try:
self.book_length = plugins['pdfreflow'][0].get_numpages(open(book_fs).read())
except:
pass
else:
print "unsupported bookmark_extension: %s" % self.bookmark_extension
# }}}

View File

@ -7,10 +7,11 @@ __docformat__ = 'restructuredtext en'
''' '''
Device driver for Amazon's Kindle Device driver for Amazon's Kindle
''' '''
import datetime, os, re, sys, json, hashlib
from cStringIO import StringIO
from struct import unpack
import datetime, os, re, sys, json, hashlib
from calibre.devices.kindle.apnx import APNXBuilder
from calibre.devices.kindle.bookmark import Bookmark
from calibre.devices.usbms.driver import USBMS from calibre.devices.usbms.driver import USBMS
''' '''
@ -170,6 +171,8 @@ class KINDLE2(KINDLE):
description = _('Communicate with the Kindle 2/3 eBook reader.') description = _('Communicate with the Kindle 2/3 eBook reader.')
FORMATS = KINDLE.FORMATS + ['pdf'] FORMATS = KINDLE.FORMATS + ['pdf']
DELETE_EXTS = KINDLE.DELETE_EXTS + ['.apnx']
PRODUCT_ID = [0x0002, 0x0004] PRODUCT_ID = [0x0002, 0x0004]
BCD = [0x0100] BCD = [0x0100]
@ -205,6 +208,23 @@ class KINDLE2(KINDLE):
if h in path_map: if h in path_map:
book.device_collections = list(sorted(path_map[h])) book.device_collections = list(sorted(path_map[h]))
def upload_cover(self, path, filename, metadata, filepath):
'''
Hijacking this function to write the apnx file.
'''
if not filepath.lower().endswith('.mobi'):
return
apnx_path = '%s.apnx' % os.path.join(path, filename)
apnx_builder = APNXBuilder()
try:
apnx_builder.write_apnx(filepath, apnx_path)
except:
print 'Failed to generate APNX'
import traceback
traceback.print_exc()
class KINDLE_DX(KINDLE2): class KINDLE_DX(KINDLE2):
name = 'Kindle DX Device Interface' name = 'Kindle DX Device Interface'
@ -214,310 +234,3 @@ class KINDLE_DX(KINDLE2):
PRODUCT_ID = [0x0003] PRODUCT_ID = [0x0003]
BCD = [0x0100] BCD = [0x0100]
class Bookmark(): # {{{
'''
A simple class fetching bookmark data
Kindle-specific
'''
def __init__(self, path, id, book_format, bookmark_extension):
self.book_format = book_format
self.bookmark_extension = bookmark_extension
self.book_length = 0
self.id = id
self.last_read = 0
self.last_read_location = 0
self.path = path
self.timestamp = 0
self.user_notes = None
self.get_bookmark_data()
self.get_book_length()
try:
self.percent_read = min(float(100*self.last_read / self.book_length),100)
except:
self.percent_read = 0
def record(self, n):
from calibre.ebooks.metadata.mobi import StreamSlicer
if n >= self.nrecs:
raise ValueError('non-existent record %r' % n)
offoff = 78 + (8 * n)
start, = unpack('>I', self.data[offoff + 0:offoff + 4])
stop = None
if n < (self.nrecs - 1):
stop, = unpack('>I', self.data[offoff + 8:offoff + 12])
return StreamSlicer(self.stream, start, stop)
def get_bookmark_data(self):
''' Return the timestamp and last_read_location '''
from calibre.ebooks.metadata.mobi import StreamSlicer
user_notes = {}
if self.bookmark_extension == 'mbp':
MAGIC_MOBI_CONSTANT = 150
with open(self.path,'rb') as f:
stream = StringIO(f.read())
data = StreamSlicer(stream)
self.timestamp, = unpack('>I', data[0x24:0x28])
bpar_offset, = unpack('>I', data[0x4e:0x52])
lrlo = bpar_offset + 0x0c
self.last_read = int(unpack('>I', data[lrlo:lrlo+4])[0])
self.last_read_location = self.last_read/MAGIC_MOBI_CONSTANT + 1
entries, = unpack('>I', data[0x4a:0x4e])
# Store the annotations/locations
bpl = bpar_offset + 4
bpar_len, = unpack('>I', data[bpl:bpl+4])
bpar_len += 8
#print "bpar_len: 0x%x" % bpar_len
eo = bpar_offset + bpar_len
# Walk bookmark entries
#print " --- %s --- " % self.path
current_entry = 1
sig = data[eo:eo+4]
previous_block = None
while sig == 'DATA':
text = None
entry_type = None
rec_len, = unpack('>I', data[eo+4:eo+8])
if rec_len == 0:
current_block = "empty_data"
elif data[eo+8:eo+12] == "EBAR":
current_block = "data_header"
#entry_type = "data_header"
location, = unpack('>I', data[eo+0x34:eo+0x38])
#print "data_header location: %d" % location
else:
current_block = "text_block"
if previous_block == 'empty_data':
entry_type = 'Note'
elif previous_block == 'data_header':
entry_type = 'Highlight'
text = data[eo+8:eo+8+rec_len].decode('utf-16-be')
if entry_type:
displayed_location = location/MAGIC_MOBI_CONSTANT + 1
user_notes[location] = dict(id=self.id,
displayed_location=displayed_location,
type=entry_type,
text=text)
eo += rec_len + 8
current_entry += 1
previous_block = current_block
sig = data[eo:eo+4]
while sig == 'BKMK':
# Fix start location for Highlights using BKMK data
end_loc, = unpack('>I', data[eo+0x10:eo+0x14])
if end_loc in user_notes and \
(user_notes[end_loc]['type'] == 'Highlight' or \
user_notes[end_loc]['type'] == 'Note'):
# Switch location to start (0x08:0x0c)
start, = unpack('>I', data[eo+8:eo+12])
user_notes[start] = user_notes[end_loc]
'''
print " %s: swapping 0x%x (%d) to 0x%x (%d)" % (user_notes[end_loc]['type'],
end_loc,
end_loc/MAGIC_MOBI_CONSTANT + 1,
start,
start//MAGIC_MOBI_CONSTANT + 1)
'''
user_notes[start]['displayed_location'] = start/MAGIC_MOBI_CONSTANT + 1
user_notes.pop(end_loc)
else:
# If a bookmark coincides with a user annotation, the locs could
# be the same - cheat by nudging -1
# Skip bookmark for last_read_location
if end_loc != self.last_read:
# print " adding Bookmark at 0x%x (%d)" % (end_loc, end_loc/MAGIC_MOBI_CONSTANT + 1)
displayed_location = end_loc/MAGIC_MOBI_CONSTANT + 1
user_notes[end_loc - 1] = dict(id=self.id,
displayed_location=displayed_location,
type='Bookmark',
text=None)
rec_len, = unpack('>I', data[eo+4:eo+8])
eo += rec_len + 8
sig = data[eo:eo+4]
elif self.bookmark_extension == 'tan':
from calibre.ebooks.metadata.topaz import get_metadata as get_topaz_metadata
def get_topaz_highlight(displayed_location):
# Parse My Clippings.txt for a matching highlight
# Search looks for book title match, highlight match, and location match
# Author is not matched
# This will find the first instance of a clipping only
book_fs = self.path.replace('.%s' % self.bookmark_extension,'.%s' % self.book_format)
with open(book_fs,'rb') as f2:
stream = StringIO(f2.read())
mi = get_topaz_metadata(stream)
my_clippings = self.path
split = my_clippings.find('documents') + len('documents/')
my_clippings = my_clippings[:split] + "My Clippings.txt"
try:
with open(my_clippings, 'r') as f2:
marker_found = 0
text = ''
search_str1 = '%s' % (mi.title)
search_str2 = '- Highlight Loc. %d' % (displayed_location)
for line in f2:
if marker_found == 0:
if line.startswith(search_str1):
marker_found = 1
elif marker_found == 1:
if line.startswith(search_str2):
marker_found = 2
elif marker_found == 2:
if line.startswith('=========='):
break
text += line.strip()
else:
raise Exception('error')
except:
text = '(Unable to extract highlight text from My Clippings.txt)'
return text
MAGIC_TOPAZ_CONSTANT = 33.33
self.timestamp = os.path.getmtime(self.path)
with open(self.path,'rb') as f:
stream = StringIO(f.read())
data = StreamSlicer(stream)
self.last_read = int(unpack('>I', data[5:9])[0])
self.last_read_location = self.last_read/MAGIC_TOPAZ_CONSTANT + 1
entries, = unpack('>I', data[9:13])
current_entry = 0
e_base = 0x0d
while current_entry < entries:
location, = unpack('>I', data[e_base+2:e_base+6])
text = None
text_len, = unpack('>I', data[e_base+0xA:e_base+0xE])
e_type, = unpack('>B', data[e_base+1])
if e_type == 0:
e_type = 'Bookmark'
elif e_type == 1:
e_type = 'Highlight'
text = get_topaz_highlight(location/MAGIC_TOPAZ_CONSTANT + 1)
elif e_type == 2:
e_type = 'Note'
text = data[e_base+0x10:e_base+0x10+text_len]
else:
e_type = 'Unknown annotation type'
displayed_location = location/MAGIC_TOPAZ_CONSTANT + 1
user_notes[location] = dict(id=self.id,
displayed_location=displayed_location,
type=e_type,
text=text)
if text_len == 0xFFFFFFFF:
e_base = e_base + 14
else:
e_base = e_base + 14 + 2 + text_len
current_entry += 1
for location in user_notes:
if location == self.last_read:
user_notes.pop(location)
break
elif self.bookmark_extension == 'pdr':
self.timestamp = os.path.getmtime(self.path)
with open(self.path,'rb') as f:
stream = StringIO(f.read())
data = StreamSlicer(stream)
self.last_read = int(unpack('>I', data[5:9])[0])
entries, = unpack('>I', data[9:13])
current_entry = 0
e_base = 0x0d
self.pdf_page_offset = 0
while current_entry < entries:
'''
location, = unpack('>I', data[e_base+2:e_base+6])
text = None
text_len, = unpack('>I', data[e_base+0xA:e_base+0xE])
e_type, = unpack('>B', data[e_base+1])
if e_type == 0:
e_type = 'Bookmark'
elif e_type == 1:
e_type = 'Highlight'
text = get_topaz_highlight(location/MAGIC_TOPAZ_CONSTANT + 1)
elif e_type == 2:
e_type = 'Note'
text = data[e_base+0x10:e_base+0x10+text_len]
else:
e_type = 'Unknown annotation type'
if self.book_format in ['tpz','azw1']:
displayed_location = location/MAGIC_TOPAZ_CONSTANT + 1
elif self.book_format == 'pdf':
# *** This needs implementation
displayed_location = location
user_notes[location] = dict(id=self.id,
displayed_location=displayed_location,
type=e_type,
text=text)
if text_len == 0xFFFFFFFF:
e_base = e_base + 14
else:
e_base = e_base + 14 + 2 + text_len
current_entry += 1
'''
# Use label as page number
pdf_location, = unpack('>I', data[e_base+1:e_base+5])
label_len, = unpack('>H', data[e_base+5:e_base+7])
location = int(data[e_base+7:e_base+7+label_len])
displayed_location = location
e_type = 'Bookmark'
text = None
user_notes[location] = dict(id=self.id,
displayed_location=displayed_location,
type=e_type,
text=text)
self.pdf_page_offset = pdf_location - location
e_base += (7 + label_len)
current_entry += 1
self.last_read_location = self.last_read - self.pdf_page_offset
else:
print "unsupported bookmark_extension: %s" % self.bookmark_extension
self.user_notes = user_notes
def get_book_length(self):
from calibre.ebooks.metadata.mobi import StreamSlicer
book_fs = self.path.replace('.%s' % self.bookmark_extension,'.%s' % self.book_format)
self.book_length = 0
if self.bookmark_extension == 'mbp':
# Read the book len from the header
try:
with open(book_fs,'rb') as f:
self.stream = StringIO(f.read())
self.data = StreamSlicer(self.stream)
self.nrecs, = unpack('>H', self.data[76:78])
record0 = self.record(0)
self.book_length = int(unpack('>I', record0[0x04:0x08])[0])
except:
pass
elif self.bookmark_extension == 'tan':
# Read bookLength from metadata
from calibre.ebooks.metadata.topaz import MetadataUpdater
try:
with open(book_fs,'rb') as f:
mu = MetadataUpdater(f)
self.book_length = mu.book_length
except:
pass
elif self.bookmark_extension == 'pdr':
from calibre import plugins
try:
self.book_length = plugins['pdfreflow'][0].get_numpages(open(book_fs).read())
except:
pass
else:
print "unsupported bookmark_extension: %s" % self.bookmark_extension
# }}}

View File

@ -76,11 +76,11 @@ class E52(USBMS):
supported_platforms = ['windows', 'linux', 'osx'] supported_platforms = ['windows', 'linux', 'osx']
VENDOR_ID = [0x421] VENDOR_ID = [0x421]
PRODUCT_ID = [0x1CD] PRODUCT_ID = [0x1CD, 0x273]
BCD = [0x100] BCD = [0x100]
FORMATS = ['mobi', 'prc'] FORMATS = ['epub', 'fb2', 'mobi', 'prc', 'txt']
EBOOK_DIR_MAIN = 'eBooks' EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True

View File

@ -92,3 +92,15 @@ class SUNSTECH_EB700(TECLAST_K3):
VENDOR_NAME = 'SUNEB700' VENDOR_NAME = 'SUNEB700'
WINDOWS_MAIN_MEM = 'USB-MSC' WINDOWS_MAIN_MEM = 'USB-MSC'
class STASH(TECLAST_K3):
name = 'Stash device interface'
gui_name = 'Stash'
description = _('Communicate with the Stash W950 reader.')
FORMATS = ['epub', 'fb2', 'lrc', 'pdb', 'html', 'fb2', 'wtxt',
'txt', 'pdf']
VENDOR_NAME = 'STASH'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'W950'

View File

@ -232,16 +232,37 @@ class Device(DeviceConfig, DevicePlugin):
time.sleep(5) time.sleep(5)
drives = {} drives = {}
seen = set()
prod_pat = re.compile(r'PROD_(.+?)&')
dup_prod_id = False
def check_for_dups(pnp_id):
try:
match = prod_pat.search(pnp_id)
if match is not None:
prodid = match.group(1)
if prodid in seen:
return True
else:
seen.add(prodid)
except:
pass
return False
for drive, pnp_id in win_pnp_drives().items(): for drive, pnp_id in win_pnp_drives().items():
if self.windows_match_device(pnp_id, 'WINDOWS_CARD_A_MEM') and \ if self.windows_match_device(pnp_id, 'WINDOWS_CARD_A_MEM') and \
not drives.get('carda', False): not drives.get('carda', False):
drives['carda'] = drive drives['carda'] = drive
dup_prod_id |= check_for_dups(pnp_id)
elif self.windows_match_device(pnp_id, 'WINDOWS_CARD_B_MEM') and \ elif self.windows_match_device(pnp_id, 'WINDOWS_CARD_B_MEM') and \
not drives.get('cardb', False): not drives.get('cardb', False):
drives['cardb'] = drive drives['cardb'] = drive
dup_prod_id |= check_for_dups(pnp_id)
elif self.windows_match_device(pnp_id, 'WINDOWS_MAIN_MEM') and \ elif self.windows_match_device(pnp_id, 'WINDOWS_MAIN_MEM') and \
not drives.get('main', False): not drives.get('main', False):
drives['main'] = drive drives['main'] = drive
dup_prod_id |= check_for_dups(pnp_id)
if 'main' in drives.keys() and 'carda' in drives.keys() and \ if 'main' in drives.keys() and 'carda' in drives.keys() and \
'cardb' in drives.keys(): 'cardb' in drives.keys():
@ -263,7 +284,8 @@ class Device(DeviceConfig, DevicePlugin):
# Sort drives by their PNP drive numbers if the CARD and MAIN # Sort drives by their PNP drive numbers if the CARD and MAIN
# MEM strings are identical # MEM strings are identical
if self.WINDOWS_MAIN_MEM in (self.WINDOWS_CARD_A_MEM, if dup_prod_id or \
self.WINDOWS_MAIN_MEM in (self.WINDOWS_CARD_A_MEM,
self.WINDOWS_CARD_B_MEM) or \ self.WINDOWS_CARD_B_MEM) or \
self.WINDOWS_CARD_A_MEM == self.WINDOWS_CARD_B_MEM: self.WINDOWS_CARD_A_MEM == self.WINDOWS_CARD_B_MEM:
letters = sorted(drives.values(), cmp=drivecmp) letters = sorted(drives.values(), cmp=drivecmp)

View File

@ -11,6 +11,7 @@ from calibre.ebooks.conversion.preprocess import DocAnalysis, Dehyphenator
from calibre.utils.logging import default_log from calibre.utils.logging import default_log
from calibre.utils.wordcount import get_wordcount_obj from calibre.utils.wordcount import get_wordcount_obj
class HeuristicProcessor(object): class HeuristicProcessor(object):
def __init__(self, extra_opts=None, log=None): def __init__(self, extra_opts=None, log=None):
@ -40,6 +41,9 @@ class HeuristicProcessor(object):
def is_pdftohtml(self, src): def is_pdftohtml(self, src):
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000] return '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
def is_abbyy(self, src):
return '<meta name="generator" content="ABBYY FineReader' in src[:1000]
def chapter_head(self, match): def chapter_head(self, match):
from calibre.utils.html2text import html2text from calibre.utils.html2text import html2text
chap = match.group('chap') chap = match.group('chap')
@ -519,6 +523,111 @@ class HeuristicProcessor(object):
return scene_break return scene_break
def abbyy_processor(self, html):
abbyy_line = re.compile('((?P<linestart><p\sstyle="(?P<styles>[^\"]*?);?">)(?P<content>.*?)(?P<lineend></p>)|(?P<image><img[^>]*>))', re.IGNORECASE)
empty_paragraph = '\n<p> </p>\n'
self.in_blockquote = False
self.previous_was_paragraph = False
html = re.sub('</?a[^>]*>', '', html)
def check_paragraph(content):
content = re.sub('\s*</?span[^>]*>\s*', '', content)
if re.match('.*[\"\'.!?:]$', content):
#print "detected this as a paragraph"
return True
else:
return False
def convert_styles(match):
#print "raw styles are: "+match.group('styles')
content = match.group('content')
#print "raw content is: "+match.group('content')
image = match.group('image')
is_paragraph = False
text_align = ''
text_indent = ''
paragraph_before = ''
paragraph_after = ''
blockquote_open = '\n<blockquote>\n'
blockquote_close = '</blockquote>\n'
indented_text = 'text-indent:3%;'
blockquote_open_loop = ''
blockquote_close_loop = ''
debugabby = False
if image:
debugabby = True
if self.in_blockquote:
self.in_blockquote = False
blockquote_close_loop = blockquote_close
self.previous_was_paragraph = False
return blockquote_close_loop+'\n'+image+'\n'
else:
styles = match.group('styles').split(';')
is_paragraph = check_paragraph(content)
#print "styles for this line are: "+str(styles)
split_styles = []
for style in styles:
#print "style is: "+str(style)
newstyle = style.split(':')
#print "newstyle is: "+str(newstyle)
split_styles.append(newstyle)
styles = split_styles
for style, setting in styles:
if style == 'text-align' and setting != 'left':
text_align = style+':'+setting+';'
if style == 'text-indent':
setting = int(re.sub('\s*pt\s*', '', setting))
if 9 < setting < 14:
text_indent = indented_text
else:
text_indent = style+':'+str(setting)+'pt;'
if style == 'padding':
setting = re.sub('pt', '', setting).split(' ')
if int(setting[1]) < 16 and int(setting[3]) < 16:
if self.in_blockquote:
debugabby = True
if is_paragraph:
self.in_blockquote = False
blockquote_close_loop = blockquote_close
if int(setting[3]) > 8 and text_indent == '':
text_indent = indented_text
if int(setting[0]) > 5:
paragraph_before = empty_paragraph
if int(setting[2]) > 5:
paragraph_after = empty_paragraph
elif not self.in_blockquote and self.previous_was_paragraph:
debugabby = True
self.in_blockquote = True
blockquote_open_loop = blockquote_open
if debugabby:
self.log.debug('\n\n******\n')
self.log.debug('padding top is: '+str(setting[0]))
self.log.debug('padding right is:'
+str(setting[1]))
self.log.debug('padding bottom is: ' +
str(setting[2]))
self.log.debug('padding left is: '
+str(setting[3]))
#print "text-align is: "+str(text_align)
#print "\n***\nline is:\n "+str(match.group(0))+'\n'
if debugabby:
#print "this line is a paragraph = "+str(is_paragraph)+", previous line was "+str(self.previous_was_paragraph)
self.log.debug("styles for this line were:", styles)
self.log.debug('newline is:')
self.log.debug(blockquote_open_loop+blockquote_close_loop+
paragraph_before+'<p style="'+text_indent+text_align+
'">'+content+'</p>'+paragraph_after+'\n\n\n\n\n')
#print "is_paragraph is "+str(is_paragraph)+", previous_was_paragraph is "+str(self.previous_was_paragraph)
self.previous_was_paragraph = is_paragraph
#print "previous_was_paragraph is now set to "+str(self.previous_was_paragraph)+"\n\n\n"
return blockquote_open_loop+blockquote_close_loop+paragraph_before+'<p style="'+text_indent+text_align+'">'+content+'</p>'+paragraph_after
html = abbyy_line.sub(convert_styles, html)
return html
def __call__(self, html): def __call__(self, html):
self.log.debug("********* Heuristic processing HTML *********") self.log.debug("********* Heuristic processing HTML *********")
@ -533,6 +642,10 @@ class HeuristicProcessor(object):
self.log.warn("flow is too short, not running heuristics") self.log.warn("flow is too short, not running heuristics")
return html return html
is_abbyy = self.is_abbyy(html)
if is_abbyy:
html = self.abbyy_processor(html)
# Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly # Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly
html = self.arrange_htm_line_endings(html) html = self.arrange_htm_line_endings(html)
#self.dump(html, 'after_arrange_line_endings') #self.dump(html, 'after_arrange_line_endings')

View File

@ -216,8 +216,8 @@ class EPUBOutput(OutputFormatPlugin):
encryption = self.encrypt_fonts(encrypted_fonts, tdir, uuid) encryption = self.encrypt_fonts(encrypted_fonts, tdir, uuid)
from calibre.ebooks.epub import initialize_container from calibre.ebooks.epub import initialize_container
epub = initialize_container(output_path, os.path.basename(opf), with initialize_container(output_path, os.path.basename(opf),
extra_entries=extra_entries) extra_entries=extra_entries) as epub:
epub.add_dir(tdir) epub.add_dir(tdir)
if encryption is not None: if encryption is not None:
epub.writestr('META-INF/encryption.xml', encryption) epub.writestr('META-INF/encryption.xml', encryption)
@ -225,12 +225,13 @@ class EPUBOutput(OutputFormatPlugin):
epub.writestr('META-INF/metadata.xml', epub.writestr('META-INF/metadata.xml',
metadata_xml.encode('utf-8')) metadata_xml.encode('utf-8'))
if opts.extract_to is not None: if opts.extract_to is not None:
from calibre.utils.zipfile import ZipFile
if os.path.exists(opts.extract_to): if os.path.exists(opts.extract_to):
shutil.rmtree(opts.extract_to) shutil.rmtree(opts.extract_to)
os.mkdir(opts.extract_to) os.mkdir(opts.extract_to)
epub.extractall(path=opts.extract_to) with ZipFile(output_path) as zf:
zf.extractall(path=opts.extract_to)
self.log.info('EPUB extracted to', opts.extract_to) self.log.info('EPUB extracted to', opts.extract_to)
epub.close()
def encrypt_fonts(self, uris, tdir, uuid): # {{{ def encrypt_fonts(self, uris, tdir, uuid): # {{{
from binascii import unhexlify from binascii import unhexlify

View File

@ -129,9 +129,6 @@ class Metadata(object):
val = NULL_VALUES.get(field, None) val = NULL_VALUES.get(field, None)
_data[field] = val _data[field] = val
elif field in _data['user_metadata'].iterkeys(): elif field in _data['user_metadata'].iterkeys():
if _data['user_metadata'][field]['datatype'] == 'composite':
_data['user_metadata'][field]['#value#'] = None
else:
_data['user_metadata'][field]['#value#'] = val _data['user_metadata'][field]['#value#'] = val
_data['user_metadata'][field]['#extra#'] = extra _data['user_metadata'][field]['#extra#'] = extra
else: else:

View File

@ -247,30 +247,24 @@ class Amazon(MetadataSource): # {{{
# }}} # }}}
class LibraryThing(MetadataSource): # {{{ class KentDistrictLibrary(MetadataSource): # {{{
name = 'LibraryThing' name = 'Kent District Library'
metadata_type = 'social' metadata_type = 'social'
description = _('Downloads series/covers/rating information from librarything.com') description = _('Downloads series information from ww2.kdl.org')
def fetch(self): def fetch(self):
if not self.isbn or not self.site_customization: if not self.title or not self.book_author:
return return
from calibre.ebooks.metadata.library_thing import get_social_metadata from calibre.ebooks.metadata.kdl import get_series
un, _, pw = self.site_customization.partition(':')
try: try:
self.results = get_social_metadata(self.title, self.book_author, self.results = get_series(self.title, self.book_author)
self.publisher, self.isbn, username=un, password=pw)
except Exception, e: except Exception, e:
import traceback
traceback.print_exc()
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@property
def string_customization_help(self):
ans = _('To use librarything.com you must sign up for a %sfree account%s '
'and enter your username and password separated by a : below.')
return '<p>'+ans%('<a href="http://www.librarything.com">', '</a>')
# }}} # }}}

View File

@ -0,0 +1,79 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re, urllib, urlparse
from calibre.ebooks.metadata.book.base import Metadata
from calibre import browser
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.ebooks.chardet import xml_to_unicode
URL = \
"http://ww2.kdl.org/libcat/WhatsNext.asp?AuthorLastName={0}&AuthorFirstName=&SeriesName=&BookTitle={1}&CategoryID=0&cmdSearch=Search&Search=1&grouping="
_ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033])
def get_series(title, authors):
mi = Metadata(title, authors)
if title and title[0] in _ignore_starts:
title = title[1:]
title = re.sub(r'^(A|The|An)\s+', '', title).strip()
if not title:
return mi
if isinstance(title, unicode):
title = title.encode('utf-8')
title = urllib.quote_plus(title)
author = authors[0].strip()
if not author:
return mi
if ',' in author:
author = author.split(',')[0]
else:
author = author.split()[-1]
url = URL.format(author, title)
br = browser()
raw = br.open(url).read()
if 'see the full results' not in raw:
return mi
raw = xml_to_unicode(raw)[0]
soup = BeautifulSoup(raw)
searcharea = soup.find('div', attrs={'class':'searcharea'})
if searcharea is None:
return mi
ss = searcharea.find('div', attrs={'class':'seriessearch'})
if ss is None:
return mi
a = ss.find('a', href=True)
if a is None:
return mi
href = a['href'].partition('?')[-1]
data = urlparse.parse_qs(href)
series = data.get('SeriesName', [])
if not series:
return mi
series = series[0]
series = re.sub(r' series$', '', series).strip()
if series:
mi.series = series
ns = ss.nextSibling
if ns.contents:
raw = unicode(ns.contents[0])
raw = raw.partition('.')[0].strip()
try:
mi.series_index = int(raw)
except:
pass
return mi
if __name__ == '__main__':
import sys
print get_series(sys.argv[-2], [sys.argv[-1]])

View File

@ -39,6 +39,13 @@ def asfloat(value):
return 0.0 return 0.0
return float(value) return float(value)
def isspace(text):
if not text:
return True
if u'\xa0' in text:
return False
return text.isspace()
class BlockState(object): class BlockState(object):
def __init__(self, body): def __init__(self, body):
self.body = body self.body = body
@ -438,7 +445,7 @@ class MobiMLizer(object):
if elem.text: if elem.text:
if istate.preserve: if istate.preserve:
text = elem.text text = elem.text
elif len(elem) > 0 and elem.text.isspace(): elif len(elem) > 0 and isspace(elem.text):
text = None text = None
else: else:
text = COLLAPSE.sub(' ', elem.text) text = COLLAPSE.sub(' ', elem.text)
@ -481,7 +488,7 @@ class MobiMLizer(object):
if child.tail: if child.tail:
if istate.preserve: if istate.preserve:
tail = child.tail tail = child.tail
elif bstate.para is None and child.tail.isspace(): elif bstate.para is None and isspace(child.tail):
tail = None tail = None
else: else:
tail = COLLAPSE.sub(' ', child.tail) tail = COLLAPSE.sub(' ', child.tail)

View File

@ -103,6 +103,8 @@ class OEBReader(object):
data = self.oeb.container.read(None) data = self.oeb.container.read(None)
data = self.oeb.decode(data) data = self.oeb.decode(data)
data = XMLDECL_RE.sub('', data) data = XMLDECL_RE.sub('', data)
data = data.replace('http://openebook.org/namespaces/oeb-package/1.0',
OPF1_NS)
try: try:
opf = etree.fromstring(data) opf = etree.fromstring(data)
except etree.XMLSyntaxError: except etree.XMLSyntaxError:

View File

@ -70,7 +70,7 @@ class PML_HTMLizer(object):
'c': ('<div style="text-align: center; margin: auto;">', '</div>'), 'c': ('<div style="text-align: center; margin: auto;">', '</div>'),
'r': ('<div style="text-align: right;">', '</div>'), 'r': ('<div style="text-align: right;">', '</div>'),
't': ('<div style="margin-left: 5%;">', '</div>'), 't': ('<div style="margin-left: 5%;">', '</div>'),
'T': ('<div style="margin-left: %s;">', '</div>'), 'T': ('<div style="text-indent: %s;">', '</div>'),
'i': ('<span style="font-style: italic;">', '</span>'), 'i': ('<span style="font-style: italic;">', '</span>'),
'u': ('<span style="text-decoration: underline;">', '</span>'), 'u': ('<span style="text-decoration: underline;">', '</span>'),
'd': ('<span style="text-decoration: line-through;">', '</span>'), 'd': ('<span style="text-decoration: line-through;">', '</span>'),
@ -499,7 +499,13 @@ class PML_HTMLizer(object):
self.toc = [] self.toc = []
self.file_name = file_name self.file_name = file_name
indent_state = {'t': False, 'T': False} # t: Are we in an open \t tag set?
# T: Are we in an open \T?
# st: Did the \t start the line?
# sT: Did the \T start the line?
# et: Did the \t end the line?
indent_state = {'t': False, 'T': False, 'st': False, 'sT': False, 'et': False}
basic_indent = False
adv_indent_val = '' adv_indent_val = ''
# Keep track of the number of empty lines # Keep track of the number of empty lines
# between paragraphs. When we reach a set number # between paragraphs. When we reach a set number
@ -512,8 +518,26 @@ class PML_HTMLizer(object):
for line in pml.splitlines(): for line in pml.splitlines():
parsed = [] parsed = []
empty = True empty = True
basic_indent = indent_state['t'] basic_indent = indent_state['t']
adv_indent = indent_state['T'] indent_state['T'] = False
# Determine if the \t starts the line or if we are
# in an open \t block.
if line.lstrip().startswith('\\t') or basic_indent:
basic_indent = True
indent_state['st'] = True
else:
indent_state['st'] = False
# Determine if the \T starts the line.
if line.lstrip().startswith('\\T'):
indent_state['sT'] = True
else:
indent_state['sT'] = False
# Determine if the \t ends the line.
if line.rstrip().endswith('\\t'):
indent_state['et'] = True
else:
indent_state['et'] = False
# Must use StringIO, cStringIO does not support unicode # Must use StringIO, cStringIO does not support unicode
line = StringIO.StringIO(line) line = StringIO.StringIO(line)
@ -575,13 +599,10 @@ class PML_HTMLizer(object):
empty = False empty = False
text = '<hr width="%s" />' % self.code_value(line) text = '<hr width="%s" />' % self.code_value(line)
elif c == 't': elif c == 't':
indent_state[c] = not indent_state[c] indent_state['t'] = not indent_state['t']
if indent_state[c]:
basic_indent = True
elif c == 'T': elif c == 'T':
# Ensure we only store the value on the first T set for the line. # Ensure we only store the value on the first T set for the line.
if not indent_state['T']: if not indent_state['T']:
adv_indent = True
adv_indent_val = self.code_value(line) adv_indent_val = self.code_value(line)
else: else:
# We detected a T previously on this line. # We detected a T previously on this line.
@ -610,10 +631,23 @@ class PML_HTMLizer(object):
text = self.end_line() text = self.end_line()
parsed.append(text) parsed.append(text)
# Basic indent will be set if the \t starts the line or
# if we are in a continuing \t block.
if basic_indent: if basic_indent:
# if the \t started the line and either it ended the line or the \t
# block is still open use a left margin.
if indent_state['st'] and (indent_state['et'] or indent_state['t']):
parsed.insert(0, self.STATES_TAGS['t'][0]) parsed.insert(0, self.STATES_TAGS['t'][0])
parsed.append(self.STATES_TAGS['t'][1]) parsed.append(self.STATES_TAGS['t'][1])
elif adv_indent: # Use a text indent instead of a margin.
# This handles cases such as:
# \tO\tne upon a time...
else:
parsed.insert(0, self.STATES_TAGS['T'][0] % '5%')
parsed.append(self.STATES_TAGS['T'][1])
# \t will override \T's on the line.
# We only handle \T's that started the line.
elif indent_state['T'] and indent_state['sT']:
parsed.insert(0, self.STATES_TAGS['T'][0] % adv_indent_val) parsed.insert(0, self.STATES_TAGS['T'][0] % adv_indent_val)
parsed.append(self.STATES_TAGS['T'][1]) parsed.append(self.STATES_TAGS['T'][1])
indent_state['T'] = False indent_state['T'] = False

View File

@ -41,7 +41,6 @@ class TextileMLizer(object):
html = re.sub(r'<\s*img[^>]*>', '', html) html = re.sub(r'<\s*img[^>]*>', '', html)
text = html2textile(html) text = html2textile(html)
text = text.replace('%', '')
# Ensure the section ends with at least two new line characters. # Ensure the section ends with at least two new line characters.
# This is to prevent the last paragraph from a section being # This is to prevent the last paragraph from a section being

View File

@ -106,9 +106,13 @@ def _config():
'clicked')) 'clicked'))
c.add_opt('asked_library_thing_password', default=False, c.add_opt('asked_library_thing_password', default=False,
help='Asked library thing password at least once.') help='Asked library thing password at least once.')
c.add_opt('search_as_you_type', default=True, c.add_opt('search_as_you_type', default=False,
help='Start searching as you type. If this is disabled then search will ' help=_('Start searching as you type. If this is disabled then search will '
'only take place when the Enter or Return key is pressed.') 'only take place when the Enter or Return key is pressed.'))
c.add_opt('highlight_search_matches', default=False,
help=_('When searching, show all books with search results '
'highlighted instead of showing only the matches. You can use the '
'N or F3 keys to go to the next match.'))
c.add_opt('save_to_disk_template_history', default=[], c.add_opt('save_to_disk_template_history', default=[],
help='Previously used Save to Disk templates') help='Previously used Save to Disk templates')
c.add_opt('send_to_device_template_history', default=[], c.add_opt('send_to_device_template_history', default=[],
@ -133,14 +137,18 @@ def _config():
help=_('Automatically download the cover, if available')) help=_('Automatically download the cover, if available'))
c.add_opt('enforce_cpu_limit', default=True, c.add_opt('enforce_cpu_limit', default=True,
help=_('Limit max simultaneous jobs to number of CPUs')) help=_('Limit max simultaneous jobs to number of CPUs'))
c.add_opt('tag_browser_hidden_categories', default=set(),
help=_('tag browser categories not to display'))
c.add_opt('gui_layout', choices=['wide', 'narrow'], c.add_opt('gui_layout', choices=['wide', 'narrow'],
help=_('The layout of the user interface'), default='wide') help=_('The layout of the user interface'), default='wide')
c.add_opt('show_avg_rating', default=True, c.add_opt('show_avg_rating', default=True,
help=_('Show the average rating per item indication in the tag browser')) help=_('Show the average rating per item indication in the tag browser'))
c.add_opt('disable_animations', default=False, c.add_opt('disable_animations', default=False,
help=_('Disable UI animations')) help=_('Disable UI animations'))
# This option is no longer used. It remains for compatibility with upgrades
# so the value can be migrated
c.add_opt('tag_browser_hidden_categories', default=set(),
help=_('tag browser categories not to display'))
c.add_opt c.add_opt
return ConfigProxy(c) return ConfigProxy(c)

View File

@ -237,6 +237,7 @@ class ChooseLibraryAction(InterfaceAction):
return return
self.stats.rename(location, newloc) self.stats.rename(location, newloc)
self.build_menus() self.build_menus()
self.gui.iactions['Copy To Library'].build_menus()
def delete_requested(self, name, location): def delete_requested(self, name, location):
loc = location.replace('/', os.sep) loc = location.replace('/', os.sep)
@ -253,6 +254,7 @@ class ChooseLibraryAction(InterfaceAction):
pass pass
self.stats.remove(location) self.stats.remove(location)
self.build_menus() self.build_menus()
self.gui.iactions['Copy To Library'].build_menus()
def backup_status(self, location): def backup_status(self, location):
dirty_text = 'no' dirty_text = 'no'
@ -329,6 +331,7 @@ class ChooseLibraryAction(InterfaceAction):
' libraries.')%loc, show=True) ' libraries.')%loc, show=True)
self.stats.remove(location) self.stats.remove(location)
self.build_menus() self.build_menus()
self.gui.iactions['Copy To Library'].build_menus()
return return
prefs['library_path'] = loc prefs['library_path'] = loc
@ -371,9 +374,20 @@ class ChooseLibraryAction(InterfaceAction):
if not self.change_library_allowed(): if not self.change_library_allowed():
return return
from calibre.gui2.dialogs.choose_library import ChooseLibrary from calibre.gui2.dialogs.choose_library import ChooseLibrary
self.gui.library_view.save_state()
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
c = ChooseLibrary(db, self.gui.library_moved, self.gui) location = self.stats.canonicalize_path(db.library_path)
self.pre_choose_dialog_location = location
c = ChooseLibrary(db, self.choose_library_callback, self.gui)
c.exec_() c.exec_()
self.choose_dialog_library_renamed = getattr(c, 'library_renamed', False)
def choose_library_callback(self, newloc, copy_structure=False):
self.gui.library_moved(newloc, copy_structure=copy_structure)
if getattr(self, 'choose_dialog_library_renamed', False):
self.stats.rename(self.pre_choose_dialog_location, prefs['library_path'])
self.build_menus()
self.gui.iactions['Copy To Library'].build_menus()
def change_library_allowed(self): def change_library_allowed(self):
if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):

View File

@ -28,21 +28,12 @@ class NextMatchAction(InterfaceAction):
self.gui.addAction(self.p_action) self.gui.addAction(self.p_action)
self.p_action.triggered.connect(self.move_backward) self.p_action.triggered.connect(self.move_backward)
def gui_layout_complete(self):
self.gui.search_highlight_only.setVisible(True)
def location_selected(self, loc): def location_selected(self, loc):
self.can_move = loc == 'library' self.can_move = loc == 'library'
try:
self.gui.search_highlight_only.setVisible(self.can_move)
except:
import traceback
traceback.print_exc()
def move_forward(self): def move_forward(self):
if self.can_move is None: if self.can_move is None:
self.can_move = self.gui.current_view() is self.gui.library_view self.can_move = self.gui.current_view() is self.gui.library_view
self.gui.search_highlight_only.setVisible(self.can_move)
if self.can_move: if self.can_move:
self.gui.current_view().move_highlighted_row(forward=True) self.gui.current_view().move_highlighted_row(forward=True)
@ -50,7 +41,6 @@ class NextMatchAction(InterfaceAction):
def move_backward(self): def move_backward(self):
if self.can_move is None: if self.can_move is None:
self.can_move = self.gui.current_view() is self.gui.library_view self.can_move = self.gui.current_view() is self.gui.library_view
self.gui.search_highlight_only.setVisible(self.can_move)
if self.can_move: if self.can_move:
self.gui.current_view().move_highlighted_row(forward=False) self.gui.current_view().move_highlighted_row(forward=False)

View File

@ -33,7 +33,8 @@ class PreferencesAction(InterfaceAction):
x.triggered.connect(self.do_config) x.triggered.connect(self.do_config)
def do_config(self, checked=False, initial_plugin=None): def do_config(self, checked=False, initial_plugin=None,
close_after_initial=False):
if self.gui.job_manager.has_jobs(): if self.gui.job_manager.has_jobs():
d = error_dialog(self.gui, _('Cannot configure'), d = error_dialog(self.gui, _('Cannot configure'),
_('Cannot configure while there are running jobs.')) _('Cannot configure while there are running jobs.'))
@ -44,7 +45,8 @@ class PreferencesAction(InterfaceAction):
_('Cannot configure before calibre is restarted.')) _('Cannot configure before calibre is restarted.'))
d.exec_() d.exec_()
return return
d = Preferences(self.gui, initial_plugin=initial_plugin) d = Preferences(self.gui, initial_plugin=initial_plugin,
close_after_initial=close_after_initial)
d.show() d.show()
d.run_wizard_requested.connect(self.gui.run_wizard, d.run_wizard_requested.connect(self.gui.run_wizard,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)

View File

@ -9,10 +9,9 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QLineEdit, QAbstractListModel, Qt, \ from PyQt4.Qt import QLineEdit, QAbstractListModel, Qt, \
QApplication, QCompleter QApplication, QCompleter
from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key, lower from calibre.utils.icu import sort_key, lower
from calibre.gui2 import NONE from calibre.gui2 import NONE
from calibre.gui2.widgets import EnComboBox from calibre.gui2.widgets import EnComboBox, LineEditECM
class CompleteModel(QAbstractListModel): class CompleteModel(QAbstractListModel):
@ -39,7 +38,7 @@ class CompleteModel(QAbstractListModel):
return NONE return NONE
class MultiCompleteLineEdit(QLineEdit): class MultiCompleteLineEdit(QLineEdit, LineEditECM):
''' '''
A line edit that completes on multiple items separated by a A line edit that completes on multiple items separated by a
separator. Use the :meth:`update_items_cache` to set the list of separator. Use the :meth:`update_items_cache` to set the list of
@ -55,6 +54,8 @@ class MultiCompleteLineEdit(QLineEdit):
self.sep = ',' self.sep = ','
self.space_before_sep = False self.space_before_sep = False
self.add_separator = True
self.original_cursor_pos = None
self._model = CompleteModel(parent=self) self._model = CompleteModel(parent=self)
self._completer = c = QCompleter(self._model, self) self._completer = c = QCompleter(self._model, self)
@ -82,6 +83,9 @@ class MultiCompleteLineEdit(QLineEdit):
def set_space_before_sep(self, space_before): def set_space_before_sep(self, space_before):
self.space_before_sep = space_before self.space_before_sep = space_before
def set_add_separator(self, what):
self.add_separator = bool(what)
# }}} # }}}
def item_entered(self, idx): def item_entered(self, idx):
@ -93,7 +97,7 @@ class MultiCompleteLineEdit(QLineEdit):
def update_completions(self): def update_completions(self):
' Update the list of completions ' ' Update the list of completions '
cpos = self.cursorPosition() self.original_cursor_pos = cpos = self.cursorPosition()
text = unicode(self.text()) text = unicode(self.text())
prefix = text[:cpos] prefix = text[:cpos]
self.current_prefix = prefix self.current_prefix = prefix
@ -103,38 +107,38 @@ class MultiCompleteLineEdit(QLineEdit):
self._completer.setCompletionPrefix(complete_prefix) self._completer.setCompletionPrefix(complete_prefix)
def get_completed_text(self, text): def get_completed_text(self, text):
''' 'Get completed text in before and after parts'
Get completed text from current cursor position and the completion
text
'''
if self.sep is None: if self.sep is None:
return -1, text return text, ''
else: else:
cursor_pos = self.original_cursor_pos
if cursor_pos is None:
cursor_pos = self.cursorPosition() cursor_pos = self.cursorPosition()
before_text = unicode(self.text())[:cursor_pos] self.original_cursor_pos = None
after_text = unicode(self.text())[cursor_pos:] # Split text
prefix_len = len(before_text.split(self.sep)[-1].lstrip()) curtext = unicode(self.text())
if tweaks['completer_append_separator']: before_text = curtext[:cursor_pos]
prefix_len = len(before_text.split(self.sep)[-1].lstrip()) after_text = curtext[cursor_pos:].rstrip()
completed_text = before_text[:cursor_pos - prefix_len] + text + self.sep + ' ' + after_text # Remove the completion prefix from the before text
prefix_len = prefix_len - len(self.sep) - 1 before_text = self.sep.join(before_text.split(self.sep)[:-1]).rstrip()
if prefix_len < 0: if before_text:
prefix_len = 0 # Add the separator to the end of before_text
if self.space_before_sep:
before_text += ' '
before_text += self.sep + ' '
if self.add_separator or after_text:
# Add separator to the end of completed text
if self.space_before_sep:
text = text.rstrip() + ' '
completed_text = text + self.sep + ' '
else: else:
prefix_len = len(before_text.split(self.sep)[-1].lstrip()) completed_text = text
completed_text = before_text[:cursor_pos - prefix_len] + text + after_text return before_text + completed_text, after_text
return prefix_len, completed_text
def completion_selected(self, text): def completion_selected(self, text):
prefix_len, ctext = self.get_completed_text(unicode(text)) before_text, after_text = self.get_completed_text(unicode(text))
if self.sep is None: self.setText(before_text + after_text)
self.setText(ctext) self.setCursorPosition(len(before_text))
self.setCursorPosition(len(ctext))
else:
cursor_pos = self.cursorPosition()
self.setText(ctext)
self.setCursorPosition(cursor_pos - prefix_len + len(text))
@dynamic_property @dynamic_property
def all_items(self): def all_items(self):
@ -164,6 +168,9 @@ class MultiCompleteComboBox(EnComboBox):
def set_space_before_sep(self, space_before): def set_space_before_sep(self, space_before):
self.lineEdit().set_space_before_sep(space_before) self.lineEdit().set_space_before_sep(space_before)
def set_add_separator(self, what):
self.lineEdit().set_add_separator(what)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -19,6 +19,7 @@ from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2.convert import Widget from calibre.gui2.convert import Widget
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
from calibre.utils.config import tweaks
def create_opf_file(db, book_id): def create_opf_file(db, book_id):
mi = db.get_metadata(book_id, index_is_id=True) mi = db.get_metadata(book_id, index_is_id=True)
@ -108,6 +109,7 @@ class MetadataWidget(Widget, Ui_Form):
all_authors.sort(key=lambda x : sort_key(x[1])) all_authors.sort(key=lambda x : sort_key(x[1]))
self.author.set_separator('&') self.author.set_separator('&')
self.author.set_space_before_sep(True) self.author.set_space_before_sep(True)
self.author.set_add_separator(tweaks['authors_completer_append_separator'])
self.author.update_items_cache(self.db.all_author_names()) self.author.update_items_cache(self.db.all_author_names())
for i in all_authors: for i in all_authors:

View File

@ -8,8 +8,6 @@ __docformat__ = 'restructuredtext en'
from calibre.gui2.convert.txt_output_ui import Ui_Form from calibre.gui2.convert.txt_output_ui import Ui_Form
from calibre.gui2.convert import Widget from calibre.gui2.convert import Widget
newline_model = None
class PluginWidget(Widget, Ui_Form): class PluginWidget(Widget, Ui_Form):
TITLE = _('TXT Output') TITLE = _('TXT Output')

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from calibre.gui2.convert.txt_output import PluginWidget as TXTPluginWidget
class PluginWidget(TXTPluginWidget):
TITLE = _('TXTZ Output')
HELP = _('Options specific to')+' TXTZ '+_('output')
COMMIT_NAME = 'txtz_output'

View File

@ -1026,6 +1026,20 @@ class DeviceMixin(object): # {{{
self.location_manager.free[1] : 'carda', self.location_manager.free[1] : 'carda',
self.location_manager.free[2] : 'cardb' } self.location_manager.free[2] : 'cardb' }
on_card = space.get(sorted(space.keys(), reverse=True)[0], None) on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
try:
total_size = sum([os.stat(f).st_size for f in files])
except:
try:
import traceback
traceback.print_exc()
except:
pass
total_size = self.location_manager.free[0]
if self.location_manager.free[0] > total_size + (1024**2):
# Send news to main memory if enough space available
# as some devices like the Nook Color cannot handle
# periodicals on SD cards properly
on_card = None
self.upload_books(files, names, metadata, self.upload_books(files, names, metadata,
on_card=on_card, on_card=on_card,
memory=[files, remove]) memory=[files, remove])

View File

@ -9,6 +9,7 @@ from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.gui2.complete import MultiCompleteComboBox from calibre.gui2.complete import MultiCompleteComboBox
from calibre.utils.config import tweaks
class AddEmptyBookDialog(QDialog): class AddEmptyBookDialog(QDialog):
@ -69,6 +70,7 @@ class AddEmptyBookDialog(QDialog):
self.authors_combo.set_separator('&') self.authors_combo.set_separator('&')
self.authors_combo.set_space_before_sep(True) self.authors_combo.set_space_before_sep(True)
self.authors_combo.set_add_separator(tweaks['authors_completer_append_separator'])
self.authors_combo.update_items_cache(db.all_author_names()) self.authors_combo.update_items_cache(db.all_author_names())
@property @property

View File

@ -71,6 +71,8 @@ class ChooseLibrary(QDialog, Ui_Dialog):
prefs['library_path'] = loc prefs['library_path'] = loc
self.callback(loc, copy_structure=self.copy_structure.isChecked()) self.callback(loc, copy_structure=self.copy_structure.isChecked())
else: else:
self.db.prefs.disable_setting = True
self.library_renamed = True
move_library(self.db.library_path, loc, self.parent(), move_library(self.db.library_path, loc, self.parent(),
self.callback) self.callback)

View File

@ -781,6 +781,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.authors.set_separator('&') self.authors.set_separator('&')
self.authors.set_space_before_sep(True) self.authors.set_space_before_sep(True)
self.authors.set_add_separator(tweaks['authors_completer_append_separator'])
self.authors.update_items_cache(self.db.all_author_names()) self.authors.update_items_cache(self.db.all_author_names())
def initialize_series(self): def initialize_series(self):

View File

@ -735,6 +735,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.authors.set_separator('&') self.authors.set_separator('&')
self.authors.set_space_before_sep(True) self.authors.set_space_before_sep(True)
self.authors.set_add_separator(tweaks['authors_completer_append_separator'])
self.authors.update_items_cache(self.db.all_author_names()) self.authors.update_items_cache(self.db.all_author_names())
def initialize_series(self): def initialize_series(self):

View File

@ -9,6 +9,7 @@ from calibre.gui2.dialogs.search_ui import Ui_Dialog
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
from calibre.gui2 import gprefs from calibre.gui2 import gprefs
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks
box_values = {} box_values = {}
@ -31,6 +32,7 @@ class SearchDialog(QDialog, Ui_Dialog):
self.authors_box.setEditText('') self.authors_box.setEditText('')
self.authors_box.set_separator('&') self.authors_box.set_separator('&')
self.authors_box.set_space_before_sep(True) self.authors_box.set_space_before_sep(True)
self.authors_box.set_add_separator(tweaks['authors_completer_append_separator'])
self.authors_box.update_items_cache(db.all_author_names()) self.authors_box.update_items_cache(db.all_author_names())
all_series = db.all_series() all_series = db.all_series()

View File

@ -99,8 +99,8 @@ class TagListEditor(QDialog, Ui_TagListEditor):
return return
self.available_tags.editItem(item) self.available_tags.editItem(item)
def delete_tags(self, item=None): def delete_tags(self):
deletes = self.available_tags.selectedItems() if item is None else [item] deletes = self.available_tags.selectedItems()
if not deletes: if not deletes:
error_dialog(self, _('No items selected'), error_dialog(self, _('No items selected'),
_('You must select at least one items from the list.')).exec_() _('You must select at least one items from the list.')).exec_()

View File

@ -64,6 +64,7 @@ class LibraryViewMixin(object): # {{{
view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book) view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book)
self.build_context_menus() self.build_context_menus()
self.library_view.model().set_highlight_only(config['highlight_search_matches'])
def build_context_menus(self): def build_context_menus(self):
lm = QMenu(self) lm = QMenu(self)

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
from functools import partial from functools import partial
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \ from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
pyqtSignal, QToolButton, QMenu, QCheckBox, \ pyqtSignal, QToolButton, QMenu, \
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup
@ -156,7 +156,8 @@ class SearchBar(QWidget): # {{{
x = ComboBoxWithHelp(self) x = ComboBoxWithHelp(self)
x.setMaximumSize(QSize(150, 16777215)) x.setMaximumSize(QSize(150, 16777215))
x.setObjectName("search_restriction") x.setObjectName("search_restriction")
x.setToolTip(_("Books display will be restricted to those matching the selected saved search")) x.setToolTip(_('Books display will be restricted to those matching the '
'selected saved search'))
l.addWidget(x) l.addWidget(x)
parent.search_restriction = x parent.search_restriction = x
@ -175,7 +176,8 @@ class SearchBar(QWidget): # {{{
x = parent.search = SearchBox2(self) x = parent.search = SearchBox2(self)
x.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) x.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
x.setObjectName("search") x.setObjectName("search")
x.setToolTip(_("<p>Search the list of books by title, author, publisher, tags, comments, etc.<br><br>Words separated by spaces are ANDed")) x.setToolTip(_("<p>Search the list of books by title, author, publisher, "
"tags, comments, etc.<br><br>Words separated by spaces are ANDed"))
l.addWidget(x) l.addWidget(x)
self.search_button = QToolButton() self.search_button = QToolButton()
@ -194,13 +196,11 @@ class SearchBar(QWidget): # {{{
l.addWidget(x) l.addWidget(x)
x.setToolTip(_("Reset Quick Search")) x.setToolTip(_("Reset Quick Search"))
x = parent.search_highlight_only = QCheckBox() x = parent.search_options_button = QToolButton(self)
x.setText(_('&Highlight')) x.setIcon(QIcon(I('config.png')))
x.setToolTip('<p>'+_('When searching, highlight matched books, instead ' x.setObjectName("search_option_button")
'of restricting the book list to the matches.<p> You can use the '
'N or F3 keys to go to the next match.'))
l.addWidget(x) l.addWidget(x)
x.setVisible(False) x.setToolTip(_("Change the way searching for books works"))
x = parent.saved_search = SavedSearchBox(self) x = parent.saved_search = SavedSearchBox(self)
x.setMaximumSize(QSize(150, 16777215)) x.setMaximumSize(QSize(150, 16777215))
@ -227,7 +227,6 @@ class SearchBar(QWidget): # {{{
x.setToolTip(_("Delete current saved search")) x.setToolTip(_("Delete current saved search"))
# }}} # }}}
class Spacer(QWidget): # {{{ class Spacer(QWidget): # {{{

View File

@ -177,6 +177,8 @@ class CompleteDelegate(QStyledItemDelegate): # {{{
editor = MultiCompleteLineEdit(parent) editor = MultiCompleteLineEdit(parent)
editor.set_separator(self.sep) editor.set_separator(self.sep)
editor.set_space_before_sep(self.space_before_sep) editor.set_space_before_sep(self.space_before_sep)
if self.sep == '&':
editor.set_add_separator(tweaks['authors_completer_append_separator'])
if not index.model().is_custom_column(col): if not index.model().is_custom_column(col):
all_items = getattr(self.db, self.items_func_name)() all_items = getattr(self.db, self.items_func_name)()
else: else:

View File

@ -238,8 +238,6 @@ class BooksModel(QAbstractTableModel): # {{{
def set_highlight_only(self, toWhat): def set_highlight_only(self, toWhat):
self.highlight_only = toWhat self.highlight_only = toWhat
if self.last_search:
self.research()
def get_current_highlighted_id(self): def get_current_highlighted_id(self):
if len(self.ids_to_highlight) == 0 or self.current_highlighted_idx is None: if len(self.ids_to_highlight) == 0 or self.current_highlighted_idx is None:

View File

@ -177,6 +177,7 @@ class AuthorsEdit(MultiCompleteComboBox):
self.set_separator('&') self.set_separator('&')
self.set_space_before_sep(True) self.set_space_before_sep(True)
self.set_add_separator(tweaks['authors_completer_append_separator'])
self.update_items_cache(db.all_author_names()) self.update_items_cache(db.all_author_names())
au = db.authors(id_, index_is_id=True) au = db.authors(id_, index_is_id=True)

View File

@ -46,7 +46,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('disable_tray_notification', config) r('disable_tray_notification', config)
r('use_roman_numerals_for_series_number', config) r('use_roman_numerals_for_series_number', config)
r('separate_cover_flow', config, restart_required=True) r('separate_cover_flow', config, restart_required=True)
r('search_as_you_type', config)
r('show_child_bar', gprefs) r('show_child_bar', gprefs)
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'), choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
@ -116,7 +115,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def refresh_gui(self, gui): def refresh_gui(self, gui):
gui.search.search_as_you_type(config['search_as_you_type'])
self.update_font_display() self.update_font_display()
gui.tags_view.reread_collapse_parameters() gui.tags_view.reread_collapse_parameters()

View File

@ -124,23 +124,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0"> <item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_separate_cover_flow"> <widget class="QCheckBox" name="opt_separate_cover_flow">
<property name="text"> <property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string> <string>Show cover &amp;browser in a separate window (needs restart)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1">
<widget class="QCheckBox" name="opt_search_as_you_type">
<property name="text">
<string>Search as you type</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2"> <item row="7" column="0" colspan="2">
<layout class="QHBoxLayout"> <layout class="QHBoxLayout">
<item> <item>

View File

@ -157,11 +157,12 @@ class Preferences(QMainWindow):
run_wizard_requested = pyqtSignal() run_wizard_requested = pyqtSignal()
def __init__(self, gui, initial_plugin=None): def __init__(self, gui, initial_plugin=None, close_after_initial=False):
QMainWindow.__init__(self, gui) QMainWindow.__init__(self, gui)
self.gui = gui self.gui = gui
self.must_restart = False self.must_restart = False
self.committed = False self.committed = False
self.close_after_initial = close_after_initial
self.resize(900, 720) self.resize(900, 720)
nh, nw = min_available_height()-25, available_width()-10 nh, nw = min_available_height()-25, available_width()-10
@ -306,7 +307,7 @@ class Preferences(QMainWindow):
def esc(self, *args): def esc(self, *args):
if self.stack.currentIndex() == 1: if self.stack.currentIndex() == 1:
self.hide_plugin() self.cancel()
elif self.stack.currentIndex() == 0: elif self.stack.currentIndex() == 0:
self.close() self.close()
@ -331,11 +332,14 @@ class Preferences(QMainWindow):
show_copy_button=False) show_copy_button=False)
self.showing_widget.refresh_gui(self.gui) self.showing_widget.refresh_gui(self.gui)
self.hide_plugin() self.hide_plugin()
if must_restart and rc: if self.close_after_initial or (must_restart and rc):
self.close() self.close()
def cancel(self, *args): def cancel(self, *args):
if self.close_after_initial:
self.close()
else:
self.hide_plugin() self.hide_plugin()
def restore_defaults(self, *args): def restore_defaults(self, *args):

View File

@ -109,7 +109,7 @@ class PluginModel(QAbstractItemModel, SearchQueryParser): # {{{
return self.index(ans[0], 0, QModelIndex()) if ans[1] < 0 else \ return self.index(ans[0], 0, QModelIndex()) if ans[1] < 0 else \
self.index(ans[1], 0, self.index(ans[0], 0, QModelIndex())) self.index(ans[1], 0, self.index(ans[0], 0, QModelIndex()))
def index(self, row, column, parent): def index(self, row, column, parent=QModelIndex()):
if not self.hasIndex(row, column, parent): if not self.hasIndex(row, column, parent):
return QModelIndex() return QModelIndex()
@ -165,8 +165,6 @@ class PluginModel(QAbstractItemModel, SearchQueryParser): # {{{
def flags(self, index): def flags(self, index):
if not index.isValid(): if not index.isValid():
return 0 return 0
if index.internalId() == 0:
return Qt.ItemIsEnabled
flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
return flags return flags

View File

@ -0,0 +1,38 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QApplication
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \
CommaSeparatedList
from calibre.gui2.preferences.search_ui import Ui_Form
from calibre.gui2 import config
from calibre.utils.config import prefs
class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
r = self.register
r('search_as_you_type', config)
r('highlight_search_matches', config)
r('limit_search_columns', prefs)
r('limit_search_columns_to', prefs, setting=CommaSeparatedList)
fl = gui.library_view.model().db.field_metadata.get_search_terms()
self.opt_limit_search_columns_to.update_items_cache(fl)
def refresh_gui(self, gui):
gui.search.search_as_you_type(config['search_as_you_type'])
gui.library_view.model().set_highlight_only(config['highlight_search_matches'])
gui.search.do_search()
if __name__ == '__main__':
app = QApplication([])
test_widget('Interface', 'Search')

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>670</width>
<height>392</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QCheckBox" name="opt_search_as_you_type">
<property name="text">
<string>Search as you &amp;type</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="opt_highlight_search_matches">
<property name="text">
<string>&amp;Highlight search results instead of restricting the book list to the results</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>What to search by default</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>When you enter a search term without a prefix, by default calibre will search all metadata for matches. For example, entering, &quot;asimov&quot; will search not just authors but title/tags/series/comments/etc. Use these options if you would like to change this behavior.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="opt_limit_search_columns">
<property name="text">
<string>&amp;Limit the searched metadata</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Columns that non-prefixed searches are limited to:</string>
</property>
<property name="buddy">
<cstring>opt_limit_search_columns_to</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="MultiCompleteLineEdit" name="opt_limit_search_columns_to"/>
</item>
<item row="5" column="0" colspan="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Note that this option affects all searches, including saved searches and restrictions. Therefore, if you use this option, it is best to ensure that you always use prefixes in your saved searches. For example, use &quot;series:Foundation&quot; rather than just &quot;Foundation&quot; in a saved search</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MultiCompleteLineEdit</class>
<extends>QLineEdit</extends>
<header>calibre/gui2.complete.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -5,37 +5,321 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import textwrap
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit
from calibre.gui2.preferences.tweaks_ui import Ui_Form from calibre.gui2.preferences.tweaks_ui import Ui_Form
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog, NONE
from calibre.utils.config import read_raw_tweaks, write_tweaks from calibre.utils.config import read_raw_tweaks, write_tweaks
from calibre.gui2.widgets import PythonHighlighter
from calibre import isbytestring
from PyQt4.Qt import QAbstractListModel, Qt, QStyledItemDelegate, QStyle, \
QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog, \
QVBoxLayout, QPlainTextEdit, QLabel
class Delegate(QStyledItemDelegate): # {{{
def __init__(self, view):
QStyledItemDelegate.__init__(self, view)
self.view = view
def paint(self, p, opt, idx):
copy = QStyleOptionViewItem(opt)
copy.showDecorationSelected = True
if self.view.currentIndex() == idx:
copy.state |= QStyle.State_HasFocus
QStyledItemDelegate.paint(self, p, copy, idx)
# }}}
class Tweak(object): # {{{
def __init__(self, name, doc, var_names, defaults, custom):
self.name = name
self.doc = doc.strip()
self.var_names = var_names
self.default_values = {}
for x in var_names:
self.default_values[x] = defaults[x]
self.custom_values = {}
for x in var_names:
if x in custom:
self.custom_values[x] = custom[x]
def __str__(self):
ans = ['#: ' + self.name]
for line in self.doc.splitlines():
if line:
ans.append('# ' + line)
for key, val in self.default_values.iteritems():
val = self.custom_values.get(key, val)
ans.append('%s = %r'%(key, val))
ans = '\n'.join(ans)
if isinstance(ans, unicode):
ans = ans.encode('utf-8')
return ans
def __cmp__(self, other):
return -1 * cmp(self.is_customized,
getattr(other, 'is_customized', False))
@property
def is_customized(self):
for x, val in self.default_values.iteritems():
if self.custom_values.get(x, val) != val:
return True
return False
@property
def edit_text(self):
ans = ['# %s'%self.name]
for x, val in self.default_values.iteritems():
val = self.custom_values.get(x, val)
ans.append('%s = %r'%(x, val))
return '\n\n'.join(ans)
def restore_to_default(self):
self.custom_values.clear()
def update(self, varmap):
self.custom_values.update(varmap)
# }}}
class Tweaks(QAbstractListModel): # {{{
def __init__(self, parent=None):
QAbstractListModel.__init__(self, parent)
raw_defaults, raw_custom = read_raw_tweaks()
self.parse_tweaks(raw_defaults, raw_custom)
def rowCount(self, *args):
return len(self.tweaks)
def data(self, index, role):
row = index.row()
try:
tweak = self.tweaks[row]
except:
return NONE
if role == Qt.DisplayRole:
return textwrap.fill(tweak.name, 40)
if role == Qt.FontRole and tweak.is_customized:
ans = QFont()
ans.setBold(True)
return ans
if role == Qt.ToolTipRole:
tt = _('This tweak has it default value')
if tweak.is_customized:
tt = '<p>'+_('This tweak has been customized')
tt += '<pre>'
for varn, val in tweak.custom_values.iteritems():
tt += '%s = %r\n\n'%(varn, val)
return tt
if role == Qt.UserRole:
return tweak
return NONE
def parse_tweaks(self, defaults, custom):
l, g = {}, {}
try:
exec custom in g, l
except:
print 'Failed to load custom tweaks file'
import traceback
traceback.print_exc()
dl, dg = {}, {}
exec defaults in dg, dl
lines = defaults.splitlines()
pos = 0
self.tweaks = []
while pos < len(lines):
line = lines[pos]
if line.startswith('#:'):
pos = self.read_tweak(lines, pos, dl, l)
pos += 1
self.tweaks.sort()
default_keys = set(dl.iterkeys())
custom_keys = set(l.iterkeys())
self.plugin_tweaks = {}
for key in custom_keys - default_keys:
self.plugin_tweaks[key] = l[key]
def read_tweak(self, lines, pos, defaults, custom):
name = lines[pos][2:].strip()
doc, var_names = [], []
while True:
pos += 1
line = lines[pos]
if not line.startswith('#'):
break
doc.append(line[1:].strip())
doc = '\n'.join(doc)
while True:
line = lines[pos]
if not line.strip():
break
spidx1 = line.find(' ')
spidx2 = line.find('=')
spidx = spidx1 if spidx1 > 0 and (spidx2 == 0 or spidx2 > spidx1) else spidx2
if spidx > 0:
var = line[:spidx]
if var not in defaults:
raise ValueError('%r not in default tweaks dict'%var)
var_names.append(var)
pos += 1
if not var_names:
raise ValueError('Failed to find any variables for %r'%name)
self.tweaks.append(Tweak(name, doc, var_names, defaults, custom))
#print '\n\n', self.tweaks[-1]
return pos
def restore_to_default(self, idx):
tweak = self.data(idx, Qt.UserRole)
if tweak is not NONE:
tweak.restore_to_default()
self.dataChanged.emit(idx, idx)
def restore_to_defaults(self):
for r in range(self.rowCount()):
self.restore_to_default(self.index(r))
def update_tweak(self, idx, varmap):
tweak = self.data(idx, Qt.UserRole)
if tweak is not NONE:
tweak.update(varmap)
self.dataChanged.emit(idx, idx)
def to_string(self):
ans = ['#!/usr/bin/env python',
'# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai', '',
'# This file was automatically generated by calibre, do not'
' edit it unless you know what you are doing.', '',
]
for tweak in self.tweaks:
ans.extend(['', str(tweak), ''])
if self.plugin_tweaks:
ans.extend(['', '',
'# The following are tweaks for installed plugins', ''])
for key, val in self.plugin_tweaks.iteritems():
ans.extend(['%s = %r'%(key, val), '', ''])
return '\n'.join(ans)
@property
def plugin_tweaks_string(self):
ans = []
for key, val in self.plugin_tweaks.iteritems():
ans.extend(['%s = %r'%(key, val), '', ''])
ans = '\n'.join(ans)
if isbytestring(ans):
ans = ans.decode('utf-8')
return ans
def set_plugin_tweaks(self, d):
self.plugin_tweaks = d
# }}}
class PluginTweaks(QDialog): # {{{
def __init__(self, raw, parent=None):
QDialog.__init__(self, parent)
self.edit = QPlainTextEdit(self)
self.highlighter = PythonHighlighter(self.edit.document())
self.l = QVBoxLayout()
self.setLayout(self.l)
self.msg = QLabel(
_('Add/edit tweaks for any custom plugins you have installed. '
'Documentation for these tweaks should be available '
'on the website from where you downloaded the plugins.'))
self.msg.setWordWrap(True)
self.l.addWidget(self.msg)
self.l.addWidget(self.edit)
self.edit.setPlainText(raw)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel,
Qt.Horizontal, self)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
self.l.addWidget(self.bb)
self.resize(550, 300)
# }}}
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui): def genesis(self, gui):
self.gui = gui self.gui = gui
self.current_tweaks.textChanged.connect(self.changed) self.delegate = Delegate(self.tweaks_view)
self.tweaks_view.setItemDelegate(self.delegate)
self.tweaks_view.currentChanged = self.current_changed
self.highlighter = PythonHighlighter(self.edit_tweak.document())
self.restore_default_button.clicked.connect(self.restore_to_default)
self.apply_button.clicked.connect(self.apply_tweak)
self.plugin_tweaks_button.clicked.connect(self.plugin_tweaks)
def plugin_tweaks(self):
raw = self.tweaks.plugin_tweaks_string
d = PluginTweaks(raw, self)
if d.exec_() == d.Accepted:
g, l = {}, {}
try:
exec unicode(d.edit.toPlainText()) in g, l
except:
import traceback
return error_dialog(self, _('Failed'),
_('There was a syntax error in your tweak. Click '
'the show details button for details.'), show=True,
det_msg=traceback.format_exc())
self.tweaks.set_plugin_tweaks(l)
self.changed()
def current_changed(self, current, previous):
tweak = self.tweaks.data(current, Qt.UserRole)
self.help.setPlainText(tweak.doc)
self.edit_tweak.setPlainText(tweak.edit_text)
def changed(self, *args): def changed(self, *args):
self.changed_signal.emit() self.changed_signal.emit()
def initialize(self): def initialize(self):
deft, curt = read_raw_tweaks() self.tweaks = Tweaks()
self.current_tweaks.blockSignals(True) self.tweaks_view.setModel(self.tweaks)
self.current_tweaks.setPlainText(curt.decode('utf-8'))
self.current_tweaks.blockSignals(False)
self.default_tweaks.setPlainText(deft.decode('utf-8')) def restore_to_default(self, *args):
idx = self.tweaks_view.currentIndex()
if idx.isValid():
self.tweaks.restore_to_default(idx)
tweak = self.tweaks.data(idx, Qt.UserRole)
self.edit_tweak.setPlainText(tweak.edit_text)
self.changed()
def restore_defaults(self): def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self) ConfigWidgetBase.restore_defaults(self)
deft, curt = read_raw_tweaks() self.tweaks.restore_to_defaults()
self.current_tweaks.setPlainText(deft.decode('utf-8')) self.changed()
def apply_tweak(self):
idx = self.tweaks_view.currentIndex()
if idx.isValid():
l, g = {}, {}
try:
exec unicode(self.edit_tweak.toPlainText()) in g, l
except:
import traceback
error_dialog(self.gui, _('Failed'),
_('There was a syntax error in your tweak. Click '
'the show details button for details.'),
det_msg=traceback.format_exc(), show=True)
return
self.tweaks.update_tweak(idx, l)
self.changed()
def commit(self): def commit(self):
raw = unicode(self.current_tweaks.toPlainText()).encode('utf-8') raw = self.tweaks.to_string()
try: try:
exec raw exec raw
except: except:
@ -54,5 +338,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if __name__ == '__main__': if __name__ == '__main__':
from PyQt4.Qt import QApplication from PyQt4.Qt import QApplication
app = QApplication([]) app = QApplication([])
#Tweaks()
#test_widget
test_widget('Advanced', 'Tweaks') test_widget('Advanced', 'Tweaks')

View File

@ -7,17 +7,19 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>660</width> <width>660</width>
<height>351</height> <height>531</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" rowspan="2">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item> <item>
<widget class="QLabel" name="label_18"> <widget class="QLabel" name="label_18">
<property name="text"> <property name="text">
<string>Values for the tweaks are shown below. Edit them to change the behavior of calibre. Your changes will only take effect after a restart of calibre.</string> <string>Values for the tweaks are shown below. Edit them to change the behavior of calibre. Your changes will only take effect &lt;b&gt;after a restart&lt;/b&gt; of calibre.</string>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>
@ -25,13 +27,53 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="groupBox_6"> <widget class="QListView" name="tweaks_view">
<property name="title"> <property name="sizePolicy">
<string>All available tweaks</string> <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="spacing">
<number>5</number>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="plugin_tweaks_button">
<property name="toolTip">
<string>Edit tweaks for any custom plugins you have installed</string>
</property>
<property name="text">
<string>&amp;Plugin tweaks</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Help</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPlainTextEdit" name="help">
<property name="lineWrapMode">
<enum>QPlainTextEdit::NoWrap</enum>
</property> </property>
<layout class="QGridLayout" name="gridLayout_11">
<item row="0" column="0">
<widget class="QPlainTextEdit" name="default_tweaks">
<property name="readOnly"> <property name="readOnly">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -40,14 +82,38 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item> <item row="1" column="1">
<widget class="QGroupBox" name="groupBox_7"> <widget class="QGroupBox" name="groupBox_2">
<property name="title"> <property name="title">
<string>&amp;Current tweaks</string> <string>Edit tweak</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout_10"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0"> <item row="0" column="0" colspan="2">
<widget class="QPlainTextEdit" name="current_tweaks"/> <widget class="QPlainTextEdit" name="edit_tweak">
<property name="lineWrapMode">
<enum>QPlainTextEdit::NoWrap</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="restore_default_button">
<property name="toolTip">
<string>Restore this tweak to its default value</string>
</property>
<property name="text">
<string>Restore &amp;default</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="apply_button">
<property name="toolTip">
<string>Apply any changes you made to this tweak</string>
</property>
<property name="text">
<string>&amp;Apply</string>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>

View File

@ -16,7 +16,6 @@ from calibre.gui2 import config
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.dialogs.search import SearchDialog from calibre.gui2.dialogs.search import SearchDialog
from calibre.utils.config import dynamic
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
@ -376,9 +375,7 @@ class SearchBoxMixin(object): # {{{
unicode(self.search.toolTip()))) unicode(self.search.toolTip())))
self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip()) self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip())
self.clear_button.setStatusTip(self.clear_button.toolTip()) self.clear_button.setStatusTip(self.clear_button.toolTip())
self.search_highlight_only.stateChanged.connect(self.highlight_only_changed) self.search_options_button.clicked.connect(self.search_options_button_clicked)
self.search_highlight_only.setChecked(
dynamic.get('search_highlight_only', False))
def focus_search_box(self, *args): def focus_search_box(self, *args):
self.search.setFocus(Qt.OtherFocusReason) self.search.setFocus(Qt.OtherFocusReason)
@ -402,14 +399,13 @@ class SearchBoxMixin(object): # {{{
self.search.do_search() self.search.do_search()
self.focus_to_library() self.focus_to_library()
def search_options_button_clicked(self):
self.iactions['Preferences'].do_config(initial_plugin=('Interface',
'Search'), close_after_initial=True)
def focus_to_library(self): def focus_to_library(self):
self.current_view().setFocus(Qt.OtherFocusReason) self.current_view().setFocus(Qt.OtherFocusReason)
def highlight_only_changed(self, toWhat):
dynamic.set('search_highlight_only', toWhat)
self.current_view().model().set_highlight_only(toWhat)
self.focus_to_library()
# }}} # }}}
class SavedSearchBoxMixin(object): # {{{ class SavedSearchBoxMixin(object): # {{{

View File

@ -116,7 +116,14 @@ class TagsView(QTreeView): # {{{
self.set_new_model(self._model.get_filter_categories_by()) self.set_new_model(self._model.get_filter_categories_by())
def set_database(self, db, tag_match, sort_by): def set_database(self, db, tag_match, sort_by):
self.hidden_categories = db.prefs.get('tag_browser_hidden_categories', None)
# migrate from config to db prefs
if self.hidden_categories is None:
self.hidden_categories = config['tag_browser_hidden_categories'] self.hidden_categories = config['tag_browser_hidden_categories']
db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
else:
self.hidden_categories = set(self.hidden_categories)
old = getattr(self, '_model', None) old = getattr(self, '_model', None)
if old is not None: if old is not None:
old.break_cycles() old.break_cycles()
@ -234,7 +241,7 @@ class TagsView(QTreeView): # {{{
gprefs['tags_browser_partition_method'] = category gprefs['tags_browser_partition_method'] = category
elif action == 'defaults': elif action == 'defaults':
self.hidden_categories.clear() self.hidden_categories.clear()
config.set('tag_browser_hidden_categories', self.hidden_categories) self.db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
self.set_new_model() self.set_new_model()
except: except:
return return
@ -1214,7 +1221,7 @@ class TagBrowserMixin(object): # {{{
db.field_metadata.remove_user_categories() db.field_metadata.remove_user_categories()
for k in d.categories: for k in d.categories:
db.field_metadata.add_user_category('@' + k, k) db.field_metadata.add_user_category('@' + k, k)
db.data.sqp_change_locations(db.field_metadata.get_search_terms()) db.data.change_search_locations(db.field_metadata.get_search_terms())
self.tags_view.set_new_model() self.tags_view.set_new_model()
self.tags_view.recount() self.tags_view.recount()

View File

@ -483,8 +483,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
action.location_selected(location) action.location_selected(location)
if location == 'library': if location == 'library':
self.search_restriction.setEnabled(True) self.search_restriction.setEnabled(True)
self.search_options_button.setEnabled(True)
else: else:
self.search_restriction.setEnabled(False) self.search_restriction.setEnabled(False)
self.search_options_button.setEnabled(False)
# Reset the view in case something changed while it was invisible # Reset the view in case something changed while it was invisible
self.current_view().reset() self.current_view().reset()
self.set_number_of_books_shown() self.set_number_of_books_shown()

View File

@ -440,16 +440,17 @@ class Document(QWebPage): # {{{
@property @property
def height(self): def height(self):
j = self.javascript('document.body.offsetHeight', 'int') # Note that document.body.offsetHeight does not include top and bottom
# margins on body and in some cases does not include the top margin on
# the first element inside body either. See ticket #8791 for an example
# of the latter.
q = self.mainFrame().contentsSize().height() q = self.mainFrame().contentsSize().height()
if q == j: if q < 0:
return j # Don't know if this is still needed, but it can't hurt
if min(j, q) <= 0: j = self.javascript('document.body.offsetHeight', 'int')
return max(j, q) if j >= 0:
window_height = self.window_height q = j
if j == window_height: return q
return j if q < 1.2*j else q
return j
@property @property
def width(self): def width(self):

View File

@ -20,7 +20,7 @@ from calibre.gui2.filename_pattern_ui import Ui_Form
from calibre import fit_image from calibre import fit_image
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata.meta import metadata_from_filename from calibre.ebooks.metadata.meta import metadata_from_filename
from calibre.utils.config import prefs, XMLConfig from calibre.utils.config import prefs, XMLConfig, tweaks
from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator
history = XMLConfig('history') history = XMLConfig('history')
@ -932,7 +932,7 @@ class SplitterHandle(QSplitterHandle):
def paintEvent(self, ev): def paintEvent(self, ev):
QSplitterHandle.paintEvent(self, ev) QSplitterHandle.paintEvent(self, ev)
if self.highlight: if self.highlight and tweaks['draw_hidden_section_indicators']:
painter = QPainter(self) painter = QPainter(self)
painter.setClipRect(ev.rect()) painter.setClipRect(ev.rect())
painter.fillRect(self.rect(), Qt.yellow) painter.fillRect(self.rect(), Qt.yellow)

View File

@ -11,7 +11,7 @@ from itertools import repeat
from datetime import timedelta from datetime import timedelta
from threading import Thread from threading import Thread
from calibre.utils.config import tweaks from calibre.utils.config import tweaks, prefs
from calibre.utils.date import parse_date, now, UNDEFINED_DATE from calibre.utils.date import parse_date, now, UNDEFINED_DATE
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.pyparsing import ParseException from calibre.utils.pyparsing import ParseException
@ -182,15 +182,16 @@ class ResultCache(SearchQueryParser): # {{{
self.first_sort = True self.first_sort = True
self.search_restriction = '' self.search_restriction = ''
self.field_metadata = field_metadata self.field_metadata = field_metadata
all_search_locations = field_metadata.get_search_terms() self.all_search_locations = field_metadata.get_search_terms()
SearchQueryParser.__init__(self, all_search_locations, optimize=True) SearchQueryParser.__init__(self, self.all_search_locations, optimize=True)
self.build_date_relop_dict() self.build_date_relop_dict()
self.build_numeric_relop_dict() self.build_numeric_relop_dict()
def break_cycles(self): def break_cycles(self):
self._data = self.field_metadata = self.FIELD_MAP = \ self._data = self.field_metadata = self.FIELD_MAP = \
self.numeric_search_relops = self.date_search_relops = \ self.numeric_search_relops = self.date_search_relops = \
self.db_prefs = None self.db_prefs = self.all_search_locations = None
self.sqp_change_locations([])
def __getitem__(self, row): def __getitem__(self, row):
@ -218,6 +219,10 @@ class ResultCache(SearchQueryParser): # {{{
def universal_set(self): def universal_set(self):
return set([i[0] for i in self._data if i is not None]) return set([i[0] for i in self._data if i is not None])
def change_search_locations(self, locations):
self.sqp_change_locations(locations)
self.all_search_locations = locations
def build_date_relop_dict(self): def build_date_relop_dict(self):
''' '''
Because the database dates have time in them, we can't use direct Because the database dates have time in them, we can't use direct
@ -432,6 +437,7 @@ class ResultCache(SearchQueryParser): # {{{
# get metadata key associated with the search term. Eliminates # get metadata key associated with the search term. Eliminates
# dealing with plurals and other aliases # dealing with plurals and other aliases
location = self.field_metadata.search_term_to_field_key(icu_lower(location.strip())) location = self.field_metadata.search_term_to_field_key(icu_lower(location.strip()))
# grouped search terms
if isinstance(location, list): if isinstance(location, list):
if allow_recursion: if allow_recursion:
for loc in location: for loc in location:
@ -440,6 +446,20 @@ class ResultCache(SearchQueryParser): # {{{
return matches return matches
raise ParseException(query, len(query), 'Recursive query group detected', self) raise ParseException(query, len(query), 'Recursive query group detected', self)
# apply the limit if appropriate
if location == 'all' and prefs['limit_search_columns'] and \
prefs['limit_search_columns_to']:
terms = set([])
for l in prefs['limit_search_columns_to']:
l = icu_lower(l.strip())
if l and l != 'all' and l in self.all_search_locations:
terms.add(l)
if terms:
for l in terms:
matches |= self.get_matches(l, query,
candidates=candidates, allow_recursion=allow_recursion)
return matches
if location in self.field_metadata: if location in self.field_metadata:
fm = self.field_metadata[location] fm = self.field_metadata[location]
# take care of dates special case # take care of dates special case

View File

@ -787,6 +787,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.id = id mi.id = id
for key, meta in self.field_metadata.custom_iteritems(): for key, meta in self.field_metadata.custom_iteritems():
mi.set_user_metadata(key, meta) mi.set_user_metadata(key, meta)
if meta['datatype'] == 'composite':
mi.set(key, val=row[meta['rec_index']])
else:
mi.set(key, val=self.get_custom(idx, label=meta['label'], mi.set(key, val=self.get_custom(idx, label=meta['label'],
index_is_id=index_is_id), index_is_id=index_is_id),
extra=self.get_custom_extra(idx, label=meta['label'], extra=self.get_custom_extra(idx, label=meta['label'],

View File

@ -7,7 +7,6 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, traceback, cStringIO, re, shutil import os, traceback, cStringIO, re, shutil
from functools import partial
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre.utils.config import Config, StringConfig, tweaks from calibre.utils.config import Config, StringConfig, tweaks
@ -142,11 +141,19 @@ class SafeFormat(TemplateFormatter):
def get_components(template, mi, id, timefmt='%b %Y', length=250, def get_components(template, mi, id, timefmt='%b %Y', length=250,
sanitize_func=ascii_filename, replace_whitespace=False, sanitize_func=ascii_filename, replace_whitespace=False,
to_lowercase=False): to_lowercase=False):
tsfmt = partial(title_sort, order=tweaks['save_template_title_series_sorting'])
tsorder = tweaks['save_template_title_series_sorting']
format_args = FORMAT_ARGS.copy() format_args = FORMAT_ARGS.copy()
format_args.update(mi.all_non_none_fields()) format_args.update(mi.all_non_none_fields())
if mi.title: if mi.title:
format_args['title'] = tsfmt(mi.title) if tsorder == 'strictly_alphabetic':
v = mi.title
else:
# title_sort might be missing or empty. Check both conditions
v = mi.get('title_sort', None)
if not v:
v = title_sort(mi.title, order=tsorder)
format_args['title'] = v
if mi.authors: if mi.authors:
format_args['authors'] = mi.format_authors() format_args['authors'] = mi.format_authors()
format_args['author'] = format_args['authors'] format_args['author'] = format_args['authors']
@ -157,7 +164,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
else: else:
format_args['tags'] = '' format_args['tags'] = ''
if mi.series: if mi.series:
format_args['series'] = tsfmt(mi.series) format_args['series'] = title_sort(mi.series, order=tsorder)
if mi.series_index is not None: if mi.series_index is not None:
format_args['series_index'] = mi.format_series_index() format_args['series_index'] = mi.format_series_index()
else: else:
@ -176,7 +183,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
cm = custom_metadata[key] cm = custom_metadata[key]
## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't... ## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't...
if cm['datatype'] == 'series': if cm['datatype'] == 'series':
format_args[key] = tsfmt(format_args[key]) format_args[key] = title_sort(format_args[key], order=tsorder)
if key+'_index' in format_args: if key+'_index' in format_args:
format_args[key+'_index'] = fmt_sidx(format_args[key+'_index']) format_args[key+'_index'] = fmt_sidx(format_args[key+'_index'])
elif cm['datatype'] == 'datetime': elif cm['datatype'] == 'datetime':

View File

@ -561,9 +561,10 @@ format, whether input or output are available in the conversion dialog under the
Convert Microsoft Word documents Convert Microsoft Word documents
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|app| does not directly convert .doc files from Microsoft Word. However, in Word, you can save the document |app| does not directly convert .doc/.docx files from Microsoft Word. However, in Word, you can save the document
as HTML and then convert the resulting HTML file with |app|. When saving as HTML, be sure to use the as HTML and then convert the resulting HTML file with |app|. When saving as HTML, be sure to use the
"Save as Web Page, Filtered" option as this will produce clean HTML that will convert well. "Save as Web Page, Filtered" option as this will produce clean HTML that will convert well. Note that Word
produces really messy HTML, converting it can take a long time, so be patient.
There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes
generating the Table of Contents much simpler. It is called BookCreator and is available for free generating the Table of Contents much simpler. It is called BookCreator and is available for free

View File

@ -182,11 +182,6 @@ If you don't want to uninstall it altogether, there are a couple of tricks you c
simplest is to simply re-name the executable file that launches the library program. More detail simplest is to simply re-name the executable file that launches the library program. More detail
`in the forums <http://www.mobileread.com/forums/showthread.php?t=65809>`_. `in the forums <http://www.mobileread.com/forums/showthread.php?t=65809>`_.
Can I use the collections feature of the SONY reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|app| has full support for collections. When you add tags to a book's metadata, those tags are turned into collections when you upload the book to the SONY reader. Also, the series information is automatically
turned into a collection on the reader. Note that the PRS-500 does not support collections for books stored on the SD card. The PRS-505 does.
How do I use |app| with my iPad/iPhone/iTouch? How do I use |app| with my iPad/iPhone/iTouch?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -375,13 +370,6 @@ Content From The Web
:depth: 1 :depth: 1
:local: :local:
My downloaded news content causes the reader to reset.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is a bug in the SONY firmware. The problem can be mitigated by switching the output format to EPUB
in the configuration dialog. Alternatively, you can use the LRF output format and use the SONY software
to transfer the files to the reader. The SONY software pre-paginates the LRF file,
thereby reducing the number of resets.
I obtained a recipe for a news site as a .py file from somewhere, how do I use it? I obtained a recipe for a news site as a .py file from somewhere, how do I use it?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Start the :guilabel:`Add custom news sources` dialog (from the :guilabel:`Fetch news` menu) and click the :guilabel:`Switch to advanced mode` button. Delete everything in the box with the recipe source code and copy paste the contents of your .py file into the box. Click :guilabel:`Add/update recipe`. Start the :guilabel:`Add custom news sources` dialog (from the :guilabel:`Fetch news` menu) and click the :guilabel:`Switch to advanced mode` button. Delete everything in the box with the recipe source code and copy paste the contents of your .py file into the box. Click :guilabel:`Add/update recipe`.
@ -391,7 +379,7 @@ I want |app| to download news from my favorite news website.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are reasonably proficient with computers, you can teach |app| to download news from any website of your choosing. To learn how to do this see :ref:`news`. If you are reasonably proficient with computers, you can teach |app| to download news from any website of your choosing. To learn how to do this see :ref:`news`.
Otherwise, you can register a request for a particular news site by adding a comment `to this ticket <http://bugs.calibre-ebook.com/ticket/405>`_. Otherwise, you can request a particular news site by posting in the `calibre Recipes forum <http://www.mobileread.com/forums/forumdisplay.php?f=228>`_.
Can I use web2disk to download an arbitrary website? Can I use web2disk to download an arbitrary website?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -480,7 +468,7 @@ How do I backup |app|?
The most important thing to backup is the |app| library folder, that contains all your books and metadata. This is the folder you chose for your |app| library when you ran |app| for the first time. You can get the path to the library folder by clicking the |app| icon on the main toolbar. You must backup this complete folder with all its files and sub-folders. The most important thing to backup is the |app| library folder, that contains all your books and metadata. This is the folder you chose for your |app| library when you ran |app| for the first time. You can get the path to the library folder by clicking the |app| icon on the main toolbar. You must backup this complete folder with all its files and sub-folders.
You can switch |app| to using a backed up library folder by simply clicking the |app| icon on the toolbar and choosing your backup library folder. You can switch |app| to using a backed up library folder by simply clicking the |app| icon on the toolbar and choosing your backup library folder. A backed up library folder backs up your custom columns and saved searches as well as all your books and metadata.
If you want to backup the |app| configuration/plugins, you have to backup the config directory. You can find this config directory via :guilabel:`Preferences->Miscellaneous`. Note that restoring configuration directories is not officially supported, but should work in most cases. Just copy the contents of the backup directory into the current configuration directory to restore. If you want to backup the |app| configuration/plugins, you have to backup the config directory. You can find this config directory via :guilabel:`Preferences->Miscellaneous`. Note that restoring configuration directories is not officially supported, but should work in most cases. Just copy the contents of the backup directory into the current configuration directory to restore.
@ -491,7 +479,7 @@ Most purchased EPUB books have `DRM <http://wiki.mobileread.com/wiki/DRM>`_. Thi
I am getting a "Permission Denied" error? I am getting a "Permission Denied" error?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A permission denied error can occur because of many possible reasons, none of them having anything to do with |app|. You can get permission denied errors if you are using an SD card with write protect enabled. Or if you, or some program you used changed the file permissions of the files in question to read only. Or if there is a filesystem error on the device which caused your operating system to mount the filesystem in read only mode or mark a particular file as read only pending recovery. Or if the files have their owner set to a user other than you. You will need to fix the underlying cause of the permissions error before resuming to use |app|. Read the error message carefully, see what file it points to and fix the permissions on that file. A permission denied error can occur because of many possible reasons, none of them having anything to do with |app|. You can get permission denied errors if you are using an SD card with write protect enabled. Or if you, or some program you used changed the file permissions of the files in question to read only. Or if there is a filesystem error on the device which caused your operating system to mount the filesystem in read only mode or mark a particular file as read only pending recovery. Or if the files have their owner set to a user other than you. Or if your file is open in another program. You will need to fix the underlying cause of the permissions error before resuming to use |app|. Read the error message carefully, see what file it points to and fix the permissions on that file.
Can I have the comment metadata show up on my reader? Can I have the comment metadata show up on my reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -504,7 +492,7 @@ I want some feature added to |app|. What can I do?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You have two choices: You have two choices:
1. Create a patch by hacking on |app| and send it to me for review and inclusion. See `Development <http://calibre-ebook.com/get-involved>`_. 1. Create a patch by hacking on |app| and send it to me for review and inclusion. See `Development <http://calibre-ebook.com/get-involved>`_.
2. `Open a ticket <http://bugs.calibre-ebook.com/newticket>`_ (you have to register and login first) and hopefully I will find the time to implement your feature. 2. `Open a ticket <http://bugs.calibre-ebook.com/newticket>`_ (you have to register and login first). Remember that |app| development is done by volunteers, so if you get no response to your feature request, it means no one feels like implementing it.
Can I include |app| on a CD to be distributed with my product/magazine? Can I include |app| on a CD to be distributed with my product/magazine?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -522,7 +510,7 @@ Why are there so many calibre-parallel processes on my system?
In addition to this some conversion plugins run tasks in their own pool of processes, so for example if you bulk convert comics, each comic conversion will use three separate processes to render the images. The job manager knows this so it will run only a single comic conversion simultaneously. In addition to this some conversion plugins run tasks in their own pool of processes, so for example if you bulk convert comics, each comic conversion will use three separate processes to render the images. The job manager knows this so it will run only a single comic conversion simultaneously.
And since I'm sure someone will ask: The reason adding/saving books are in separate processes is because of PDF. PDF processing libraries can crash on reading PDFs and I dont want the crash to take down all of calibre. Also when adding EPUB books, in order to extract the cover you have to sometimes render the HTML of the first page, which means that it either has to run the GUI thread of the main process or in a separate process. And since I'm sure someone will ask: The reason adding/saving books are in separate processes is because of PDF. PDF processing libraries can crash on reading PDFs and I dont want the crash to take down all of calibre. Also when adding EPUB books, in order to extract the cover you have to sometimes render the HTML of the first page, which means that it either has to run in the GUI thread of the main process or in a separate process.
Finally, the reason calibre keep workers alive and idle instead of launching on demand is to workaround the slow startup time of python processes. Finally, the reason calibre keep workers alive and idle instead of launching on demand is to workaround the slow startup time of python processes.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More