mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
70636b42c4
69
format_docs/pdb/apnx.txt
Normal file
69
format_docs/pdb/apnx.txt
Normal 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.
|
@ -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,7 +29,7 @@ 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
|
||||||
@ -38,6 +38,7 @@ series_index_auto_increment = 'next'
|
|||||||
authors_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)
|
||||||
@ -49,6 +50,7 @@ authors_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
|
||||||
@ -63,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
|
||||||
@ -74,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
|
||||||
@ -101,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)
|
||||||
@ -121,25 +126,30 @@ 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 how title and series names are formatted when saving to disk/sending
|
||||||
# If set to 'library_order', Leading articles such as The and A will be ignored.
|
# to device. The behavior depends on the field being processed. If processing
|
||||||
# If set to 'strictly_alphabetic', the titles will be sorted without processing
|
# title, then if set to 'library_order', the title will be replaced with
|
||||||
# For example, with library_order, The Client will sort under 'C'. With
|
# title_sort. If set to 'strictly_alphabetic', then the title is left unchanged.
|
||||||
# strictly_alphabetic, the book will sort under 'T'.
|
# If processing series, then if set to 'library_order', articles such as 'The'
|
||||||
# This flag affects Calibre's library display. It has no effect on devices. In
|
# and 'An' will be moved to the end. If set to 'strictly_alphabetic', the series
|
||||||
# addition, titles for books added before changing the flag will retain their
|
# will be sent without change. For example, with library_order, "The Lord of the
|
||||||
# order until the title is edited. Double-clicking on a title and hitting return
|
# Rings" will become "Lord of the Rings, The". With strictly_alphabetic, it
|
||||||
# without changing anything is sufficient to change the sort.
|
# would remain "The Lord of the Rings".
|
||||||
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 set to 'library_order', the title will be replaced with
|
||||||
# If set to 'strictly_alphabetic', the titles will be sorted without processing
|
# title_sort. If set to 'strictly_alphabetic', then the title is left unchanged.
|
||||||
# For example, with library_order, "The Client" will become "Client, The". With
|
# If processing series, then if set to 'library_order', articles such as 'The'
|
||||||
# strictly_alphabetic, it would remain "The Client".
|
# and 'An' will be moved to the end. If set to 'strictly_alphabetic', the series
|
||||||
|
# will be sent without change. For example, with library_order, "The Lord of the
|
||||||
|
# Rings" will become "Lord of the Rings, The". With 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
|
||||||
@ -149,7 +159,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
|
||||||
@ -159,7 +169,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
|
||||||
@ -212,7 +222,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
|
||||||
@ -231,7 +241,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':
|
||||||
@ -244,15 +254,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.
|
||||||
@ -270,13 +282,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
|
||||||
@ -284,16 +310,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.
|
||||||
@ -302,7 +326,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.
|
||||||
@ -313,12 +338,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,
|
||||||
@ -326,3 +352,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
|
||||||
|
|
||||||
|
67
resources/recipes/jakarta_post.recipe
Normal file
67
resources/recipes/jakarta_post.recipe
Normal 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'),
|
||||||
|
]
|
77
resources/recipes/kompas.recipe
Normal file
77
resources/recipes/kompas.recipe
Normal 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'),
|
||||||
|
]
|
@ -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, \
|
||||||
@ -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,
|
||||||
|
@ -83,7 +83,7 @@ class ANDROID(USBMS):
|
|||||||
'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']
|
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE']
|
||||||
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']
|
||||||
|
68
src/calibre/devices/kindle/apnx.py
Normal file
68
src/calibre/devices/kindle/apnx.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# -*- 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):
|
||||||
|
'''
|
||||||
|
Currently uses the Adobe 1024 byte count equal one page formula.
|
||||||
|
'''
|
||||||
|
|
||||||
|
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 += 1024
|
||||||
|
|
||||||
|
return pages
|
315
src/calibre/devices/kindle/bookmark.py
Normal file
315
src/calibre/devices/kindle/bookmark.py
Normal 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
|
||||||
|
|
||||||
|
# }}}
|
@ -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
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -342,11 +342,9 @@ class HeuristicProcessor(object):
|
|||||||
return content
|
return content
|
||||||
|
|
||||||
def txt_process(self, match):
|
def txt_process(self, match):
|
||||||
from calibre.ebooks.txt.processor import convert_basic, preserve_spaces, \
|
from calibre.ebooks.txt.processor import convert_basic, separate_paragraphs_single_line
|
||||||
separate_paragraphs_single_line
|
|
||||||
content = match.group('text')
|
content = match.group('text')
|
||||||
content = separate_paragraphs_single_line(content)
|
content = separate_paragraphs_single_line(content)
|
||||||
content = preserve_spaces(content)
|
|
||||||
content = convert_basic(content, epub_split_size_kb=0)
|
content = convert_basic(content, epub_split_size_kb=0)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
@ -356,6 +354,8 @@ class HeuristicProcessor(object):
|
|||||||
self.log.debug("Running Text Processing")
|
self.log.debug("Running Text Processing")
|
||||||
outerhtml = re.compile(r'.*?(?<=<pre>)(?P<text>.*?)</pre>', re.IGNORECASE|re.DOTALL)
|
outerhtml = re.compile(r'.*?(?<=<pre>)(?P<text>.*?)</pre>', re.IGNORECASE|re.DOTALL)
|
||||||
html = outerhtml.sub(self.txt_process, html)
|
html = outerhtml.sub(self.txt_process, html)
|
||||||
|
from calibre.ebooks.conversion.preprocess import convert_entities
|
||||||
|
html = re.sub(r'&(\S+?);', convert_entities, html)
|
||||||
else:
|
else:
|
||||||
# Add markup naively
|
# Add markup naively
|
||||||
# TODO - find out if there are cases where there are more than one <pre> tag or
|
# TODO - find out if there are cases where there are more than one <pre> tag or
|
||||||
|
@ -216,21 +216,22 @@ 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)
|
||||||
if metadata_xml is not None:
|
if metadata_xml is not None:
|
||||||
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
|
||||||
|
@ -37,13 +37,12 @@ class LITInput(InputFormatPlugin):
|
|||||||
body = body[0]
|
body = body[0]
|
||||||
if len(body) == 1 and body[0].tag == XHTML('pre'):
|
if len(body) == 1 and body[0].tag == XHTML('pre'):
|
||||||
pre = body[0]
|
pre = body[0]
|
||||||
from calibre.ebooks.txt.processor import convert_basic, preserve_spaces, \
|
from calibre.ebooks.txt.processor import convert_basic, \
|
||||||
separate_paragraphs_single_line
|
separate_paragraphs_single_line
|
||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import copy
|
import copy
|
||||||
html = separate_paragraphs_single_line(pre.text)
|
html = separate_paragraphs_single_line(pre.text)
|
||||||
html = preserve_spaces(html)
|
|
||||||
html = convert_basic(html).replace('<html>',
|
html = convert_basic(html).replace('<html>',
|
||||||
'<html xmlns="%s">'%XHTML_NS)
|
'<html xmlns="%s">'%XHTML_NS)
|
||||||
html = xml_to_unicode(html, strip_encoding_pats=True,
|
html = xml_to_unicode(html, strip_encoding_pats=True,
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -5,37 +5,312 @@ __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 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 = _('This tweak has been customized')
|
||||||
|
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
|
||||||
|
|
||||||
|
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.l.addWidget(QLabel(
|
||||||
|
_('Add/edit tweaks for any custom plugins you have installed.')))
|
||||||
|
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 +329,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')
|
||||||
|
|
||||||
|
@ -7,31 +7,73 @@
|
|||||||
<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>
|
<item row="0" column="0" rowspan="2">
|
||||||
<widget class="QLabel" name="label_18">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<property name="text">
|
<item>
|
||||||
<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>
|
<widget class="QLabel" name="label_18">
|
||||||
</property>
|
<property name="text">
|
||||||
<property name="wordWrap">
|
<string>Values for the tweaks are shown below. Edit them to change the behavior of calibre. Your changes will only take effect <b>after a restart</b> of calibre.</string>
|
||||||
<bool>true</bool>
|
</property>
|
||||||
</property>
|
<property name="wordWrap">
|
||||||
</widget>
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QListView" name="tweaks_view">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<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>&Plugin tweaks</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="0" column="1">
|
||||||
<widget class="QGroupBox" name="groupBox_6">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>All available tweaks</string>
|
<string>Help</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout_11">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item row="0" column="0">
|
<item>
|
||||||
<widget class="QPlainTextEdit" name="default_tweaks">
|
<widget class="QPlainTextEdit" name="help">
|
||||||
|
<property name="lineWrapMode">
|
||||||
|
<enum>QPlainTextEdit::NoWrap</enum>
|
||||||
|
</property>
|
||||||
<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>&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 &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>&Apply</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
@ -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)
|
||||||
|
@ -142,11 +142,13 @@ 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)
|
format_args['title'] = mi.title if tsorder == 'strictly_alphabetic' \
|
||||||
|
else mi.get('title_sort', title_sort(mi.title, order='library_order'))
|
||||||
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 +159,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 +178,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':
|
||||||
|
@ -107,6 +107,7 @@ _extra_lang_codes = {
|
|||||||
'en_CZ' : _('English (Czechoslovakia)'),
|
'en_CZ' : _('English (Czechoslovakia)'),
|
||||||
'en_PK' : _('English (Pakistan)'),
|
'en_PK' : _('English (Pakistan)'),
|
||||||
'en_HR' : _('English (Croatia)'),
|
'en_HR' : _('English (Croatia)'),
|
||||||
|
'en_ID' : _('English (Indonesia)'),
|
||||||
'en_IL' : _('English (Israel)'),
|
'en_IL' : _('English (Israel)'),
|
||||||
'en_SG' : _('English (Singapore)'),
|
'en_SG' : _('English (Singapore)'),
|
||||||
'en_YE' : _('English (Yemen)'),
|
'en_YE' : _('English (Yemen)'),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user