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
7053884ea4
100
Changelog.yaml
100
Changelog.yaml
@ -19,6 +19,106 @@
|
||||
# new recipes:
|
||||
# - title:
|
||||
|
||||
- version: 0.7.45
|
||||
date: 2011-02-11
|
||||
|
||||
new features:
|
||||
- title: "Add plugin to download series information from the Kent District Library"
|
||||
|
||||
- title: "Kindle driver: When uploading MOBI files to the device, upload page number information as well (used by the not yet released Kindle 3.1 firmware)"
|
||||
|
||||
- title: "When automatically sending news to device, send to main memory preferentially, if it has enough space."
|
||||
tickets: [8877]
|
||||
|
||||
- title: "Allow customization of which metadata fields are searched by default (click the preferences icon next to the search box)"
|
||||
|
||||
- title: "New format TXTZ: which is a zip file containing the TXT file and associated images + metadata. calibre can convert to and from this format and read/write metadata to it."
|
||||
|
||||
- title: "New option to control how automerge handles duplicate formats when adding books to your calibre library. See Preferences->Adding books"
|
||||
|
||||
- title: "Driver for Nokia X6, Nexus S, WiBook, MyTouch 4G and Huawei Ideos S7"
|
||||
|
||||
- title: "Nicer interface for editing tweaks"
|
||||
|
||||
- title: "Add tweak to remove yellow lines from edges of book list"
|
||||
|
||||
- title: "Completion: Restore adding of comma at end after completion for tags type fields. Add a tweak to control if an & is added after completion for author type fields"
|
||||
|
||||
- title: "Turn search as you type off by default for searching the book list. You can turn it on by clicking the preferences button next to the search bar."
|
||||
|
||||
- title: "TXT Input: Add option to remove indents and fix bug where spaces were not retained properly."
|
||||
|
||||
bug fixes:
|
||||
- title: "Fix a regression in 0.7.44 that could cause setting authors to fail in windows when the author name is very long"
|
||||
tickets: [8797]
|
||||
|
||||
- title: "E-book viewer: Fix bug that could cause the bottom of chapters to get cut-off if the topmost element had a large top margin."
|
||||
tickets: [8791]
|
||||
|
||||
- title: "Fix regression that caused a spurious error message after moving a library. Also ensure that the entries in the Copy to Library menu are updated after a library is moved/renamed/deleted."
|
||||
tickets: [8905]
|
||||
|
||||
- title: "PML Input: New handling of t and T tags. T's that do not start the line are ignored. t's that start and end the line use a margin for the text block"
|
||||
|
||||
- title: "News download: Remove all invalid ASCII control characters from article descriptions as they cause XML parsing to fail"
|
||||
|
||||
- title: "MOBI Output: Fix bug that was discarding non breaking spaces at the start of a paragraph when they were followed immediately by a tag."
|
||||
tickets: [4887]
|
||||
|
||||
- title: "LIT Input: Fix a regression in handling LIT files that contain txt rather than html data"
|
||||
tickets: [8904]
|
||||
|
||||
- title: "Fix bug in search box in the plugins dialog"
|
||||
tickets: [8882]
|
||||
|
||||
- title: "Fix renaming of categories via the Tag Browser"
|
||||
tickets: [8807]
|
||||
|
||||
- title: "Content server: Do not send mobile version to iPad"
|
||||
tickets: [8820]
|
||||
|
||||
- title: "Fix undefined publication date appearing in book jacket as 101"
|
||||
tickets: [8799]
|
||||
|
||||
- title: "Heuristics: Fix issue with invalid markup from italicize patterns."
|
||||
|
||||
- title: "TXT Input: De-hyphenate textile and markdown inpu as well. Fix inline toc not showing all items."
|
||||
|
||||
- title: "RTF Input: More encoding token splitting fixes."
|
||||
|
||||
- title: "Fix regression that broke the convenience Email to xxx entry in the connect share menu."
|
||||
tickets: [8775]
|
||||
|
||||
- title: "Fix editing of series type custom columns in the book list."
|
||||
tickets: [8765]
|
||||
|
||||
improved recipes:
|
||||
- El periodico de Aragon
|
||||
- B92
|
||||
- French Belgian news sources
|
||||
|
||||
new recipes:
|
||||
- title: "ABC.es"
|
||||
author: "Ricardo Jurado"
|
||||
|
||||
- title: "Korespondent and Kopalnia Wiedzy"
|
||||
author: "Attis"
|
||||
|
||||
- title: "Radio Prague"
|
||||
author: "Francois Pellicaan"
|
||||
|
||||
- title: "Europa Press"
|
||||
author: "Luis Hernandez"
|
||||
|
||||
- title: "Interoperability Happens and njuz.net"
|
||||
author: "Darko Miletic"
|
||||
|
||||
- title: "Weblogs SL"
|
||||
author: "desUBIKado"
|
||||
|
||||
- title: "Kompas and Jakarta Post"
|
||||
author: "Adrian Gunawan"
|
||||
|
||||
- version: 0.7.44
|
||||
date: 2011-02-04
|
||||
|
||||
|
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.
|
||||
'''
|
||||
|
||||
|
||||
#: Auto increment series index
|
||||
# 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
|
||||
# if a constant non-integer is specified.
|
||||
@ -29,14 +29,16 @@ defaults.
|
||||
# series_index_auto_increment = 16.5
|
||||
series_index_auto_increment = 'next'
|
||||
|
||||
|
||||
#: Add separator after completing an author name
|
||||
# Should the completion separator be append
|
||||
# to the end of the completed text to
|
||||
# automatically begin a new completion operation.
|
||||
# automatically begin a new completion operation
|
||||
# for authors.
|
||||
# Can be either True or False
|
||||
completer_append_separator = False
|
||||
authors_completer_append_separator = False
|
||||
|
||||
|
||||
#: Author sort name algorithm
|
||||
# The algorithm used to copy author to author_sort
|
||||
# Possible values are:
|
||||
# invert: use "fn ln" -> "ln, fn" (the default algorithm)
|
||||
@ -48,6 +50,7 @@ completer_append_separator = False
|
||||
# selecting 'manage authors', and pressing 'Recalculate all author sort values'.
|
||||
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,
|
||||
# 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
|
||||
@ -62,6 +65,7 @@ author_sort_copy_method = 'invert'
|
||||
# categories_use_field_for_author_name = 'author_sort'
|
||||
categories_use_field_for_author_name = 'author'
|
||||
|
||||
#: Control partitioning of Tag Browser
|
||||
# When partitioning the tags browser, the format of the subcategory label is
|
||||
# controlled by a template: categories_collapsed_name_template if sorting by
|
||||
# name, categories_collapsed_rating_template if sorting by average rating, and
|
||||
@ -73,24 +77,25 @@ categories_use_field_for_author_name = 'author'
|
||||
# author category will be the name of the author. The sub-values available are:
|
||||
# name: the printable name of the 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
|
||||
# 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
|
||||
# (\ characters) in the template. It doesn't hurt anything to leave it there
|
||||
# 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_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.
|
||||
# Two-values for true booleans
|
||||
# three-values for yes/no/unknown
|
||||
# Set to 'yes' for three-values, 'no' for two-values
|
||||
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
|
||||
# The argument is None if saved sort history is to be used
|
||||
# otherwise it is a list of column,order pairs. Column is the
|
||||
@ -100,6 +105,7 @@ bool_custom_columns_are_tristate = 'yes'
|
||||
# title within authors.
|
||||
sort_columns_at_startup = None
|
||||
|
||||
#; Control how dates are displayed
|
||||
# Format to be used for publication date and the timestamp (date).
|
||||
# A string controlling how the publication date is displayed in the GUI
|
||||
# d the day as number without a leading zero (1 to 31)
|
||||
@ -120,25 +126,34 @@ sort_columns_at_startup = None
|
||||
gui_pubdate_display_format = 'MMM yyyy'
|
||||
gui_timestamp_display_format = 'dd MMM yyyy'
|
||||
|
||||
# Control title and series sorting in the library view.
|
||||
# If set to 'library_order', Leading articles such as The and A will be ignored.
|
||||
# If set to 'strictly_alphabetic', the titles will be sorted without processing
|
||||
# For example, with library_order, The Client will sort under 'C'. With
|
||||
# strictly_alphabetic, the book will sort under 'T'.
|
||||
#: Control sorting of titles and series in the library display
|
||||
# Control title and series sorting in the library view. If set to
|
||||
# 'library_order', the title sort field will be used instead of the title.
|
||||
# Unless you have manually edited the title sort field, leading articles such as
|
||||
# The and A will be ignored. If set to 'strictly_alphabetic', the titles will be
|
||||
# sorted as-is (sort by title instead of title sort). For example, with
|
||||
# library_order, The Client will sort under 'C'. With strictly_alphabetic, the
|
||||
# book will sort under 'T'.
|
||||
# This flag affects Calibre's library display. It has no effect on devices. In
|
||||
# addition, titles for books added before changing the flag will retain their
|
||||
# order until the title is edited. Double-clicking on a title and hitting return
|
||||
# without changing anything is sufficient to change the sort.
|
||||
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
|
||||
# to device. If set to library_order, leading articles such as The and A will
|
||||
# be put at the end
|
||||
# If set to 'strictly_alphabetic', the titles will be sorted without processing
|
||||
# For example, with library_order, "The Client" will become "Client, The". With
|
||||
# strictly_alphabetic, it would remain "The Client".
|
||||
# to device. The behavior depends on the field being processed. If processing
|
||||
# title, then if this tweak is set to 'library_order', the title will be
|
||||
# replaced with title_sort. If it is set to 'strictly_alphabetic', then the
|
||||
# title will not be changed. If processing series, then if set to
|
||||
# 'library_order', articles such as 'The' and 'An' will be moved to the end. If
|
||||
# set to 'strictly_alphabetic', the series will be sent without change.
|
||||
# For example, if the tweak is set to library_order, "The Lord of the Rings"
|
||||
# will become "Lord of the Rings, The". If the tweak is set to
|
||||
# strictly_alphabetic, it would remain "The Lord of the Rings".
|
||||
save_template_title_series_sorting = 'library_order'
|
||||
|
||||
#: 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
|
||||
# title sort strings. The list is a regular expression, with the articles
|
||||
# separated by 'or' bars. Comparisons are case insensitive, and that cannot be
|
||||
@ -148,7 +163,7 @@ save_template_title_series_sorting = 'library_order'
|
||||
# Default: '^(A|The|An)\s+'
|
||||
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
|
||||
# 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
|
||||
@ -158,7 +173,7 @@ title_sort_articles=r'^(A|The|An)\s+'
|
||||
# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
|
||||
auto_connect_to_folder = ''
|
||||
|
||||
|
||||
#: Specify renaming rules for SONY collections
|
||||
# Specify renaming rules for sony collections. This tweak is only applicable if
|
||||
# metadata management is set to automatic. Collections on Sonys are named
|
||||
# depending upon whether the field is standard or custom. A collection derived
|
||||
@ -211,7 +226,7 @@ auto_connect_to_folder = ''
|
||||
sony_collection_renaming_rules={}
|
||||
sony_collection_name_template='{value}{category:| (|)}'
|
||||
|
||||
|
||||
#: Specify how SONY collections are sorted
|
||||
# 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
|
||||
# be used to sort on a collection-by-collection basis. The format of the tweak
|
||||
@ -230,7 +245,7 @@ sony_collection_name_template='{value}{category:| (|)}'
|
||||
sony_collection_sorting_rules = []
|
||||
|
||||
|
||||
# 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'...] ...}
|
||||
# Example: create the term 'myseries' that when used as myseries:foo would
|
||||
# search all of the search categories 'series', '#myseries', and '#myseries2':
|
||||
@ -243,15 +258,17 @@ sony_collection_sorting_rules = []
|
||||
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
|
||||
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
|
||||
|
||||
# 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_wont_display is a list of custom fields not to be displayed.
|
||||
# wont_display has priority over will_display.
|
||||
@ -269,13 +286,27 @@ max_content_server_tags_shown=5
|
||||
content_server_will_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
|
||||
# fields are not affected
|
||||
book_details_will_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
|
||||
# library after certain operations such as searches or device insertion. Each
|
||||
# sort level adds a performance penalty. If the database is large (thousands of
|
||||
@ -283,16 +314,14 @@ book_details_wont_display = []
|
||||
# level sorts, and if you are seeing a slowdown, reduce the value of this tweak.
|
||||
maximum_resort_levels = 5
|
||||
|
||||
# Absolute path to a TTF font file to use as the font for the title and author
|
||||
# when generating a default cover. Useful if the default font (Liberation
|
||||
#: Specify which font to use when generating a default cover
|
||||
# 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.
|
||||
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
|
||||
|
||||
|
||||
#: Control behavior of double clicks on the book list
|
||||
# Behavior of doubleclick on the books list. Choices: open_viewer, do_nothing,
|
||||
# edit_cell, edit_metadata. Selecting edit_metadata has the side effect of
|
||||
# disabling editing a field using a single click.
|
||||
@ -301,7 +330,8 @@ generate_cover_foot_font = None
|
||||
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
|
||||
# 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.
|
||||
@ -312,12 +342,13 @@ doubleclick_on_library_view = 'open_viewer'
|
||||
# Example: locale_for_sorting = 'nb' -- sort using Norwegian rules.
|
||||
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
|
||||
# metadata one book at a time. If True, then the fields are laid out using two
|
||||
# columns. If False, one column is used.
|
||||
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
|
||||
# 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,
|
||||
@ -325,3 +356,9 @@ metadata_single_use_2_cols_for_custom_fields = True
|
||||
# calibre.
|
||||
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
|
||||
|
||||
|
BIN
resources/images/news/njuz_net.png
Normal file
BIN
resources/images/news/njuz_net.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 914 B |
68
resources/recipes/abc_es.recipe
Normal file
68
resources/recipes/abc_es.recipe
Normal file
@ -0,0 +1,68 @@
|
||||
__license__ = 'GPL v3'
|
||||
__author__ = 'Ricardo Jurado'
|
||||
__copyright__ = 'Ricardo Jurado'
|
||||
__version__ = 'v0.4'
|
||||
__date__ = '11 February 2011'
|
||||
|
||||
'''
|
||||
http://www.abc.es/
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1296604369(BasicNewsRecipe):
|
||||
|
||||
title = u'ABC.es'
|
||||
masthead_url = 'http://www.abc.es/img/logo-abc.gif'
|
||||
cover_url = 'http://www.abc.es/img/logo-abc.gif'
|
||||
publisher = u'Grupo VOCENTO'
|
||||
|
||||
__author__ = 'Ricardo Jurado'
|
||||
description = 'Noticias de Spain y el mundo'
|
||||
category = 'News,Spain,National,International,Economy'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 10
|
||||
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
encoding = 'ISO-8859-1'
|
||||
remove_javascript = True
|
||||
language = 'es'
|
||||
|
||||
extra_css = """
|
||||
p{text-align: justify; font-size: 100%}
|
||||
body{ text-align: left; font-size:100% }
|
||||
h3{font-family: sans-serif; font-size:120%; font-weight:bold; text-align: justify; }
|
||||
h2{font-family: sans-serif; font-size:100%; font-weight:bold; text-align: justify; }
|
||||
h1{font-family: sans-serif; font-size:150%; font-weight:bold; text-align: justify; }
|
||||
"""
|
||||
|
||||
keep_only_tags = [
|
||||
# dict(name='h2', attrs={'class':['logos']}),
|
||||
dict(name='h3', attrs={'class':['overhead']}),
|
||||
dict(name='h1', attrs={'class':'headline'}),
|
||||
dict(name='h3', attrs={'class':['subhead']}),
|
||||
dict(name='div', attrs={'class':'datosi'}),
|
||||
dict(name='div', attrs={'class':'photo-alt1'}),
|
||||
dict(name='div', attrs={'class':'text'})
|
||||
]
|
||||
|
||||
# remove_tags_before = dict(name='div' , attrs={'id':['cabecera2']})
|
||||
|
||||
feeds = [
|
||||
(u'PORTADA', u'http://www.abc.es/rss/feeds/abcPortada.xml')
|
||||
,(u'ULTIMAS', u'http://www.abc.es/rss/feeds/abc_ultima.xml')
|
||||
,(u'NACIONAL', u'http://www.abc.es/rss/feeds/abc_EspanaEspana.xml')
|
||||
,(u'INTERNACIONAL', u'http://www.abc.es/rss/feeds/abc_Internacional.xml')
|
||||
,(u'OPINION', u'http://www.abc.es/rss/feeds/abc_opinioncompleto.xml')
|
||||
,(u'BLOGS ABC', u'http://www.abc.es/rss/feeds/blogs-abc.xml')
|
||||
,(u'ECONOMIA', u'http://www.abc.es/rss/feeds/abc_Economia.xml')
|
||||
,(u'CIENCIA Y TECNOLOGIA', u'http://www.abc.es/rss/feeds/abc_Ciencia_Tecnologia.xml')
|
||||
,(u'CULTURA', u'http://www.abc.es/rss/feeds/abc_Cultura.xml')
|
||||
,(u'LIBROS', u'http://www.abc.es/rss/feeds/abc_Libros.xml')
|
||||
,(u'MEDIOS Y REDES', u'http://www.abc.es/rss/feeds/ABC_Medios_Redes.xml')
|
||||
,(u'EVASION', u'http://www.abc.es/rss/feeds/abc_evasion.xml')
|
||||
,(u'ESPECTACULOS', u'http://www.abc.es/rss/feeds/abc_Espectaculos.xml')
|
||||
,(u'GENTE', u'http://www.abc.es/rss/feeds/abc_Gente.xml')
|
||||
,(u'DEPORTES', u'http://www.abc.es/rss/feeds/abc_Deportes.xml')
|
||||
]
|
@ -1,6 +1,6 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2008-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
b92.net
|
||||
'''
|
||||
@ -10,7 +10,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
||||
class B92(BasicNewsRecipe):
|
||||
title = 'B92'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'B92 info, najnovije vesti iz Srbije, regiona i sveta'
|
||||
description = 'Najnovije vesti iz Srbije, regiona i sveta, aktuelne teme iz sveta politike, ekonomije, drustva, foto galerija, kolumne'
|
||||
publisher = 'B92'
|
||||
category = 'news, politics, Serbia'
|
||||
oldest_article = 2
|
||||
@ -20,34 +20,44 @@ class B92(BasicNewsRecipe):
|
||||
encoding = 'cp1250'
|
||||
language = 'sr'
|
||||
publication_type = 'newsportal'
|
||||
extra_css = ' @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: serif1, serif} '
|
||||
masthead_url = 'http://www.b92.net/images/fp/logo.gif'
|
||||
extra_css = """
|
||||
@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
|
||||
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
|
||||
body{font-family: Arial,Helvetica,sans1,sans-serif}
|
||||
.articledescription{font-family: serif1, serif}
|
||||
.article-info2,.article-info1{text-transform: uppercase; font-size: small}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher': publisher
|
||||
, 'language' : language
|
||||
, 'linearize_tables' : True
|
||||
}
|
||||
|
||||
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
||||
|
||||
keep_only_tags = [dict(name='table', attrs={'class':'maindocument'})]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='ul', attrs={'class':'comment-nav'})
|
||||
,dict(name=['embed','link','base'] )
|
||||
,dict(name='div', attrs={'class':'udokum'} )
|
||||
]
|
||||
keep_only_tags = [dict(attrs={'class':['article-info1','article-text']})]
|
||||
remove_attributes = ['width','height','align','hspace','vspace','border']
|
||||
remove_tags = [dict(name=['embed','link','base','meta'])]
|
||||
|
||||
feeds = [
|
||||
(u'Vesti', u'http://www.b92.net/info/rss/vesti.xml')
|
||||
,(u'Biz' , u'http://www.b92.net/info/rss/biz.xml' )
|
||||
(u'Vesti' , u'http://www.b92.net/info/rss/vesti.xml' )
|
||||
,(u'Biz' , u'http://www.b92.net/info/rss/biz.xml' )
|
||||
,(u'Sport' , u'http://www.b92.net/info/rss/sport.xml' )
|
||||
,(u'Zivot' , u'http://www.b92.net/info/rss/zivot.xml' )
|
||||
,(u'Kultura' , u'http://www.b92.net/info/rss/kultura.xml' )
|
||||
,(u'Automobili' , u'http://www.b92.net/info/rss/automobili.xml')
|
||||
,(u'Tehnopolis' , u'http://www.b92.net/info/rss/tehnopolis.xml')
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
return url + '&version=print'
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for alink in soup.findAll('a'):
|
||||
if alink.string is not None:
|
||||
tstr = alink.string
|
||||
alink.replaceWith(tstr)
|
||||
return soup
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
'''
|
||||
cinebel.be
|
||||
'''
|
||||
@ -14,14 +14,14 @@ class Cinebel(BasicNewsRecipe):
|
||||
description = u'Cinema news from Belgium in French'
|
||||
publisher = u'cinebel.be'
|
||||
category = 'news, cinema, movie, Belgium'
|
||||
oldest_article = 3
|
||||
encoding = 'utf8'
|
||||
language = 'fr_BE'
|
||||
oldest_article = 15
|
||||
language = 'fr'
|
||||
|
||||
max_articles_per_feed = 20
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
timefmt = ' [%d %b %Y]'
|
||||
filterDuplicates = True
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name = 'span', attrs = {'class': 'movieMainTitle'})
|
||||
@ -35,6 +35,13 @@ class Cinebel(BasicNewsRecipe):
|
||||
,(u'Top 10' , u'http://www.cinebel.be/Servlets/RssServlet?languageCode=fr&rssType=2' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for alink in soup.findAll('a'):
|
||||
if alink.has_key('href'):
|
||||
tstr = "Site officiel: " + alink['href']
|
||||
alink.replaceWith(tstr)
|
||||
return soup
|
||||
|
||||
def get_cover_url(self):
|
||||
cover_url = 'http://www.cinebel.be/portal/resources/common/logo_index.gif'
|
||||
return cover_url
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
'''
|
||||
dhnet.be
|
||||
'''
|
||||
@ -16,7 +16,8 @@ class DHNetBe(BasicNewsRecipe):
|
||||
publisher = u'dhnet.be'
|
||||
category = 'news, Belgium'
|
||||
oldest_article = 3
|
||||
language = 'fr_BE'
|
||||
language = 'fr'
|
||||
masthead_url = 'http://www.dhnet.be/images/homepage_logo_dh.gif'
|
||||
|
||||
max_articles_per_feed = 20
|
||||
no_stylesheets = True
|
||||
@ -34,6 +35,13 @@ class DHNetBe(BasicNewsRecipe):
|
||||
,(u'La Une Info' , u'http://www.dhnet.be/rss/dhinfos/' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for alink in soup.findAll('a'):
|
||||
if alink.string is not None:
|
||||
tstr = alink.string
|
||||
alink.replaceWith(tstr)
|
||||
return soup
|
||||
|
||||
def get_cover_url(self):
|
||||
cover_url = strftime('http://pdf-online.dhnet.be/pdfonline/image/%Y%m%d/dh_%Y%m%d_nam_infoge_001.pdf.L.jpg')
|
||||
return cover_url
|
||||
|
@ -5,8 +5,8 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '04 December 2010, desUBIKado'
|
||||
__author__ = 'desUBIKado'
|
||||
__description__ = 'Daily newspaper from Aragon'
|
||||
__version__ = 'v0.05'
|
||||
__date__ = '07, December 2010'
|
||||
__version__ = 'v0.07'
|
||||
__date__ = '06, February 2011'
|
||||
'''
|
||||
elperiodicodearagon.com
|
||||
'''
|
||||
@ -38,22 +38,26 @@ class elperiodicodearagon(BasicNewsRecipe):
|
||||
,'publisher' : publisher
|
||||
}
|
||||
|
||||
feeds = [(u'Arag\xf3n', u'http://elperiodicodearagon.com/RSS/2.xml'),
|
||||
(u'Internacional', u'http://elperiodicodearagon.com/RSS/4.xml'),
|
||||
(u'Espa\xf1a', u'http://elperiodicodearagon.com/RSS/3.xml'),
|
||||
(u'Econom\xeda', u'http://elperiodicodearagon.com/RSS/5.xml'),
|
||||
(u'Deportes', u'http://elperiodicodearagon.com/RSS/7.xml'),
|
||||
(u'Real Zaragoza', u'http://elperiodicodearagon.com/RSS/10.xml'),
|
||||
(u'Opini\xf3n', u'http://elperiodicodearagon.com/RSS/103.xml'),
|
||||
(u'Escenarios', u'http://elperiodicodearagon.com/RSS/105.xml'),
|
||||
(u'Sociedad', u'http://elperiodicodearagon.com/RSS/104.xml'),
|
||||
(u'Gente', u'http://elperiodicodearagon.com/RSS/330.xml')]
|
||||
feeds = [
|
||||
(u'Arag\xf3n', u'http://elperiodicodearagon.com/RSS/2.xml'),
|
||||
(u'Internacional', u'http://elperiodicodearagon.com/RSS/4.xml'),
|
||||
(u'Espa\xf1a', u'http://elperiodicodearagon.com/RSS/3.xml'),
|
||||
(u'Econom\xeda', u'http://elperiodicodearagon.com/RSS/5.xml'),
|
||||
(u'Deportes', u'http://elperiodicodearagon.com/RSS/7.xml'),
|
||||
(u'Real Zaragoza', u'http://elperiodicodearagon.com/RSS/10.xml'),
|
||||
(u'Opini\xf3n', u'http://elperiodicodearagon.com/RSS/103.xml'),
|
||||
(u'Escenarios', u'http://elperiodicodearagon.com/RSS/105.xml'),
|
||||
(u'Sociedad', u'http://elperiodicodearagon.com/RSS/104.xml'),
|
||||
(u'Gente', u'http://elperiodicodearagon.com/RSS/330.xml')
|
||||
]
|
||||
|
||||
|
||||
extra_css = '''
|
||||
h3{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:xx-large;}
|
||||
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
|
||||
dd{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
|
||||
h3 {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:30px;}
|
||||
h2 {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:18px;}
|
||||
h4 {font-family:Arial,Helvetica,sans-serif; font-style:italic; font-weight:normal;font-size:20px;}
|
||||
.columnaDeRecursosRelacionados {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:14px;}
|
||||
img{margin-bottom: 0.4em}
|
||||
'''
|
||||
|
||||
remove_attributes = ['height','width']
|
||||
@ -82,6 +86,7 @@ class elperiodicodearagon(BasicNewsRecipe):
|
||||
dict(name='a', attrs={'class':'AvisoComentario'}),
|
||||
dict(name='div', attrs={'class':'CajaAvisoComentario'}),
|
||||
dict(name='div', attrs={'class':'navegaNoticias'}),
|
||||
dict(name='div', attrs={'class':'Mensaje'}),
|
||||
dict(name='div', attrs={'id':'PaginadorDiCom'}),
|
||||
dict(name='div', attrs={'id':'CajaAccesoCuentaUsuario'}),
|
||||
dict(name='div', attrs={'id':'CintilloComentario'}),
|
||||
@ -107,3 +112,15 @@ class elperiodicodearagon(BasicNewsRecipe):
|
||||
(re.compile(r'<p> </p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
|
||||
(re.compile(r'<p id="">', re.DOTALL|re.IGNORECASE), lambda match: '<p>')
|
||||
]
|
||||
|
||||
# Para sustituir el video incrustado de YouTube por una imagen
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for video_yt in soup.findAll('iframe',{'title':'YouTube video player'}):
|
||||
if video_yt:
|
||||
video_yt.name = 'img'
|
||||
fuente = video_yt['src']
|
||||
fuente2 = fuente.replace('http://www.youtube.com/embed/','http://img.youtube.com/vi/')
|
||||
video_yt['src'] = fuente2 + '/0.jpg'
|
||||
|
||||
return soup
|
||||
|
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'),
|
||||
]
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
'''
|
||||
lalibre.be
|
||||
'''
|
||||
@ -16,18 +16,18 @@ class LaLibre(BasicNewsRecipe):
|
||||
publisher = u'lalibre.be'
|
||||
category = 'news, Belgium'
|
||||
oldest_article = 3
|
||||
language = 'fr_BE'
|
||||
language = 'fr'
|
||||
masthead_url = 'http://www.lalibre.be/img/logoLaLibre.gif'
|
||||
|
||||
max_articles_per_feed = 20
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
timefmt = ' [%d %b %Y]'
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name = 'div', attrs = {'id': 'articleHat'})
|
||||
,dict(name = 'p', attrs = {'id': 'publicationDate'})
|
||||
,dict(name = 'div', attrs = {'id': 'articleText'})
|
||||
]
|
||||
remove_tags_before = dict(name = 'div', attrs = {'class': 'extraMainContent'})
|
||||
remove_tags_after = dict(name = 'div', attrs = {'id': 'articleText'})
|
||||
|
||||
remove_tags = [dict(name = 'div', attrs = {'id': 'strongArticleLinks'})]
|
||||
|
||||
feeds = [
|
||||
(u'L\'actu' , u'http://www.lalibre.be/rss/?section=10' )
|
||||
@ -38,6 +38,13 @@ class LaLibre(BasicNewsRecipe):
|
||||
,(u'Societe' , u'http://www.lalibre.be/rss/?section=12' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for alink in soup.findAll('a'):
|
||||
if alink.string is not None:
|
||||
tstr = alink.string
|
||||
alink.replaceWith(tstr)
|
||||
return soup
|
||||
|
||||
def get_cover_url(self):
|
||||
cover_url = strftime('http://pdf-online.lalibre.be/pdfonline/image/%Y%m%d/llb_%Y%m%d_nam_libre_001.pdf.L.jpg')
|
||||
return cover_url
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
'''
|
||||
lameuse.be
|
||||
'''
|
||||
@ -16,8 +16,8 @@ class LaMeuse(BasicNewsRecipe):
|
||||
publisher = u'lameuse.be'
|
||||
category = 'news, Belgium'
|
||||
oldest_article = 3
|
||||
encoding = 'utf8'
|
||||
language = 'fr_BE'
|
||||
language = 'fr'
|
||||
masthead_url = 'http://www.lameuse.be/images/SPV3/logo_header_LM.gif'
|
||||
|
||||
max_articles_per_feed = 20
|
||||
no_stylesheets = True
|
||||
@ -32,6 +32,11 @@ class LaMeuse(BasicNewsRecipe):
|
||||
dict(name = 'div', attrs = {'class': 'sb-group'})
|
||||
,dict(name = 'div', attrs = {'id': 'share'})
|
||||
,dict(name = 'div', attrs = {'id': 'commentaires'})
|
||||
,dict(name = 'ul', attrs = {'class': 'right liensutiles'})
|
||||
,dict(name = 'ul', attrs = {'class': 'bas liensutiles'})
|
||||
,dict(name = 'p', attrs = {'class': 'ariane'})
|
||||
,dict(name = 'div', attrs = {'class': 'inner-bloc'})
|
||||
,dict(name = 'div', attrs = {'class': 'block-01'})
|
||||
]
|
||||
|
||||
feeds = [
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
'''
|
||||
lavenir.net
|
||||
'''
|
||||
@ -15,8 +15,7 @@ class LAvenir(BasicNewsRecipe):
|
||||
publisher = u'lavenir.net'
|
||||
category = 'news, Belgium'
|
||||
oldest_article = 3
|
||||
encoding = 'utf8'
|
||||
language = 'fr_BE'
|
||||
language = 'fr'
|
||||
|
||||
max_articles_per_feed = 20
|
||||
no_stylesheets = True
|
||||
@ -35,6 +34,13 @@ class LAvenir(BasicNewsRecipe):
|
||||
,(u'Societe' , u'http://www.lavenir.net/rss.aspx?foto=1&intro=1§ion=info&info=12e1a2f4-7e03-4cf1-afec-016869072317' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for alink in soup.findAll('a'):
|
||||
if alink.string is not None:
|
||||
tstr = alink.string
|
||||
alink.replaceWith(tstr)
|
||||
return soup
|
||||
|
||||
def get_cover_url(self):
|
||||
cover_url = 'http://www.lavenir.net/extra/Static/journal/Pdf/1/UNE_Nationale.PDF'
|
||||
return cover_url
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
|
||||
'''
|
||||
lesoir.be
|
||||
'''
|
||||
@ -16,7 +16,8 @@ class LeSoirBe(BasicNewsRecipe):
|
||||
publisher = u'lesoir.be'
|
||||
category = 'news, Belgium'
|
||||
oldest_article = 3
|
||||
language = 'fr_BE'
|
||||
language = 'fr'
|
||||
masthead_url = 'http://pdf.lesoir.be/pdf/images/SOIR//logo.gif'
|
||||
|
||||
max_articles_per_feed = 20
|
||||
no_stylesheets = True
|
||||
|
61
resources/recipes/njuz_net.recipe
Normal file
61
resources/recipes/njuz_net.recipe
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
njuz.net
|
||||
'''
|
||||
import re
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class NjuzNet(BasicNewsRecipe):
|
||||
title = 'Njuz.net'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Iscasene vesti iz Srbije, regiona i sveta'
|
||||
publisher = 'njuz.net'
|
||||
category = 'news, politics, humor, Serbia'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
encoding = 'utf8'
|
||||
language = 'sr'
|
||||
publication_type = 'newsportal'
|
||||
masthead_url = 'http://www.njuz.net/njuznet.jpg'
|
||||
extra_css = """
|
||||
@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
|
||||
body{font-family: serif1, serif}
|
||||
.articledescription{font-family: serif1, serif}
|
||||
.wp-caption-text{font-size: x-small}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
||||
|
||||
keep_only_tags = [
|
||||
dict(attrs={'id':'entryMeta'})
|
||||
,dict(attrs={'class':'post'})
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name=['embed','link','base','iframe','object','meta','fb:like'])
|
||||
,dict(name='div', attrs={'id':'tagsandcats'})
|
||||
]
|
||||
remove_tags_after= dict(name='div', attrs={'id':'tagsandcats'})
|
||||
remove_attributes= ['lang']
|
||||
feeds = [(u'Clanci', u'http://www.njuz.net/feed/')]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for alink in soup.findAll('a'):
|
||||
if alink.string is not None:
|
||||
tstr = alink.string
|
||||
alink.replaceWith(tstr)
|
||||
return soup
|
||||
|
@ -182,6 +182,10 @@ class NYTimes(BasicNewsRecipe):
|
||||
'mediaOverlay slideshow',
|
||||
'headlinesOnly multiline flush',
|
||||
'wideThumb',
|
||||
'video', #added 02-11-2011
|
||||
'videoHeader',#added 02-11-2011
|
||||
'articleInlineVideoHolder', #added 02-11-2011
|
||||
'assetCompanionAd',
|
||||
re.compile('^subNavigation'),
|
||||
re.compile('^leaderboard'),
|
||||
re.compile('^module'),
|
||||
@ -664,7 +668,7 @@ class NYTimes(BasicNewsRecipe):
|
||||
|
||||
try:
|
||||
#remove "Related content" bar
|
||||
runAroundsFound = soup.findAll('div',{'class':['articleInline runaroundLeft','articleInline doubleRule runaroundLeft','articleInline runaroundLeft firstArticleInline']})
|
||||
runAroundsFound = soup.findAll('div',{'class':['articleInline runaroundLeft','articleInline doubleRule runaroundLeft','articleInline runaroundLeft firstArticleInline','articleInline runaroundLeft ']})
|
||||
if runAroundsFound:
|
||||
for runAround in runAroundsFound:
|
||||
#find all section headers
|
||||
@ -672,6 +676,12 @@ class NYTimes(BasicNewsRecipe):
|
||||
if hlines:
|
||||
for hline in hlines:
|
||||
hline.extract()
|
||||
|
||||
#find all section headers
|
||||
hlines = runAround.findAll('h6')
|
||||
if hlines:
|
||||
for hline in hlines:
|
||||
hline.extract()
|
||||
except:
|
||||
self.log("Error removing related content bar")
|
||||
|
||||
|
@ -27,7 +27,7 @@ class TazDigiabo(BasicNewsRecipe):
|
||||
}
|
||||
|
||||
def build_index(self):
|
||||
domain = "http://www.taz.de"
|
||||
domain = "http://dl.taz.de"
|
||||
|
||||
url = domain + "/epub/"
|
||||
|
||||
|
33
resources/recipes/tedneward.recipe
Normal file
33
resources/recipes/tedneward.recipe
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
blogs.tedneward.com
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class InteroperabilityHappens(BasicNewsRecipe):
|
||||
title = 'Interoperability Happens'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Tech blog by Ted Neward'
|
||||
oldest_article = 15
|
||||
max_articles_per_feed = 100
|
||||
language = 'en'
|
||||
encoding = 'utf-8'
|
||||
no_stylesheets = True
|
||||
use_embedded_content = True
|
||||
publication_type = 'blog'
|
||||
extra_css = """
|
||||
body{font-family: Verdana,Arial,Helvetica,sans-serif}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : 'blog, technology, microsoft, programming, C#, Java'
|
||||
, 'publisher': 'Ted Neward'
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
feeds = [(u'Posts', u'http://blogs.tedneward.com/SyndicationService.asmx/GetRss')]
|
||||
|
104
resources/recipes/weblogs_sl.recipe
Normal file
104
resources/recipes/weblogs_sl.recipe
Normal file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '4 February 2011, desUBIKado'
|
||||
__author__ = 'desUBIKado'
|
||||
__version__ = 'v0.05'
|
||||
__date__ = '9, February 2011'
|
||||
'''
|
||||
http://www.weblogssl.com/
|
||||
'''
|
||||
|
||||
import re
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class weblogssl(BasicNewsRecipe):
|
||||
__author__ = 'desUBIKado'
|
||||
description = u'Weblogs colectivos dedicados a seguir la actualidad sobre tecnologia, entretenimiento, estilos de vida, motor, deportes y economia.'
|
||||
title = u'Weblogs SL (Xataka, Genbeta, VidaExtra, Blog de Cine y otros)'
|
||||
publisher = 'Weblogs SL'
|
||||
category = 'Gadgets, Tech news, Product reviews, mobiles, science, cinema, entertainment, culture, tv, food, recipes, life style, motor, F1, sports, economy'
|
||||
language = 'es'
|
||||
timefmt = '[%a, %d %b, %Y]'
|
||||
oldest_article = 1.5
|
||||
max_articles_per_feed = 100
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = False
|
||||
remove_empty_feeds = True
|
||||
remove_javascript = True
|
||||
no_stylesheets = True
|
||||
|
||||
# Si no se quiere recuperar todos los blogs se puede suprimir la descarga del que se desee poniendo
|
||||
# un caracter # por delante, es decir, # (u'Applesfera', u'http://feeds.weblogssl.com/applesfera'),
|
||||
# haría que no se descargase Applesfera. OJO: El último feed no debe llevar la coma al final
|
||||
|
||||
feeds = [
|
||||
(u'Xataka', u'http://feeds.weblogssl.com/xataka2'),
|
||||
(u'Xataka M\xf3vil', u'http://feeds.weblogssl.com/xatakamovil'),
|
||||
(u'Xataka Android', u'http://feeds.weblogssl.com/xatakandroid'),
|
||||
(u'Xataka Foto', u'http://feeds.weblogssl.com/xatakafoto'),
|
||||
(u'Xataka ON', u'http://feeds.weblogssl.com/xatakaon'),
|
||||
(u'Xataka Ciencia', u'http://feeds.weblogssl.com/xatakaciencia'),
|
||||
(u'Genbeta', u'http://feeds.weblogssl.com/genbeta'),
|
||||
(u'Applesfera', u'http://feeds.weblogssl.com/applesfera'),
|
||||
(u'Vida Extra', u'http://feeds.weblogssl.com/vidaextra'),
|
||||
(u'Naci\xf3n Red', u'http://feeds.weblogssl.com/nacionred'),
|
||||
(u'Blog de Cine', u'http://feeds.weblogssl.com/blogdecine'),
|
||||
(u'Vaya tele', u'http://feeds.weblogssl.com/vayatele2'),
|
||||
(u'Hipers\xf3nica', u'http://feeds.weblogssl.com/hipersonica'),
|
||||
(u'Diario del viajero', u'http://feeds.weblogssl.com/diariodelviajero'),
|
||||
(u'Papel en blanco', u'http://feeds.weblogssl.com/papelenblanco'),
|
||||
(u'Pop rosa', u'http://feeds.weblogssl.com/poprosa'),
|
||||
(u'Zona FandoM', u'http://feeds.weblogssl.com/zonafandom'),
|
||||
(u'Fandemia', u'http://feeds.weblogssl.com/fandemia'),
|
||||
(u'Noctamina', u'http://feeds.weblogssl.com/noctamina'),
|
||||
(u'Tendencias', u'http://feeds.weblogssl.com/trendencias'),
|
||||
(u'Beb\xe9s y m\xe1s', u'http://feeds.weblogssl.com/bebesymas'),
|
||||
(u'Directo al paladar', u'http://feeds.weblogssl.com/directoalpaladar'),
|
||||
(u'Compradicci\xf3n', u'http://feeds.weblogssl.com/compradiccion'),
|
||||
(u'Decoesfera', u'http://feeds.weblogssl.com/decoesfera'),
|
||||
(u'Embelezzia', u'http://feeds.weblogssl.com/embelezzia'),
|
||||
(u'Vit\xf3nica', u'http://feeds.weblogssl.com/vitonica'),
|
||||
(u'Ambiente G', u'http://feeds.weblogssl.com/ambienteg'),
|
||||
(u'Arrebatadora', u'http://feeds.weblogssl.com/arrebatadora'),
|
||||
(u'Mensencia', u'http://feeds.weblogssl.com/mensencia'),
|
||||
(u'Peques y m\xe1s', u'http://feeds.weblogssl.com/pequesymas'),
|
||||
(u'Motorpasi\xf3n', u'http://feeds.weblogssl.com/motorpasion'),
|
||||
(u'Motorpasi\xf3n F1', u'http://feeds.weblogssl.com/motorpasionf1'),
|
||||
(u'Motorpasi\xf3n Moto', u'http://feeds.weblogssl.com/motorpasionmoto'),
|
||||
(u'Notas de futbol', u'http://feeds.weblogssl.com/notasdefutbol'),
|
||||
(u'Fuera de l\xedmites', u'http://feeds.weblogssl.com/fueradelimites'),
|
||||
(u'Salir a ganar', u'http://feeds.weblogssl.com/saliraganar'),
|
||||
(u'El blog salm\xf3n', u'http://feeds.weblogssl.com/elblogsalmon2'),
|
||||
(u'Pymes y aut\xf3nomos', u'http://feeds.weblogssl.com/pymesyautonomos'),
|
||||
(u'Tecnolog\xeda Pyme', u'http://feeds.weblogssl.com/tecnologiapyme'),
|
||||
(u'Ahorro diario', u'http://feeds.weblogssl.com/ahorrodiario')
|
||||
]
|
||||
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'id':'infoblock'}),
|
||||
dict(name='div', attrs={'class':'post'}),
|
||||
dict(name='div', attrs={'id':'blog-comments'})
|
||||
]
|
||||
|
||||
remove_tags = [dict(name='div', attrs={'id':'comment-nav'})]
|
||||
|
||||
def print_version(self, url):
|
||||
return url.replace('http://www.', 'http://m.')
|
||||
|
||||
preprocess_regexps = [
|
||||
# Para poner una linea en blanco entre un comentario y el siguiente
|
||||
(re.compile(r'<li id="c', re.DOTALL|re.IGNORECASE), lambda match: '<br><br><li id="c')
|
||||
]
|
||||
|
||||
# Para sustituir el video incrustado de YouTube por una imagen
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for video_yt in soup.findAll('iframe',{'title':'YouTube video player'}):
|
||||
if video_yt:
|
||||
video_yt.name = 'img'
|
||||
fuente = video_yt['src']
|
||||
fuente2 = fuente.replace('http://www.youtube.com/embed/','http://img.youtube.com/vi/')
|
||||
fuente3 = fuente2.replace('?rel=0','')
|
||||
video_yt['src'] = fuente3 + '/0.jpg'
|
||||
|
||||
return soup
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.44'
|
||||
__version__ = '0.7.45'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
@ -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.edge.driver import EDGE
|
||||
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.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
|
||||
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR, \
|
||||
@ -507,7 +507,7 @@ from calibre.devices.kobo.driver import KOBO
|
||||
from calibre.devices.bambook.driver import BAMBOOK
|
||||
|
||||
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
|
||||
LibraryThing
|
||||
KentDistrictLibrary
|
||||
from calibre.ebooks.metadata.douban import DoubanBooks
|
||||
from calibre.ebooks.metadata.nicebooks import NiceBooks, NiceBooksCovers
|
||||
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
|
||||
@ -517,7 +517,7 @@ from calibre.ebooks.epub.fix.unmanifested import Unmanifested
|
||||
from calibre.ebooks.epub.fix.epubcheck import Epubcheck
|
||||
|
||||
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
|
||||
LibraryThing, DoubanBooks, NiceBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
|
||||
KentDistrictLibrary, DoubanBooks, NiceBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
|
||||
Epubcheck, OpenLibraryCovers, LibraryThingCovers, DoubanCovers,
|
||||
NiceBooksCovers]
|
||||
plugins += [
|
||||
@ -605,9 +605,8 @@ plugins += [
|
||||
ELONEX,
|
||||
TECLAST_K3,
|
||||
NEWSMY,
|
||||
PICO, SUNSTECH_EB700, ARCHOS7O,
|
||||
PICO, SUNSTECH_EB700, ARCHOS7O, SOVOS, STASH,
|
||||
IPAPYRUS,
|
||||
SOVOS,
|
||||
EDGE,
|
||||
SNE,
|
||||
ALEX,
|
||||
@ -791,6 +790,17 @@ class Toolbar(PreferencesPlugin):
|
||||
description = _('Customize the toolbars and context menus, changing which'
|
||||
' actions are available in each')
|
||||
|
||||
class Search(PreferencesPlugin):
|
||||
name = 'Search'
|
||||
icon = I('search.png')
|
||||
gui_name = _('Customize searching')
|
||||
category = 'Interface'
|
||||
gui_category = _('Interface')
|
||||
category_order = 1
|
||||
name_order = 5
|
||||
config_widget = 'calibre.gui2.preferences.search'
|
||||
description = _('Customize the way searching for books works in calibre')
|
||||
|
||||
class InputOptions(PreferencesPlugin):
|
||||
name = 'Input Options'
|
||||
icon = I('arrow-down.png')
|
||||
@ -941,7 +951,7 @@ class Misc(PreferencesPlugin):
|
||||
config_widget = 'calibre.gui2.preferences.misc'
|
||||
description = _('Miscellaneous advanced configuration')
|
||||
|
||||
plugins += [LookAndFeel, Behavior, Columns, Toolbar, InputOptions,
|
||||
plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
|
||||
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
|
||||
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions]
|
||||
|
||||
|
@ -62,6 +62,12 @@ class ANDROID(USBMS):
|
||||
# Archos
|
||||
0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216], 0x1422 : [0x0216]},
|
||||
|
||||
# Huawei
|
||||
0x45e : { 0x00e1 : [0x007], },
|
||||
|
||||
# T-Mobile
|
||||
0x0408 : { 0x03ba : [0x0109], },
|
||||
|
||||
}
|
||||
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
|
||||
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
|
||||
@ -71,12 +77,13 @@ class ANDROID(USBMS):
|
||||
|
||||
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
||||
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
||||
'TELECHIP']
|
||||
'TELECHIP', 'HUAWEI', 'T-MOBILE', ]
|
||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
|
||||
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H']
|
||||
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
|
||||
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD']
|
||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||
'A70S', 'A101IT']
|
||||
|
90
src/calibre/devices/kindle/apnx.py
Normal file
90
src/calibre/devices/kindle/apnx.py
Normal file
@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, John Schember <john at nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
Generates and writes an APNX page mapping file.
|
||||
'''
|
||||
|
||||
import struct
|
||||
import uuid
|
||||
|
||||
from calibre.ebooks.pdb.header import PdbHeaderReader
|
||||
|
||||
class APNXBuilder(object):
|
||||
'''
|
||||
2300 characters of uncompressed text per page. This is
|
||||
not meant to map 1 to 1 to a print book but to be a
|
||||
close enough measure.
|
||||
|
||||
A test book was chosen and the characters were counted
|
||||
on one page. This number was round to 2240 then 60
|
||||
characters of markup were added to the total giving
|
||||
2300.
|
||||
|
||||
Uncompressed text length is used because it's easily
|
||||
accessible in MOBI files (part of the header). Also,
|
||||
It's faster to work off of the length then to
|
||||
decompress and parse the actual text.
|
||||
|
||||
A better but much more resource intensive and slower
|
||||
method to calculate the page length would be to parse
|
||||
the uncompressed text. For each paragraph we would
|
||||
want to find how many lines it would occupy in a paper
|
||||
back book. 70 characters per line and 32 lines per page.
|
||||
So divide the number of characters (minus markup) in
|
||||
each paragraph by 70. If there are less than 70
|
||||
characters in the paragraph then it is 1 line. Then,
|
||||
count every 32 lines and mark that location as a page.
|
||||
'''
|
||||
|
||||
def write_apnx(self, mobi_file_path, apnx_path):
|
||||
with open(mobi_file_path, 'rb') as mf:
|
||||
phead = PdbHeaderReader(mf)
|
||||
r0 = phead.section_data(0)
|
||||
text_length = struct.unpack('>I', r0[4:8])[0]
|
||||
|
||||
pages = self.get_pages(text_length)
|
||||
apnx = self.generate_apnx(pages)
|
||||
|
||||
with open(apnx_path, 'wb') as apnxf:
|
||||
apnxf.write(apnx)
|
||||
|
||||
def generate_apnx(self, pages):
|
||||
apnx = ''
|
||||
|
||||
content_vals = {
|
||||
'guid': str(uuid.uuid4()).replace('-', '')[:8],
|
||||
'isbn': '',
|
||||
}
|
||||
|
||||
content_header = '{"contentGuid":"%(guid)s","asin":"%(isbn)s","cdeType":"EBOK","fileRevisionId":"1"}' % content_vals
|
||||
page_header = '{"asin":"%(isbn)s","pageMap":"(1,a,1)"}' % content_vals
|
||||
|
||||
apnx += struct.pack('>I', 65537)
|
||||
apnx += struct.pack('>I', 12 + len(content_header))
|
||||
apnx += struct.pack('>I', len(content_header))
|
||||
apnx += content_header
|
||||
apnx += struct.pack('>H', 1)
|
||||
apnx += struct.pack('>H', len(page_header))
|
||||
apnx += struct.pack('>H', len(pages))
|
||||
apnx += struct.pack('>H', 32)
|
||||
apnx += page_header
|
||||
|
||||
# write page values to apnx
|
||||
for page in pages:
|
||||
apnx += struct.pack('>L', page)
|
||||
|
||||
return apnx
|
||||
|
||||
def get_pages(self, text_length):
|
||||
pages = []
|
||||
count = 0
|
||||
|
||||
while count < text_length:
|
||||
pages.append(count)
|
||||
count += 2300
|
||||
|
||||
return pages
|
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
|
||||
'''
|
||||
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
|
||||
|
||||
'''
|
||||
@ -170,6 +171,8 @@ class KINDLE2(KINDLE):
|
||||
description = _('Communicate with the Kindle 2/3 eBook reader.')
|
||||
|
||||
FORMATS = KINDLE.FORMATS + ['pdf']
|
||||
DELETE_EXTS = KINDLE.DELETE_EXTS + ['.apnx']
|
||||
|
||||
PRODUCT_ID = [0x0002, 0x0004]
|
||||
BCD = [0x0100]
|
||||
|
||||
@ -205,6 +208,23 @@ class KINDLE2(KINDLE):
|
||||
if h in path_map:
|
||||
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):
|
||||
|
||||
name = 'Kindle DX Device Interface'
|
||||
@ -214,310 +234,3 @@ class KINDLE_DX(KINDLE2):
|
||||
PRODUCT_ID = [0x0003]
|
||||
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
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -76,11 +76,11 @@ class E52(USBMS):
|
||||
supported_platforms = ['windows', 'linux', 'osx']
|
||||
|
||||
VENDOR_ID = [0x421]
|
||||
PRODUCT_ID = [0x1CD]
|
||||
PRODUCT_ID = [0x1CD, 0x273]
|
||||
BCD = [0x100]
|
||||
|
||||
|
||||
FORMATS = ['mobi', 'prc']
|
||||
FORMATS = ['epub', 'fb2', 'mobi', 'prc', 'txt']
|
||||
|
||||
EBOOK_DIR_MAIN = 'eBooks'
|
||||
SUPPORTS_SUB_DIRS = True
|
||||
|
@ -92,3 +92,15 @@ class SUNSTECH_EB700(TECLAST_K3):
|
||||
VENDOR_NAME = 'SUNEB700'
|
||||
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'
|
||||
|
||||
|
@ -232,16 +232,37 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
|
||||
time.sleep(5)
|
||||
drives = {}
|
||||
seen = set()
|
||||
prod_pat = re.compile(r'PROD_(.+?)&')
|
||||
dup_prod_id = False
|
||||
|
||||
def check_for_dups(pnp_id):
|
||||
try:
|
||||
match = prod_pat.search(pnp_id)
|
||||
if match is not None:
|
||||
prodid = match.group(1)
|
||||
if prodid in seen:
|
||||
return True
|
||||
else:
|
||||
seen.add(prodid)
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
for drive, pnp_id in win_pnp_drives().items():
|
||||
if self.windows_match_device(pnp_id, 'WINDOWS_CARD_A_MEM') and \
|
||||
not drives.get('carda', False):
|
||||
drives['carda'] = drive
|
||||
dup_prod_id |= check_for_dups(pnp_id)
|
||||
elif self.windows_match_device(pnp_id, 'WINDOWS_CARD_B_MEM') and \
|
||||
not drives.get('cardb', False):
|
||||
drives['cardb'] = drive
|
||||
dup_prod_id |= check_for_dups(pnp_id)
|
||||
elif self.windows_match_device(pnp_id, 'WINDOWS_MAIN_MEM') and \
|
||||
not drives.get('main', False):
|
||||
drives['main'] = drive
|
||||
dup_prod_id |= check_for_dups(pnp_id)
|
||||
|
||||
if 'main' in drives.keys() and 'carda' in drives.keys() and \
|
||||
'cardb' in drives.keys():
|
||||
@ -263,7 +284,8 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
|
||||
# Sort drives by their PNP drive numbers if the CARD and MAIN
|
||||
# MEM strings are identical
|
||||
if self.WINDOWS_MAIN_MEM in (self.WINDOWS_CARD_A_MEM,
|
||||
if dup_prod_id or \
|
||||
self.WINDOWS_MAIN_MEM in (self.WINDOWS_CARD_A_MEM,
|
||||
self.WINDOWS_CARD_B_MEM) or \
|
||||
self.WINDOWS_CARD_A_MEM == self.WINDOWS_CARD_B_MEM:
|
||||
letters = sorted(drives.values(), cmp=drivecmp)
|
||||
|
@ -11,6 +11,7 @@ from calibre.ebooks.conversion.preprocess import DocAnalysis, Dehyphenator
|
||||
from calibre.utils.logging import default_log
|
||||
from calibre.utils.wordcount import get_wordcount_obj
|
||||
|
||||
|
||||
class HeuristicProcessor(object):
|
||||
|
||||
def __init__(self, extra_opts=None, log=None):
|
||||
@ -40,6 +41,9 @@ class HeuristicProcessor(object):
|
||||
def is_pdftohtml(self, src):
|
||||
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
|
||||
|
||||
def is_abbyy(self, src):
|
||||
return '<meta name="generator" content="ABBYY FineReader' in src[:1000]
|
||||
|
||||
def chapter_head(self, match):
|
||||
from calibre.utils.html2text import html2text
|
||||
chap = match.group('chap')
|
||||
@ -519,6 +523,111 @@ class HeuristicProcessor(object):
|
||||
|
||||
return scene_break
|
||||
|
||||
def abbyy_processor(self, html):
|
||||
abbyy_line = re.compile('((?P<linestart><p\sstyle="(?P<styles>[^\"]*?);?">)(?P<content>.*?)(?P<lineend></p>)|(?P<image><img[^>]*>))', re.IGNORECASE)
|
||||
empty_paragraph = '\n<p> </p>\n'
|
||||
self.in_blockquote = False
|
||||
self.previous_was_paragraph = False
|
||||
html = re.sub('</?a[^>]*>', '', html)
|
||||
|
||||
def check_paragraph(content):
|
||||
content = re.sub('\s*</?span[^>]*>\s*', '', content)
|
||||
if re.match('.*[\"\'.!?:]$', content):
|
||||
#print "detected this as a paragraph"
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def convert_styles(match):
|
||||
#print "raw styles are: "+match.group('styles')
|
||||
content = match.group('content')
|
||||
#print "raw content is: "+match.group('content')
|
||||
image = match.group('image')
|
||||
|
||||
is_paragraph = False
|
||||
text_align = ''
|
||||
text_indent = ''
|
||||
paragraph_before = ''
|
||||
paragraph_after = ''
|
||||
blockquote_open = '\n<blockquote>\n'
|
||||
blockquote_close = '</blockquote>\n'
|
||||
indented_text = 'text-indent:3%;'
|
||||
blockquote_open_loop = ''
|
||||
blockquote_close_loop = ''
|
||||
debugabby = False
|
||||
|
||||
if image:
|
||||
debugabby = True
|
||||
if self.in_blockquote:
|
||||
self.in_blockquote = False
|
||||
blockquote_close_loop = blockquote_close
|
||||
self.previous_was_paragraph = False
|
||||
return blockquote_close_loop+'\n'+image+'\n'
|
||||
else:
|
||||
styles = match.group('styles').split(';')
|
||||
is_paragraph = check_paragraph(content)
|
||||
#print "styles for this line are: "+str(styles)
|
||||
split_styles = []
|
||||
for style in styles:
|
||||
#print "style is: "+str(style)
|
||||
newstyle = style.split(':')
|
||||
#print "newstyle is: "+str(newstyle)
|
||||
split_styles.append(newstyle)
|
||||
styles = split_styles
|
||||
for style, setting in styles:
|
||||
if style == 'text-align' and setting != 'left':
|
||||
text_align = style+':'+setting+';'
|
||||
if style == 'text-indent':
|
||||
setting = int(re.sub('\s*pt\s*', '', setting))
|
||||
if 9 < setting < 14:
|
||||
text_indent = indented_text
|
||||
else:
|
||||
text_indent = style+':'+str(setting)+'pt;'
|
||||
if style == 'padding':
|
||||
setting = re.sub('pt', '', setting).split(' ')
|
||||
if int(setting[1]) < 16 and int(setting[3]) < 16:
|
||||
if self.in_blockquote:
|
||||
debugabby = True
|
||||
if is_paragraph:
|
||||
self.in_blockquote = False
|
||||
blockquote_close_loop = blockquote_close
|
||||
if int(setting[3]) > 8 and text_indent == '':
|
||||
text_indent = indented_text
|
||||
if int(setting[0]) > 5:
|
||||
paragraph_before = empty_paragraph
|
||||
if int(setting[2]) > 5:
|
||||
paragraph_after = empty_paragraph
|
||||
elif not self.in_blockquote and self.previous_was_paragraph:
|
||||
debugabby = True
|
||||
self.in_blockquote = True
|
||||
blockquote_open_loop = blockquote_open
|
||||
if debugabby:
|
||||
self.log.debug('\n\n******\n')
|
||||
self.log.debug('padding top is: '+str(setting[0]))
|
||||
self.log.debug('padding right is:'
|
||||
+str(setting[1]))
|
||||
self.log.debug('padding bottom is: ' +
|
||||
str(setting[2]))
|
||||
self.log.debug('padding left is: '
|
||||
+str(setting[3]))
|
||||
|
||||
#print "text-align is: "+str(text_align)
|
||||
#print "\n***\nline is:\n "+str(match.group(0))+'\n'
|
||||
if debugabby:
|
||||
#print "this line is a paragraph = "+str(is_paragraph)+", previous line was "+str(self.previous_was_paragraph)
|
||||
self.log.debug("styles for this line were:", styles)
|
||||
self.log.debug('newline is:')
|
||||
self.log.debug(blockquote_open_loop+blockquote_close_loop+
|
||||
paragraph_before+'<p style="'+text_indent+text_align+
|
||||
'">'+content+'</p>'+paragraph_after+'\n\n\n\n\n')
|
||||
#print "is_paragraph is "+str(is_paragraph)+", previous_was_paragraph is "+str(self.previous_was_paragraph)
|
||||
self.previous_was_paragraph = is_paragraph
|
||||
#print "previous_was_paragraph is now set to "+str(self.previous_was_paragraph)+"\n\n\n"
|
||||
return blockquote_open_loop+blockquote_close_loop+paragraph_before+'<p style="'+text_indent+text_align+'">'+content+'</p>'+paragraph_after
|
||||
|
||||
html = abbyy_line.sub(convert_styles, html)
|
||||
return html
|
||||
|
||||
|
||||
def __call__(self, html):
|
||||
self.log.debug("********* Heuristic processing HTML *********")
|
||||
@ -533,6 +642,10 @@ class HeuristicProcessor(object):
|
||||
self.log.warn("flow is too short, not running heuristics")
|
||||
return html
|
||||
|
||||
is_abbyy = self.is_abbyy(html)
|
||||
if is_abbyy:
|
||||
html = self.abbyy_processor(html)
|
||||
|
||||
# Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly
|
||||
html = self.arrange_htm_line_endings(html)
|
||||
#self.dump(html, 'after_arrange_line_endings')
|
||||
|
@ -216,21 +216,22 @@ class EPUBOutput(OutputFormatPlugin):
|
||||
encryption = self.encrypt_fonts(encrypted_fonts, tdir, uuid)
|
||||
|
||||
from calibre.ebooks.epub import initialize_container
|
||||
epub = initialize_container(output_path, os.path.basename(opf),
|
||||
extra_entries=extra_entries)
|
||||
epub.add_dir(tdir)
|
||||
if encryption is not None:
|
||||
epub.writestr('META-INF/encryption.xml', encryption)
|
||||
if metadata_xml is not None:
|
||||
epub.writestr('META-INF/metadata.xml',
|
||||
metadata_xml.encode('utf-8'))
|
||||
with initialize_container(output_path, os.path.basename(opf),
|
||||
extra_entries=extra_entries) as epub:
|
||||
epub.add_dir(tdir)
|
||||
if encryption is not None:
|
||||
epub.writestr('META-INF/encryption.xml', encryption)
|
||||
if metadata_xml is not None:
|
||||
epub.writestr('META-INF/metadata.xml',
|
||||
metadata_xml.encode('utf-8'))
|
||||
if opts.extract_to is not None:
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
if os.path.exists(opts.extract_to):
|
||||
shutil.rmtree(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)
|
||||
epub.close()
|
||||
|
||||
def encrypt_fonts(self, uris, tdir, uuid): # {{{
|
||||
from binascii import unhexlify
|
||||
|
@ -129,10 +129,7 @@ class Metadata(object):
|
||||
val = NULL_VALUES.get(field, None)
|
||||
_data[field] = val
|
||||
elif field in _data['user_metadata'].iterkeys():
|
||||
if _data['user_metadata'][field]['datatype'] == 'composite':
|
||||
_data['user_metadata'][field]['#value#'] = None
|
||||
else:
|
||||
_data['user_metadata'][field]['#value#'] = val
|
||||
_data['user_metadata'][field]['#value#'] = val
|
||||
_data['user_metadata'][field]['#extra#'] = extra
|
||||
else:
|
||||
# You are allowed to stick arbitrary attributes onto this object as
|
||||
|
@ -247,30 +247,24 @@ class Amazon(MetadataSource): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class LibraryThing(MetadataSource): # {{{
|
||||
class KentDistrictLibrary(MetadataSource): # {{{
|
||||
|
||||
name = 'LibraryThing'
|
||||
name = 'Kent District Library'
|
||||
metadata_type = 'social'
|
||||
description = _('Downloads series/covers/rating information from librarything.com')
|
||||
description = _('Downloads series information from ww2.kdl.org')
|
||||
|
||||
def fetch(self):
|
||||
if not self.isbn or not self.site_customization:
|
||||
if not self.title or not self.book_author:
|
||||
return
|
||||
from calibre.ebooks.metadata.library_thing import get_social_metadata
|
||||
un, _, pw = self.site_customization.partition(':')
|
||||
from calibre.ebooks.metadata.kdl import get_series
|
||||
try:
|
||||
self.results = get_social_metadata(self.title, self.book_author,
|
||||
self.publisher, self.isbn, username=un, password=pw)
|
||||
self.results = get_series(self.title, self.book_author)
|
||||
except Exception, e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.exception = e
|
||||
self.tb = traceback.format_exc()
|
||||
|
||||
@property
|
||||
def string_customization_help(self):
|
||||
ans = _('To use librarything.com you must sign up for a %sfree account%s '
|
||||
'and enter your username and password separated by a : below.')
|
||||
return '<p>'+ans%('<a href="http://www.librarything.com">', '</a>')
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
|
79
src/calibre/ebooks/metadata/kdl.py
Normal file
79
src/calibre/ebooks/metadata/kdl.py
Normal file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re, urllib, urlparse
|
||||
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
from calibre import browser
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
|
||||
URL = \
|
||||
"http://ww2.kdl.org/libcat/WhatsNext.asp?AuthorLastName={0}&AuthorFirstName=&SeriesName=&BookTitle={1}&CategoryID=0&cmdSearch=Search&Search=1&grouping="
|
||||
|
||||
_ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033])
|
||||
|
||||
def get_series(title, authors):
|
||||
mi = Metadata(title, authors)
|
||||
if title and title[0] in _ignore_starts:
|
||||
title = title[1:]
|
||||
title = re.sub(r'^(A|The|An)\s+', '', title).strip()
|
||||
if not title:
|
||||
return mi
|
||||
if isinstance(title, unicode):
|
||||
title = title.encode('utf-8')
|
||||
|
||||
title = urllib.quote_plus(title)
|
||||
|
||||
author = authors[0].strip()
|
||||
if not author:
|
||||
return mi
|
||||
if ',' in author:
|
||||
author = author.split(',')[0]
|
||||
else:
|
||||
author = author.split()[-1]
|
||||
|
||||
url = URL.format(author, title)
|
||||
br = browser()
|
||||
raw = br.open(url).read()
|
||||
if 'see the full results' not in raw:
|
||||
return mi
|
||||
raw = xml_to_unicode(raw)[0]
|
||||
soup = BeautifulSoup(raw)
|
||||
searcharea = soup.find('div', attrs={'class':'searcharea'})
|
||||
if searcharea is None:
|
||||
return mi
|
||||
ss = searcharea.find('div', attrs={'class':'seriessearch'})
|
||||
if ss is None:
|
||||
return mi
|
||||
a = ss.find('a', href=True)
|
||||
if a is None:
|
||||
return mi
|
||||
href = a['href'].partition('?')[-1]
|
||||
data = urlparse.parse_qs(href)
|
||||
series = data.get('SeriesName', [])
|
||||
if not series:
|
||||
return mi
|
||||
series = series[0]
|
||||
series = re.sub(r' series$', '', series).strip()
|
||||
if series:
|
||||
mi.series = series
|
||||
ns = ss.nextSibling
|
||||
if ns.contents:
|
||||
raw = unicode(ns.contents[0])
|
||||
raw = raw.partition('.')[0].strip()
|
||||
try:
|
||||
mi.series_index = int(raw)
|
||||
except:
|
||||
pass
|
||||
return mi
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
print get_series(sys.argv[-2], [sys.argv[-1]])
|
||||
|
@ -39,6 +39,13 @@ def asfloat(value):
|
||||
return 0.0
|
||||
return float(value)
|
||||
|
||||
def isspace(text):
|
||||
if not text:
|
||||
return True
|
||||
if u'\xa0' in text:
|
||||
return False
|
||||
return text.isspace()
|
||||
|
||||
class BlockState(object):
|
||||
def __init__(self, body):
|
||||
self.body = body
|
||||
@ -438,7 +445,7 @@ class MobiMLizer(object):
|
||||
if elem.text:
|
||||
if istate.preserve:
|
||||
text = elem.text
|
||||
elif len(elem) > 0 and elem.text.isspace():
|
||||
elif len(elem) > 0 and isspace(elem.text):
|
||||
text = None
|
||||
else:
|
||||
text = COLLAPSE.sub(' ', elem.text)
|
||||
@ -481,7 +488,7 @@ class MobiMLizer(object):
|
||||
if child.tail:
|
||||
if istate.preserve:
|
||||
tail = child.tail
|
||||
elif bstate.para is None and child.tail.isspace():
|
||||
elif bstate.para is None and isspace(child.tail):
|
||||
tail = None
|
||||
else:
|
||||
tail = COLLAPSE.sub(' ', child.tail)
|
||||
|
@ -103,6 +103,8 @@ class OEBReader(object):
|
||||
data = self.oeb.container.read(None)
|
||||
data = self.oeb.decode(data)
|
||||
data = XMLDECL_RE.sub('', data)
|
||||
data = data.replace('http://openebook.org/namespaces/oeb-package/1.0',
|
||||
OPF1_NS)
|
||||
try:
|
||||
opf = etree.fromstring(data)
|
||||
except etree.XMLSyntaxError:
|
||||
|
@ -56,7 +56,7 @@ def add_options(parser):
|
||||
group = OptionGroup(parser, _('Merge Options:'), _('Options to control the transformation of pdf'))
|
||||
parser.add_option_group(group)
|
||||
add_option = group.add_option
|
||||
|
||||
|
||||
for rec in OPTIONS:
|
||||
option_recommendation_to_cli_option(add_option, rec)
|
||||
|
||||
@ -82,15 +82,15 @@ def main(args=sys.argv, name=''):
|
||||
log = Log()
|
||||
parser = option_parser(name)
|
||||
add_options(parser)
|
||||
|
||||
|
||||
opts, args = parser.parse_args(args)
|
||||
args = args[1:]
|
||||
|
||||
|
||||
if len(args) < 2:
|
||||
print 'Error: Two or more PDF files are required.\n'
|
||||
print_help(parser, log)
|
||||
return 1
|
||||
|
||||
|
||||
bad_pdfs = is_valid_pdfs(args)
|
||||
if bad_pdfs != []:
|
||||
for pdf in bad_pdfs:
|
||||
@ -104,7 +104,7 @@ def main(args=sys.argv, name=''):
|
||||
print 'Error: file `%s` is encrypted.' % pdf
|
||||
if enc:
|
||||
return 1
|
||||
|
||||
|
||||
mi = metadata_from_formats([args[0]])
|
||||
|
||||
merge_files(args, opts.output, mi)
|
||||
|
@ -70,7 +70,7 @@ class PML_HTMLizer(object):
|
||||
'c': ('<div style="text-align: center; margin: auto;">', '</div>'),
|
||||
'r': ('<div style="text-align: right;">', '</div>'),
|
||||
't': ('<div style="margin-left: 5%;">', '</div>'),
|
||||
'T': ('<div style="margin-left: %s;">', '</div>'),
|
||||
'T': ('<div style="text-indent: %s;">', '</div>'),
|
||||
'i': ('<span style="font-style: italic;">', '</span>'),
|
||||
'u': ('<span style="text-decoration: underline;">', '</span>'),
|
||||
'd': ('<span style="text-decoration: line-through;">', '</span>'),
|
||||
@ -499,7 +499,13 @@ class PML_HTMLizer(object):
|
||||
self.toc = []
|
||||
self.file_name = file_name
|
||||
|
||||
indent_state = {'t': False, 'T': False}
|
||||
# t: Are we in an open \t tag set?
|
||||
# T: Are we in an open \T?
|
||||
# st: Did the \t start the line?
|
||||
# sT: Did the \T start the line?
|
||||
# et: Did the \t end the line?
|
||||
indent_state = {'t': False, 'T': False, 'st': False, 'sT': False, 'et': False}
|
||||
basic_indent = False
|
||||
adv_indent_val = ''
|
||||
# Keep track of the number of empty lines
|
||||
# between paragraphs. When we reach a set number
|
||||
@ -512,8 +518,26 @@ class PML_HTMLizer(object):
|
||||
for line in pml.splitlines():
|
||||
parsed = []
|
||||
empty = True
|
||||
|
||||
basic_indent = indent_state['t']
|
||||
adv_indent = indent_state['T']
|
||||
indent_state['T'] = False
|
||||
# Determine if the \t starts the line or if we are
|
||||
# in an open \t block.
|
||||
if line.lstrip().startswith('\\t') or basic_indent:
|
||||
basic_indent = True
|
||||
indent_state['st'] = True
|
||||
else:
|
||||
indent_state['st'] = False
|
||||
# Determine if the \T starts the line.
|
||||
if line.lstrip().startswith('\\T'):
|
||||
indent_state['sT'] = True
|
||||
else:
|
||||
indent_state['sT'] = False
|
||||
# Determine if the \t ends the line.
|
||||
if line.rstrip().endswith('\\t'):
|
||||
indent_state['et'] = True
|
||||
else:
|
||||
indent_state['et'] = False
|
||||
|
||||
# Must use StringIO, cStringIO does not support unicode
|
||||
line = StringIO.StringIO(line)
|
||||
@ -575,13 +599,10 @@ class PML_HTMLizer(object):
|
||||
empty = False
|
||||
text = '<hr width="%s" />' % self.code_value(line)
|
||||
elif c == 't':
|
||||
indent_state[c] = not indent_state[c]
|
||||
if indent_state[c]:
|
||||
basic_indent = True
|
||||
indent_state['t'] = not indent_state['t']
|
||||
elif c == 'T':
|
||||
# Ensure we only store the value on the first T set for the line.
|
||||
if not indent_state['T']:
|
||||
adv_indent = True
|
||||
adv_indent_val = self.code_value(line)
|
||||
else:
|
||||
# We detected a T previously on this line.
|
||||
@ -610,10 +631,23 @@ class PML_HTMLizer(object):
|
||||
text = self.end_line()
|
||||
parsed.append(text)
|
||||
|
||||
# Basic indent will be set if the \t starts the line or
|
||||
# if we are in a continuing \t block.
|
||||
if basic_indent:
|
||||
parsed.insert(0, self.STATES_TAGS['t'][0])
|
||||
parsed.append(self.STATES_TAGS['t'][1])
|
||||
elif adv_indent:
|
||||
# if the \t started the line and either it ended the line or the \t
|
||||
# block is still open use a left margin.
|
||||
if indent_state['st'] and (indent_state['et'] or indent_state['t']):
|
||||
parsed.insert(0, self.STATES_TAGS['t'][0])
|
||||
parsed.append(self.STATES_TAGS['t'][1])
|
||||
# Use a text indent instead of a margin.
|
||||
# This handles cases such as:
|
||||
# \tO\tne upon a time...
|
||||
else:
|
||||
parsed.insert(0, self.STATES_TAGS['T'][0] % '5%')
|
||||
parsed.append(self.STATES_TAGS['T'][1])
|
||||
# \t will override \T's on the line.
|
||||
# We only handle \T's that started the line.
|
||||
elif indent_state['T'] and indent_state['sT']:
|
||||
parsed.insert(0, self.STATES_TAGS['T'][0] % adv_indent_val)
|
||||
parsed.append(self.STATES_TAGS['T'][1])
|
||||
indent_state['T'] = False
|
||||
|
@ -41,7 +41,6 @@ class TextileMLizer(object):
|
||||
html = re.sub(r'<\s*img[^>]*>', '', html)
|
||||
|
||||
text = html2textile(html)
|
||||
text = text.replace('%', '')
|
||||
|
||||
# Ensure the section ends with at least two new line characters.
|
||||
# This is to prevent the last paragraph from a section being
|
||||
|
@ -106,9 +106,13 @@ def _config():
|
||||
'clicked'))
|
||||
c.add_opt('asked_library_thing_password', default=False,
|
||||
help='Asked library thing password at least once.')
|
||||
c.add_opt('search_as_you_type', default=True,
|
||||
help='Start searching as you type. If this is disabled then search will '
|
||||
'only take place when the Enter or Return key is pressed.')
|
||||
c.add_opt('search_as_you_type', default=False,
|
||||
help=_('Start searching as you type. If this is disabled then search will '
|
||||
'only take place when the Enter or Return key is pressed.'))
|
||||
c.add_opt('highlight_search_matches', default=False,
|
||||
help=_('When searching, show all books with search results '
|
||||
'highlighted instead of showing only the matches. You can use the '
|
||||
'N or F3 keys to go to the next match.'))
|
||||
c.add_opt('save_to_disk_template_history', default=[],
|
||||
help='Previously used Save to Disk templates')
|
||||
c.add_opt('send_to_device_template_history', default=[],
|
||||
@ -133,14 +137,18 @@ def _config():
|
||||
help=_('Automatically download the cover, if available'))
|
||||
c.add_opt('enforce_cpu_limit', default=True,
|
||||
help=_('Limit max simultaneous jobs to number of CPUs'))
|
||||
c.add_opt('tag_browser_hidden_categories', default=set(),
|
||||
help=_('tag browser categories not to display'))
|
||||
c.add_opt('gui_layout', choices=['wide', 'narrow'],
|
||||
help=_('The layout of the user interface'), default='wide')
|
||||
c.add_opt('show_avg_rating', default=True,
|
||||
help=_('Show the average rating per item indication in the tag browser'))
|
||||
c.add_opt('disable_animations', default=False,
|
||||
help=_('Disable UI animations'))
|
||||
|
||||
# This option is no longer used. It remains for compatibility with upgrades
|
||||
# so the value can be migrated
|
||||
c.add_opt('tag_browser_hidden_categories', default=set(),
|
||||
help=_('tag browser categories not to display'))
|
||||
|
||||
c.add_opt
|
||||
return ConfigProxy(c)
|
||||
|
||||
|
@ -237,6 +237,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
return
|
||||
self.stats.rename(location, newloc)
|
||||
self.build_menus()
|
||||
self.gui.iactions['Copy To Library'].build_menus()
|
||||
|
||||
def delete_requested(self, name, location):
|
||||
loc = location.replace('/', os.sep)
|
||||
@ -253,6 +254,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
pass
|
||||
self.stats.remove(location)
|
||||
self.build_menus()
|
||||
self.gui.iactions['Copy To Library'].build_menus()
|
||||
|
||||
def backup_status(self, location):
|
||||
dirty_text = 'no'
|
||||
@ -329,6 +331,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
' libraries.')%loc, show=True)
|
||||
self.stats.remove(location)
|
||||
self.build_menus()
|
||||
self.gui.iactions['Copy To Library'].build_menus()
|
||||
return
|
||||
|
||||
prefs['library_path'] = loc
|
||||
@ -371,9 +374,20 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
if not self.change_library_allowed():
|
||||
return
|
||||
from calibre.gui2.dialogs.choose_library import ChooseLibrary
|
||||
self.gui.library_view.save_state()
|
||||
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_()
|
||||
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):
|
||||
if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
|
||||
|
@ -28,21 +28,12 @@ class NextMatchAction(InterfaceAction):
|
||||
self.gui.addAction(self.p_action)
|
||||
self.p_action.triggered.connect(self.move_backward)
|
||||
|
||||
def gui_layout_complete(self):
|
||||
self.gui.search_highlight_only.setVisible(True)
|
||||
|
||||
def location_selected(self, loc):
|
||||
self.can_move = loc == 'library'
|
||||
try:
|
||||
self.gui.search_highlight_only.setVisible(self.can_move)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def move_forward(self):
|
||||
if self.can_move is None:
|
||||
self.can_move = self.gui.current_view() is self.gui.library_view
|
||||
self.gui.search_highlight_only.setVisible(self.can_move)
|
||||
|
||||
if self.can_move:
|
||||
self.gui.current_view().move_highlighted_row(forward=True)
|
||||
@ -50,7 +41,6 @@ class NextMatchAction(InterfaceAction):
|
||||
def move_backward(self):
|
||||
if self.can_move is None:
|
||||
self.can_move = self.gui.current_view() is self.gui.library_view
|
||||
self.gui.search_highlight_only.setVisible(self.can_move)
|
||||
|
||||
if self.can_move:
|
||||
self.gui.current_view().move_highlighted_row(forward=False)
|
||||
|
@ -33,7 +33,8 @@ class PreferencesAction(InterfaceAction):
|
||||
x.triggered.connect(self.do_config)
|
||||
|
||||
|
||||
def do_config(self, checked=False, initial_plugin=None):
|
||||
def do_config(self, checked=False, initial_plugin=None,
|
||||
close_after_initial=False):
|
||||
if self.gui.job_manager.has_jobs():
|
||||
d = error_dialog(self.gui, _('Cannot configure'),
|
||||
_('Cannot configure while there are running jobs.'))
|
||||
@ -44,7 +45,8 @@ class PreferencesAction(InterfaceAction):
|
||||
_('Cannot configure before calibre is restarted.'))
|
||||
d.exec_()
|
||||
return
|
||||
d = Preferences(self.gui, initial_plugin=initial_plugin)
|
||||
d = Preferences(self.gui, initial_plugin=initial_plugin,
|
||||
close_after_initial=close_after_initial)
|
||||
d.show()
|
||||
d.run_wizard_requested.connect(self.gui.run_wizard,
|
||||
type=Qt.QueuedConnection)
|
||||
|
@ -9,10 +9,9 @@ __docformat__ = 'restructuredtext en'
|
||||
from PyQt4.Qt import QLineEdit, QAbstractListModel, Qt, \
|
||||
QApplication, QCompleter
|
||||
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.utils.icu import sort_key, lower
|
||||
from calibre.gui2 import NONE
|
||||
from calibre.gui2.widgets import EnComboBox
|
||||
from calibre.gui2.widgets import EnComboBox, LineEditECM
|
||||
|
||||
class CompleteModel(QAbstractListModel):
|
||||
|
||||
@ -39,7 +38,7 @@ class CompleteModel(QAbstractListModel):
|
||||
return NONE
|
||||
|
||||
|
||||
class MultiCompleteLineEdit(QLineEdit):
|
||||
class MultiCompleteLineEdit(QLineEdit, LineEditECM):
|
||||
'''
|
||||
A line edit that completes on multiple items separated by a
|
||||
separator. Use the :meth:`update_items_cache` to set the list of
|
||||
@ -55,6 +54,8 @@ class MultiCompleteLineEdit(QLineEdit):
|
||||
|
||||
self.sep = ','
|
||||
self.space_before_sep = False
|
||||
self.add_separator = True
|
||||
self.original_cursor_pos = None
|
||||
|
||||
self._model = CompleteModel(parent=self)
|
||||
self._completer = c = QCompleter(self._model, self)
|
||||
@ -82,6 +83,9 @@ class MultiCompleteLineEdit(QLineEdit):
|
||||
def set_space_before_sep(self, space_before):
|
||||
self.space_before_sep = space_before
|
||||
|
||||
def set_add_separator(self, what):
|
||||
self.add_separator = bool(what)
|
||||
|
||||
# }}}
|
||||
|
||||
def item_entered(self, idx):
|
||||
@ -93,7 +97,7 @@ class MultiCompleteLineEdit(QLineEdit):
|
||||
|
||||
def update_completions(self):
|
||||
' Update the list of completions '
|
||||
cpos = self.cursorPosition()
|
||||
self.original_cursor_pos = cpos = self.cursorPosition()
|
||||
text = unicode(self.text())
|
||||
prefix = text[:cpos]
|
||||
self.current_prefix = prefix
|
||||
@ -103,38 +107,38 @@ class MultiCompleteLineEdit(QLineEdit):
|
||||
self._completer.setCompletionPrefix(complete_prefix)
|
||||
|
||||
def get_completed_text(self, text):
|
||||
'''
|
||||
Get completed text from current cursor position and the completion
|
||||
text
|
||||
'''
|
||||
'Get completed text in before and after parts'
|
||||
if self.sep is None:
|
||||
return -1, text
|
||||
return text, ''
|
||||
else:
|
||||
cursor_pos = self.cursorPosition()
|
||||
before_text = unicode(self.text())[:cursor_pos]
|
||||
after_text = unicode(self.text())[cursor_pos:]
|
||||
prefix_len = len(before_text.split(self.sep)[-1].lstrip())
|
||||
if tweaks['completer_append_separator']:
|
||||
prefix_len = len(before_text.split(self.sep)[-1].lstrip())
|
||||
completed_text = before_text[:cursor_pos - prefix_len] + text + self.sep + ' ' + after_text
|
||||
prefix_len = prefix_len - len(self.sep) - 1
|
||||
if prefix_len < 0:
|
||||
prefix_len = 0
|
||||
cursor_pos = self.original_cursor_pos
|
||||
if cursor_pos is None:
|
||||
cursor_pos = self.cursorPosition()
|
||||
self.original_cursor_pos = None
|
||||
# Split text
|
||||
curtext = unicode(self.text())
|
||||
before_text = curtext[:cursor_pos]
|
||||
after_text = curtext[cursor_pos:].rstrip()
|
||||
# Remove the completion prefix from the before text
|
||||
before_text = self.sep.join(before_text.split(self.sep)[:-1]).rstrip()
|
||||
if before_text:
|
||||
# Add the separator to the end of before_text
|
||||
if self.space_before_sep:
|
||||
before_text += ' '
|
||||
before_text += self.sep + ' '
|
||||
if self.add_separator or after_text:
|
||||
# Add separator to the end of completed text
|
||||
if self.space_before_sep:
|
||||
text = text.rstrip() + ' '
|
||||
completed_text = text + self.sep + ' '
|
||||
else:
|
||||
prefix_len = len(before_text.split(self.sep)[-1].lstrip())
|
||||
completed_text = before_text[:cursor_pos - prefix_len] + text + after_text
|
||||
return prefix_len, completed_text
|
||||
|
||||
completed_text = text
|
||||
return before_text + completed_text, after_text
|
||||
|
||||
def completion_selected(self, text):
|
||||
prefix_len, ctext = self.get_completed_text(unicode(text))
|
||||
if self.sep is None:
|
||||
self.setText(ctext)
|
||||
self.setCursorPosition(len(ctext))
|
||||
else:
|
||||
cursor_pos = self.cursorPosition()
|
||||
self.setText(ctext)
|
||||
self.setCursorPosition(cursor_pos - prefix_len + len(text))
|
||||
before_text, after_text = self.get_completed_text(unicode(text))
|
||||
self.setText(before_text + after_text)
|
||||
self.setCursorPosition(len(before_text))
|
||||
|
||||
@dynamic_property
|
||||
def all_items(self):
|
||||
@ -164,6 +168,9 @@ class MultiCompleteComboBox(EnComboBox):
|
||||
def set_space_before_sep(self, space_before):
|
||||
self.lineEdit().set_space_before_sep(space_before)
|
||||
|
||||
def set_add_separator(self, what):
|
||||
self.lineEdit().set_add_separator(what)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -19,6 +19,7 @@ from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.gui2.convert import Widget
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.library.comments import comments_to_html
|
||||
from calibre.utils.config import tweaks
|
||||
|
||||
def create_opf_file(db, book_id):
|
||||
mi = db.get_metadata(book_id, index_is_id=True)
|
||||
@ -108,6 +109,7 @@ class MetadataWidget(Widget, Ui_Form):
|
||||
all_authors.sort(key=lambda x : sort_key(x[1]))
|
||||
self.author.set_separator('&')
|
||||
self.author.set_space_before_sep(True)
|
||||
self.author.set_add_separator(tweaks['authors_completer_append_separator'])
|
||||
self.author.update_items_cache(self.db.all_author_names())
|
||||
|
||||
for i in all_authors:
|
||||
|
@ -8,8 +8,6 @@ __docformat__ = 'restructuredtext en'
|
||||
from calibre.gui2.convert.txt_output_ui import Ui_Form
|
||||
from calibre.gui2.convert import Widget
|
||||
|
||||
newline_model = None
|
||||
|
||||
class PluginWidget(Widget, Ui_Form):
|
||||
|
||||
TITLE = _('TXT Output')
|
||||
|
14
src/calibre/gui2/convert/txtz_output.py
Normal file
14
src/calibre/gui2/convert/txtz_output.py
Normal file
@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
from calibre.gui2.convert.txt_output import PluginWidget as TXTPluginWidget
|
||||
|
||||
class PluginWidget(TXTPluginWidget):
|
||||
|
||||
TITLE = _('TXTZ Output')
|
||||
HELP = _('Options specific to')+' TXTZ '+_('output')
|
||||
COMMIT_NAME = 'txtz_output'
|
@ -1026,6 +1026,20 @@ class DeviceMixin(object): # {{{
|
||||
self.location_manager.free[1] : 'carda',
|
||||
self.location_manager.free[2] : 'cardb' }
|
||||
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
|
||||
try:
|
||||
total_size = sum([os.stat(f).st_size for f in files])
|
||||
except:
|
||||
try:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
except:
|
||||
pass
|
||||
total_size = self.location_manager.free[0]
|
||||
if self.location_manager.free[0] > total_size + (1024**2):
|
||||
# Send news to main memory if enough space available
|
||||
# as some devices like the Nook Color cannot handle
|
||||
# periodicals on SD cards properly
|
||||
on_card = None
|
||||
self.upload_books(files, names, metadata,
|
||||
on_card=on_card,
|
||||
memory=[files, remove])
|
||||
|
@ -9,6 +9,7 @@ from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
|
||||
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.gui2.complete import MultiCompleteComboBox
|
||||
from calibre.utils.config import tweaks
|
||||
|
||||
class AddEmptyBookDialog(QDialog):
|
||||
|
||||
@ -69,6 +70,7 @@ class AddEmptyBookDialog(QDialog):
|
||||
|
||||
self.authors_combo.set_separator('&')
|
||||
self.authors_combo.set_space_before_sep(True)
|
||||
self.authors_combo.set_add_separator(tweaks['authors_completer_append_separator'])
|
||||
self.authors_combo.update_items_cache(db.all_author_names())
|
||||
|
||||
@property
|
||||
|
@ -71,6 +71,8 @@ class ChooseLibrary(QDialog, Ui_Dialog):
|
||||
prefs['library_path'] = loc
|
||||
self.callback(loc, copy_structure=self.copy_structure.isChecked())
|
||||
else:
|
||||
self.db.prefs.disable_setting = True
|
||||
self.library_renamed = True
|
||||
move_library(self.db.library_path, loc, self.parent(),
|
||||
self.callback)
|
||||
|
||||
|
@ -781,6 +781,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
||||
|
||||
self.authors.set_separator('&')
|
||||
self.authors.set_space_before_sep(True)
|
||||
self.authors.set_add_separator(tweaks['authors_completer_append_separator'])
|
||||
self.authors.update_items_cache(self.db.all_author_names())
|
||||
|
||||
def initialize_series(self):
|
||||
|
@ -735,6 +735,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
|
||||
self.authors.set_separator('&')
|
||||
self.authors.set_space_before_sep(True)
|
||||
self.authors.set_add_separator(tweaks['authors_completer_append_separator'])
|
||||
self.authors.update_items_cache(self.db.all_author_names())
|
||||
|
||||
def initialize_series(self):
|
||||
|
@ -9,6 +9,7 @@ from calibre.gui2.dialogs.search_ui import Ui_Dialog
|
||||
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
|
||||
from calibre.gui2 import gprefs
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.config import tweaks
|
||||
|
||||
box_values = {}
|
||||
|
||||
@ -31,6 +32,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
self.authors_box.setEditText('')
|
||||
self.authors_box.set_separator('&')
|
||||
self.authors_box.set_space_before_sep(True)
|
||||
self.authors_box.set_add_separator(tweaks['authors_completer_append_separator'])
|
||||
self.authors_box.update_items_cache(db.all_author_names())
|
||||
|
||||
all_series = db.all_series()
|
||||
|
@ -99,8 +99,8 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
return
|
||||
self.available_tags.editItem(item)
|
||||
|
||||
def delete_tags(self, item=None):
|
||||
deletes = self.available_tags.selectedItems() if item is None else [item]
|
||||
def delete_tags(self):
|
||||
deletes = self.available_tags.selectedItems()
|
||||
if not deletes:
|
||||
error_dialog(self, _('No items selected'),
|
||||
_('You must select at least one items from the list.')).exec_()
|
||||
|
@ -64,6 +64,7 @@ class LibraryViewMixin(object): # {{{
|
||||
view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book)
|
||||
|
||||
self.build_context_menus()
|
||||
self.library_view.model().set_highlight_only(config['highlight_search_matches'])
|
||||
|
||||
def build_context_menus(self):
|
||||
lm = QMenu(self)
|
||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
|
||||
pyqtSignal, QToolButton, QMenu, QCheckBox, \
|
||||
pyqtSignal, QToolButton, QMenu, \
|
||||
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup
|
||||
|
||||
|
||||
@ -156,7 +156,8 @@ class SearchBar(QWidget): # {{{
|
||||
x = ComboBoxWithHelp(self)
|
||||
x.setMaximumSize(QSize(150, 16777215))
|
||||
x.setObjectName("search_restriction")
|
||||
x.setToolTip(_("Books display will be restricted to those matching the selected saved search"))
|
||||
x.setToolTip(_('Books display will be restricted to those matching the '
|
||||
'selected saved search'))
|
||||
l.addWidget(x)
|
||||
parent.search_restriction = x
|
||||
|
||||
@ -175,7 +176,8 @@ class SearchBar(QWidget): # {{{
|
||||
x = parent.search = SearchBox2(self)
|
||||
x.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
x.setObjectName("search")
|
||||
x.setToolTip(_("<p>Search the list of books by title, author, publisher, tags, comments, etc.<br><br>Words separated by spaces are ANDed"))
|
||||
x.setToolTip(_("<p>Search the list of books by title, author, publisher, "
|
||||
"tags, comments, etc.<br><br>Words separated by spaces are ANDed"))
|
||||
l.addWidget(x)
|
||||
|
||||
self.search_button = QToolButton()
|
||||
@ -194,13 +196,11 @@ class SearchBar(QWidget): # {{{
|
||||
l.addWidget(x)
|
||||
x.setToolTip(_("Reset Quick Search"))
|
||||
|
||||
x = parent.search_highlight_only = QCheckBox()
|
||||
x.setText(_('&Highlight'))
|
||||
x.setToolTip('<p>'+_('When searching, highlight matched books, instead '
|
||||
'of restricting the book list to the matches.<p> You can use the '
|
||||
'N or F3 keys to go to the next match.'))
|
||||
x = parent.search_options_button = QToolButton(self)
|
||||
x.setIcon(QIcon(I('config.png')))
|
||||
x.setObjectName("search_option_button")
|
||||
l.addWidget(x)
|
||||
x.setVisible(False)
|
||||
x.setToolTip(_("Change the way searching for books works"))
|
||||
|
||||
x = parent.saved_search = SavedSearchBox(self)
|
||||
x.setMaximumSize(QSize(150, 16777215))
|
||||
@ -227,7 +227,6 @@ class SearchBar(QWidget): # {{{
|
||||
x.setToolTip(_("Delete current saved search"))
|
||||
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
class Spacer(QWidget): # {{{
|
||||
|
@ -177,6 +177,8 @@ class CompleteDelegate(QStyledItemDelegate): # {{{
|
||||
editor = MultiCompleteLineEdit(parent)
|
||||
editor.set_separator(self.sep)
|
||||
editor.set_space_before_sep(self.space_before_sep)
|
||||
if self.sep == '&':
|
||||
editor.set_add_separator(tweaks['authors_completer_append_separator'])
|
||||
if not index.model().is_custom_column(col):
|
||||
all_items = getattr(self.db, self.items_func_name)()
|
||||
else:
|
||||
|
@ -238,8 +238,6 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
|
||||
def set_highlight_only(self, toWhat):
|
||||
self.highlight_only = toWhat
|
||||
if self.last_search:
|
||||
self.research()
|
||||
|
||||
def get_current_highlighted_id(self):
|
||||
if len(self.ids_to_highlight) == 0 or self.current_highlighted_idx is None:
|
||||
|
@ -177,6 +177,7 @@ class AuthorsEdit(MultiCompleteComboBox):
|
||||
|
||||
self.set_separator('&')
|
||||
self.set_space_before_sep(True)
|
||||
self.set_add_separator(tweaks['authors_completer_append_separator'])
|
||||
self.update_items_cache(db.all_author_names())
|
||||
|
||||
au = db.authors(id_, index_is_id=True)
|
||||
|
@ -46,7 +46,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
r('disable_tray_notification', config)
|
||||
r('use_roman_numerals_for_series_number', config)
|
||||
r('separate_cover_flow', config, restart_required=True)
|
||||
r('search_as_you_type', config)
|
||||
r('show_child_bar', gprefs)
|
||||
|
||||
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
|
||||
@ -116,7 +115,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
|
||||
def refresh_gui(self, gui):
|
||||
gui.search.search_as_you_type(config['search_as_you_type'])
|
||||
self.update_font_display()
|
||||
gui.tags_view.reread_collapse_parameters()
|
||||
|
||||
|
@ -124,23 +124,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_separate_cover_flow">
|
||||
<property name="text">
|
||||
<string>Show cover &browser in a separate window (needs restart)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QCheckBox" name="opt_search_as_you_type">
|
||||
<property name="text">
|
||||
<string>Search as you type</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
@ -177,7 +167,7 @@ if you never want subcategories</string>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="opt_tags_browser_collapse_at">
|
||||
<property name="toolTip">
|
||||
<string>If a Tag Browser category has more than this number of items, it is divided
|
||||
<string>If a Tag Browser category has more than this number of items, it is divided
|
||||
up into sub-categories. If the partition method is set to disable, this value is ignored.</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
|
@ -157,11 +157,12 @@ class Preferences(QMainWindow):
|
||||
|
||||
run_wizard_requested = pyqtSignal()
|
||||
|
||||
def __init__(self, gui, initial_plugin=None):
|
||||
def __init__(self, gui, initial_plugin=None, close_after_initial=False):
|
||||
QMainWindow.__init__(self, gui)
|
||||
self.gui = gui
|
||||
self.must_restart = False
|
||||
self.committed = False
|
||||
self.close_after_initial = close_after_initial
|
||||
|
||||
self.resize(900, 720)
|
||||
nh, nw = min_available_height()-25, available_width()-10
|
||||
@ -306,7 +307,7 @@ class Preferences(QMainWindow):
|
||||
|
||||
def esc(self, *args):
|
||||
if self.stack.currentIndex() == 1:
|
||||
self.hide_plugin()
|
||||
self.cancel()
|
||||
elif self.stack.currentIndex() == 0:
|
||||
self.close()
|
||||
|
||||
@ -331,12 +332,15 @@ class Preferences(QMainWindow):
|
||||
show_copy_button=False)
|
||||
self.showing_widget.refresh_gui(self.gui)
|
||||
self.hide_plugin()
|
||||
if must_restart and rc:
|
||||
if self.close_after_initial or (must_restart and rc):
|
||||
self.close()
|
||||
|
||||
|
||||
def cancel(self, *args):
|
||||
self.hide_plugin()
|
||||
if self.close_after_initial:
|
||||
self.close()
|
||||
else:
|
||||
self.hide_plugin()
|
||||
|
||||
def restore_defaults(self, *args):
|
||||
self.showing_widget.restore_defaults()
|
||||
|
@ -109,7 +109,7 @@ class PluginModel(QAbstractItemModel, SearchQueryParser): # {{{
|
||||
return self.index(ans[0], 0, QModelIndex()) if ans[1] < 0 else \
|
||||
self.index(ans[1], 0, self.index(ans[0], 0, QModelIndex()))
|
||||
|
||||
def index(self, row, column, parent):
|
||||
def index(self, row, column, parent=QModelIndex()):
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QModelIndex()
|
||||
|
||||
@ -165,8 +165,6 @@ class PluginModel(QAbstractItemModel, SearchQueryParser): # {{{
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return 0
|
||||
if index.internalId() == 0:
|
||||
return Qt.ItemIsEnabled
|
||||
flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
|
||||
return flags
|
||||
|
||||
|
38
src/calibre/gui2/preferences/search.py
Normal file
38
src/calibre/gui2/preferences/search.py
Normal file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import QApplication
|
||||
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \
|
||||
CommaSeparatedList
|
||||
from calibre.gui2.preferences.search_ui import Ui_Form
|
||||
from calibre.gui2 import config
|
||||
from calibre.utils.config import prefs
|
||||
|
||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
def genesis(self, gui):
|
||||
self.gui = gui
|
||||
|
||||
r = self.register
|
||||
|
||||
r('search_as_you_type', config)
|
||||
r('highlight_search_matches', config)
|
||||
r('limit_search_columns', prefs)
|
||||
r('limit_search_columns_to', prefs, setting=CommaSeparatedList)
|
||||
fl = gui.library_view.model().db.field_metadata.get_search_terms()
|
||||
self.opt_limit_search_columns_to.update_items_cache(fl)
|
||||
|
||||
def refresh_gui(self, gui):
|
||||
gui.search.search_as_you_type(config['search_as_you_type'])
|
||||
gui.library_view.model().set_highlight_only(config['highlight_search_matches'])
|
||||
gui.search.do_search()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication([])
|
||||
test_widget('Interface', 'Search')
|
||||
|
104
src/calibre/gui2/preferences/search.ui
Normal file
104
src/calibre/gui2/preferences/search.ui
Normal file
@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>670</width>
|
||||
<height>392</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="opt_search_as_you_type">
|
||||
<property name="text">
|
||||
<string>Search as you &type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="opt_highlight_search_matches">
|
||||
<property name="text">
|
||||
<string>&Highlight search results instead of restricting the book list to the results</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>What to search by default</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>When you enter a search term without a prefix, by default calibre will search all metadata for matches. For example, entering, "asimov" will search not just authors but title/tags/series/comments/etc. Use these options if you would like to change this behavior.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_limit_search_columns">
|
||||
<property name="text">
|
||||
<string>&Limit the searched metadata</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>&Columns that non-prefixed searches are limited to:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_limit_search_columns_to</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="MultiCompleteLineEdit" name="opt_limit_search_columns_to"/>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Note that this option affects all searches, including saved searches and restrictions. Therefore, if you use this option, it is best to ensure that you always use prefixes in your saved searches. For example, use "series:Foundation" rather than just "Foundation" in a saved search</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>MultiCompleteLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>calibre/gui2.complete.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -5,37 +5,321 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import textwrap
|
||||
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit
|
||||
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.gui2.widgets import PythonHighlighter
|
||||
from calibre import isbytestring
|
||||
|
||||
from PyQt4.Qt import QAbstractListModel, Qt, QStyledItemDelegate, QStyle, \
|
||||
QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog, \
|
||||
QVBoxLayout, QPlainTextEdit, QLabel
|
||||
|
||||
class Delegate(QStyledItemDelegate): # {{{
|
||||
def __init__(self, view):
|
||||
QStyledItemDelegate.__init__(self, view)
|
||||
self.view = view
|
||||
|
||||
def paint(self, p, opt, idx):
|
||||
copy = QStyleOptionViewItem(opt)
|
||||
copy.showDecorationSelected = True
|
||||
if self.view.currentIndex() == idx:
|
||||
copy.state |= QStyle.State_HasFocus
|
||||
QStyledItemDelegate.paint(self, p, copy, idx)
|
||||
|
||||
# }}}
|
||||
|
||||
class Tweak(object): # {{{
|
||||
|
||||
def __init__(self, name, doc, var_names, defaults, custom):
|
||||
self.name = name
|
||||
self.doc = doc.strip()
|
||||
self.var_names = var_names
|
||||
self.default_values = {}
|
||||
for x in var_names:
|
||||
self.default_values[x] = defaults[x]
|
||||
self.custom_values = {}
|
||||
for x in var_names:
|
||||
if x in custom:
|
||||
self.custom_values[x] = custom[x]
|
||||
|
||||
def __str__(self):
|
||||
ans = ['#: ' + self.name]
|
||||
for line in self.doc.splitlines():
|
||||
if line:
|
||||
ans.append('# ' + line)
|
||||
for key, val in self.default_values.iteritems():
|
||||
val = self.custom_values.get(key, val)
|
||||
ans.append('%s = %r'%(key, val))
|
||||
ans = '\n'.join(ans)
|
||||
if isinstance(ans, unicode):
|
||||
ans = ans.encode('utf-8')
|
||||
return ans
|
||||
|
||||
def __cmp__(self, other):
|
||||
return -1 * cmp(self.is_customized,
|
||||
getattr(other, 'is_customized', False))
|
||||
|
||||
@property
|
||||
def is_customized(self):
|
||||
for x, val in self.default_values.iteritems():
|
||||
if self.custom_values.get(x, val) != val:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def edit_text(self):
|
||||
ans = ['# %s'%self.name]
|
||||
for x, val in self.default_values.iteritems():
|
||||
val = self.custom_values.get(x, val)
|
||||
ans.append('%s = %r'%(x, val))
|
||||
return '\n\n'.join(ans)
|
||||
|
||||
def restore_to_default(self):
|
||||
self.custom_values.clear()
|
||||
|
||||
def update(self, varmap):
|
||||
self.custom_values.update(varmap)
|
||||
|
||||
# }}}
|
||||
|
||||
class Tweaks(QAbstractListModel): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QAbstractListModel.__init__(self, parent)
|
||||
raw_defaults, raw_custom = read_raw_tweaks()
|
||||
|
||||
self.parse_tweaks(raw_defaults, raw_custom)
|
||||
|
||||
def rowCount(self, *args):
|
||||
return len(self.tweaks)
|
||||
|
||||
def data(self, index, role):
|
||||
row = index.row()
|
||||
try:
|
||||
tweak = self.tweaks[row]
|
||||
except:
|
||||
return NONE
|
||||
if role == Qt.DisplayRole:
|
||||
return textwrap.fill(tweak.name, 40)
|
||||
if role == Qt.FontRole and tweak.is_customized:
|
||||
ans = QFont()
|
||||
ans.setBold(True)
|
||||
return ans
|
||||
if role == Qt.ToolTipRole:
|
||||
tt = _('This tweak has it default value')
|
||||
if tweak.is_customized:
|
||||
tt = '<p>'+_('This tweak has been customized')
|
||||
tt += '<pre>'
|
||||
for varn, val in tweak.custom_values.iteritems():
|
||||
tt += '%s = %r\n\n'%(varn, val)
|
||||
return tt
|
||||
if role == Qt.UserRole:
|
||||
return tweak
|
||||
return NONE
|
||||
|
||||
def parse_tweaks(self, defaults, custom):
|
||||
l, g = {}, {}
|
||||
try:
|
||||
exec custom in g, l
|
||||
except:
|
||||
print 'Failed to load custom tweaks file'
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
dl, dg = {}, {}
|
||||
exec defaults in dg, dl
|
||||
lines = defaults.splitlines()
|
||||
pos = 0
|
||||
self.tweaks = []
|
||||
while pos < len(lines):
|
||||
line = lines[pos]
|
||||
if line.startswith('#:'):
|
||||
pos = self.read_tweak(lines, pos, dl, l)
|
||||
pos += 1
|
||||
|
||||
self.tweaks.sort()
|
||||
default_keys = set(dl.iterkeys())
|
||||
custom_keys = set(l.iterkeys())
|
||||
|
||||
self.plugin_tweaks = {}
|
||||
for key in custom_keys - default_keys:
|
||||
self.plugin_tweaks[key] = l[key]
|
||||
|
||||
def read_tweak(self, lines, pos, defaults, custom):
|
||||
name = lines[pos][2:].strip()
|
||||
doc, var_names = [], []
|
||||
while True:
|
||||
pos += 1
|
||||
line = lines[pos]
|
||||
if not line.startswith('#'):
|
||||
break
|
||||
doc.append(line[1:].strip())
|
||||
doc = '\n'.join(doc)
|
||||
while True:
|
||||
line = lines[pos]
|
||||
if not line.strip():
|
||||
break
|
||||
spidx1 = line.find(' ')
|
||||
spidx2 = line.find('=')
|
||||
spidx = spidx1 if spidx1 > 0 and (spidx2 == 0 or spidx2 > spidx1) else spidx2
|
||||
if spidx > 0:
|
||||
var = line[:spidx]
|
||||
if var not in defaults:
|
||||
raise ValueError('%r not in default tweaks dict'%var)
|
||||
var_names.append(var)
|
||||
pos += 1
|
||||
if not var_names:
|
||||
raise ValueError('Failed to find any variables for %r'%name)
|
||||
self.tweaks.append(Tweak(name, doc, var_names, defaults, custom))
|
||||
#print '\n\n', self.tweaks[-1]
|
||||
return pos
|
||||
|
||||
def restore_to_default(self, idx):
|
||||
tweak = self.data(idx, Qt.UserRole)
|
||||
if tweak is not NONE:
|
||||
tweak.restore_to_default()
|
||||
self.dataChanged.emit(idx, idx)
|
||||
|
||||
def restore_to_defaults(self):
|
||||
for r in range(self.rowCount()):
|
||||
self.restore_to_default(self.index(r))
|
||||
|
||||
def update_tweak(self, idx, varmap):
|
||||
tweak = self.data(idx, Qt.UserRole)
|
||||
if tweak is not NONE:
|
||||
tweak.update(varmap)
|
||||
self.dataChanged.emit(idx, idx)
|
||||
|
||||
def to_string(self):
|
||||
ans = ['#!/usr/bin/env python',
|
||||
'# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai', '',
|
||||
'# This file was automatically generated by calibre, do not'
|
||||
' edit it unless you know what you are doing.', '',
|
||||
]
|
||||
for tweak in self.tweaks:
|
||||
ans.extend(['', str(tweak), ''])
|
||||
|
||||
if self.plugin_tweaks:
|
||||
ans.extend(['', '',
|
||||
'# The following are tweaks for installed plugins', ''])
|
||||
for key, val in self.plugin_tweaks.iteritems():
|
||||
ans.extend(['%s = %r'%(key, val), '', ''])
|
||||
return '\n'.join(ans)
|
||||
|
||||
@property
|
||||
def plugin_tweaks_string(self):
|
||||
ans = []
|
||||
for key, val in self.plugin_tweaks.iteritems():
|
||||
ans.extend(['%s = %r'%(key, val), '', ''])
|
||||
ans = '\n'.join(ans)
|
||||
if isbytestring(ans):
|
||||
ans = ans.decode('utf-8')
|
||||
return ans
|
||||
|
||||
def set_plugin_tweaks(self, d):
|
||||
self.plugin_tweaks = d
|
||||
|
||||
# }}}
|
||||
|
||||
class PluginTweaks(QDialog): # {{{
|
||||
|
||||
def __init__(self, raw, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.edit = QPlainTextEdit(self)
|
||||
self.highlighter = PythonHighlighter(self.edit.document())
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
self.msg = QLabel(
|
||||
_('Add/edit tweaks for any custom plugins you have installed. '
|
||||
'Documentation for these tweaks should be available '
|
||||
'on the website from where you downloaded the plugins.'))
|
||||
self.msg.setWordWrap(True)
|
||||
self.l.addWidget(self.msg)
|
||||
self.l.addWidget(self.edit)
|
||||
self.edit.setPlainText(raw)
|
||||
self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel,
|
||||
Qt.Horizontal, self)
|
||||
self.bb.accepted.connect(self.accept)
|
||||
self.bb.rejected.connect(self.reject)
|
||||
self.l.addWidget(self.bb)
|
||||
self.resize(550, 300)
|
||||
|
||||
# }}}
|
||||
|
||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
def genesis(self, 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):
|
||||
self.changed_signal.emit()
|
||||
|
||||
def initialize(self):
|
||||
deft, curt = read_raw_tweaks()
|
||||
self.current_tweaks.blockSignals(True)
|
||||
self.current_tweaks.setPlainText(curt.decode('utf-8'))
|
||||
self.current_tweaks.blockSignals(False)
|
||||
self.tweaks = Tweaks()
|
||||
self.tweaks_view.setModel(self.tweaks)
|
||||
|
||||
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):
|
||||
ConfigWidgetBase.restore_defaults(self)
|
||||
deft, curt = read_raw_tweaks()
|
||||
self.current_tweaks.setPlainText(deft.decode('utf-8'))
|
||||
self.tweaks.restore_to_defaults()
|
||||
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):
|
||||
raw = unicode(self.current_tweaks.toPlainText()).encode('utf-8')
|
||||
raw = self.tweaks.to_string()
|
||||
try:
|
||||
exec raw
|
||||
except:
|
||||
@ -54,5 +338,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
#Tweaks()
|
||||
#test_widget
|
||||
test_widget('Advanced', 'Tweaks')
|
||||
|
||||
|
@ -7,31 +7,73 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>660</width>
|
||||
<height>351</height>
|
||||
<height>531</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_18">
|
||||
<property name="text">
|
||||
<string>Values for the tweaks are shown below. Edit them to change the behavior of calibre. Your changes will only take effect after a restart of calibre.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0" rowspan="2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_18">
|
||||
<property name="text">
|
||||
<string>Values for the tweaks are shown below. Edit them to change the behavior of calibre. Your changes will only take effect <b>after a restart</b> of calibre.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<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>
|
||||
<widget class="QGroupBox" name="groupBox_6">
|
||||
<item row="0" column="1">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>All available tweaks</string>
|
||||
<string>Help</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_11">
|
||||
<item row="0" column="0">
|
||||
<widget class="QPlainTextEdit" name="default_tweaks">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="help">
|
||||
<property name="lineWrapMode">
|
||||
<enum>QPlainTextEdit::NoWrap</enum>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
@ -40,14 +82,38 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_7">
|
||||
<item row="1" column="1">
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>&Current tweaks</string>
|
||||
<string>Edit tweak</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_10">
|
||||
<item row="0" column="0">
|
||||
<widget class="QPlainTextEdit" name="current_tweaks"/>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<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>
|
||||
</layout>
|
||||
</widget>
|
||||
|
@ -16,7 +16,6 @@ from calibre.gui2 import config
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
||||
from calibre.gui2.dialogs.search import SearchDialog
|
||||
from calibre.utils.config import dynamic
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.utils.icu import sort_key
|
||||
|
||||
@ -271,7 +270,7 @@ class SavedSearchBox(QComboBox): # {{{
|
||||
def initialize(self, _search_box, colorize=False, help_text=_('Search')):
|
||||
self.search_box = _search_box
|
||||
try:
|
||||
self.line_edit.setPlaceholderText(help_text)
|
||||
self.line_edit.setPlaceholderText(help_text)
|
||||
except:
|
||||
# Using Qt < 4.7
|
||||
pass
|
||||
@ -376,9 +375,7 @@ class SearchBoxMixin(object): # {{{
|
||||
unicode(self.search.toolTip())))
|
||||
self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip())
|
||||
self.clear_button.setStatusTip(self.clear_button.toolTip())
|
||||
self.search_highlight_only.stateChanged.connect(self.highlight_only_changed)
|
||||
self.search_highlight_only.setChecked(
|
||||
dynamic.get('search_highlight_only', False))
|
||||
self.search_options_button.clicked.connect(self.search_options_button_clicked)
|
||||
|
||||
def focus_search_box(self, *args):
|
||||
self.search.setFocus(Qt.OtherFocusReason)
|
||||
@ -402,14 +399,13 @@ class SearchBoxMixin(object): # {{{
|
||||
self.search.do_search()
|
||||
self.focus_to_library()
|
||||
|
||||
def search_options_button_clicked(self):
|
||||
self.iactions['Preferences'].do_config(initial_plugin=('Interface',
|
||||
'Search'), close_after_initial=True)
|
||||
|
||||
def focus_to_library(self):
|
||||
self.current_view().setFocus(Qt.OtherFocusReason)
|
||||
|
||||
def highlight_only_changed(self, toWhat):
|
||||
dynamic.set('search_highlight_only', toWhat)
|
||||
self.current_view().model().set_highlight_only(toWhat)
|
||||
self.focus_to_library()
|
||||
|
||||
# }}}
|
||||
|
||||
class SavedSearchBoxMixin(object): # {{{
|
||||
|
@ -116,7 +116,14 @@ class TagsView(QTreeView): # {{{
|
||||
self.set_new_model(self._model.get_filter_categories_by())
|
||||
|
||||
def set_database(self, db, tag_match, sort_by):
|
||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||
self.hidden_categories = db.prefs.get('tag_browser_hidden_categories', None)
|
||||
# migrate from config to db prefs
|
||||
if self.hidden_categories is None:
|
||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||
db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
|
||||
else:
|
||||
self.hidden_categories = set(self.hidden_categories)
|
||||
|
||||
old = getattr(self, '_model', None)
|
||||
if old is not None:
|
||||
old.break_cycles()
|
||||
@ -234,7 +241,7 @@ class TagsView(QTreeView): # {{{
|
||||
gprefs['tags_browser_partition_method'] = category
|
||||
elif action == 'defaults':
|
||||
self.hidden_categories.clear()
|
||||
config.set('tag_browser_hidden_categories', self.hidden_categories)
|
||||
self.db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
|
||||
self.set_new_model()
|
||||
except:
|
||||
return
|
||||
@ -1214,7 +1221,7 @@ class TagBrowserMixin(object): # {{{
|
||||
db.field_metadata.remove_user_categories()
|
||||
for k in d.categories:
|
||||
db.field_metadata.add_user_category('@' + k, k)
|
||||
db.data.sqp_change_locations(db.field_metadata.get_search_terms())
|
||||
db.data.change_search_locations(db.field_metadata.get_search_terms())
|
||||
self.tags_view.set_new_model()
|
||||
self.tags_view.recount()
|
||||
|
||||
|
@ -483,8 +483,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
action.location_selected(location)
|
||||
if location == 'library':
|
||||
self.search_restriction.setEnabled(True)
|
||||
self.search_options_button.setEnabled(True)
|
||||
else:
|
||||
self.search_restriction.setEnabled(False)
|
||||
self.search_options_button.setEnabled(False)
|
||||
# Reset the view in case something changed while it was invisible
|
||||
self.current_view().reset()
|
||||
self.set_number_of_books_shown()
|
||||
|
@ -440,16 +440,17 @@ class Document(QWebPage): # {{{
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
j = self.javascript('document.body.offsetHeight', 'int')
|
||||
# Note that document.body.offsetHeight does not include top and bottom
|
||||
# margins on body and in some cases does not include the top margin on
|
||||
# the first element inside body either. See ticket #8791 for an example
|
||||
# of the latter.
|
||||
q = self.mainFrame().contentsSize().height()
|
||||
if q == j:
|
||||
return j
|
||||
if min(j, q) <= 0:
|
||||
return max(j, q)
|
||||
window_height = self.window_height
|
||||
if j == window_height:
|
||||
return j if q < 1.2*j else q
|
||||
return j
|
||||
if q < 0:
|
||||
# Don't know if this is still needed, but it can't hurt
|
||||
j = self.javascript('document.body.offsetHeight', 'int')
|
||||
if j >= 0:
|
||||
q = j
|
||||
return q
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
|
@ -20,7 +20,7 @@ from calibre.gui2.filename_pattern_ui import Ui_Form
|
||||
from calibre import fit_image
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
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
|
||||
|
||||
history = XMLConfig('history')
|
||||
@ -932,7 +932,7 @@ class SplitterHandle(QSplitterHandle):
|
||||
|
||||
def paintEvent(self, ev):
|
||||
QSplitterHandle.paintEvent(self, ev)
|
||||
if self.highlight:
|
||||
if self.highlight and tweaks['draw_hidden_section_indicators']:
|
||||
painter = QPainter(self)
|
||||
painter.setClipRect(ev.rect())
|
||||
painter.fillRect(self.rect(), Qt.yellow)
|
||||
|
@ -11,7 +11,7 @@ from itertools import repeat
|
||||
from datetime import timedelta
|
||||
from threading import Thread
|
||||
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.utils.config import tweaks, prefs
|
||||
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
||||
from calibre.utils.search_query_parser import SearchQueryParser
|
||||
from calibre.utils.pyparsing import ParseException
|
||||
@ -182,15 +182,16 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
self.first_sort = True
|
||||
self.search_restriction = ''
|
||||
self.field_metadata = field_metadata
|
||||
all_search_locations = field_metadata.get_search_terms()
|
||||
SearchQueryParser.__init__(self, all_search_locations, optimize=True)
|
||||
self.all_search_locations = field_metadata.get_search_terms()
|
||||
SearchQueryParser.__init__(self, self.all_search_locations, optimize=True)
|
||||
self.build_date_relop_dict()
|
||||
self.build_numeric_relop_dict()
|
||||
|
||||
def break_cycles(self):
|
||||
self._data = self.field_metadata = self.FIELD_MAP = \
|
||||
self.numeric_search_relops = self.date_search_relops = \
|
||||
self.db_prefs = None
|
||||
self.db_prefs = self.all_search_locations = None
|
||||
self.sqp_change_locations([])
|
||||
|
||||
|
||||
def __getitem__(self, row):
|
||||
@ -218,6 +219,10 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
def universal_set(self):
|
||||
return set([i[0] for i in self._data if i is not None])
|
||||
|
||||
def change_search_locations(self, locations):
|
||||
self.sqp_change_locations(locations)
|
||||
self.all_search_locations = locations
|
||||
|
||||
def build_date_relop_dict(self):
|
||||
'''
|
||||
Because the database dates have time in them, we can't use direct
|
||||
@ -432,6 +437,7 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
# get metadata key associated with the search term. Eliminates
|
||||
# dealing with plurals and other aliases
|
||||
location = self.field_metadata.search_term_to_field_key(icu_lower(location.strip()))
|
||||
# grouped search terms
|
||||
if isinstance(location, list):
|
||||
if allow_recursion:
|
||||
for loc in location:
|
||||
@ -440,6 +446,20 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
return matches
|
||||
raise ParseException(query, len(query), 'Recursive query group detected', self)
|
||||
|
||||
# apply the limit if appropriate
|
||||
if location == 'all' and prefs['limit_search_columns'] and \
|
||||
prefs['limit_search_columns_to']:
|
||||
terms = set([])
|
||||
for l in prefs['limit_search_columns_to']:
|
||||
l = icu_lower(l.strip())
|
||||
if l and l != 'all' and l in self.all_search_locations:
|
||||
terms.add(l)
|
||||
if terms:
|
||||
for l in terms:
|
||||
matches |= self.get_matches(l, query,
|
||||
candidates=candidates, allow_recursion=allow_recursion)
|
||||
return matches
|
||||
|
||||
if location in self.field_metadata:
|
||||
fm = self.field_metadata[location]
|
||||
# take care of dates special case
|
||||
|
@ -787,7 +787,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
mi.id = id
|
||||
for key, meta in self.field_metadata.custom_iteritems():
|
||||
mi.set_user_metadata(key, meta)
|
||||
mi.set(key, val=self.get_custom(idx, label=meta['label'],
|
||||
if meta['datatype'] == 'composite':
|
||||
mi.set(key, val=row[meta['rec_index']])
|
||||
else:
|
||||
mi.set(key, val=self.get_custom(idx, label=meta['label'],
|
||||
index_is_id=index_is_id),
|
||||
extra=self.get_custom_extra(idx, label=meta['label'],
|
||||
index_is_id=index_is_id))
|
||||
|
@ -7,7 +7,6 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, traceback, cStringIO, re, shutil
|
||||
from functools import partial
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
from calibre.utils.config import Config, StringConfig, tweaks
|
||||
@ -142,11 +141,19 @@ class SafeFormat(TemplateFormatter):
|
||||
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||
sanitize_func=ascii_filename, replace_whitespace=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.update(mi.all_non_none_fields())
|
||||
if mi.title:
|
||||
format_args['title'] = tsfmt(mi.title)
|
||||
if tsorder == 'strictly_alphabetic':
|
||||
v = mi.title
|
||||
else:
|
||||
# title_sort might be missing or empty. Check both conditions
|
||||
v = mi.get('title_sort', None)
|
||||
if not v:
|
||||
v = title_sort(mi.title, order=tsorder)
|
||||
format_args['title'] = v
|
||||
if mi.authors:
|
||||
format_args['authors'] = mi.format_authors()
|
||||
format_args['author'] = format_args['authors']
|
||||
@ -157,7 +164,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||
else:
|
||||
format_args['tags'] = ''
|
||||
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:
|
||||
format_args['series_index'] = mi.format_series_index()
|
||||
else:
|
||||
@ -176,7 +183,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||
cm = custom_metadata[key]
|
||||
## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't...
|
||||
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:
|
||||
format_args[key+'_index'] = fmt_sidx(format_args[key+'_index'])
|
||||
elif cm['datatype'] == 'datetime':
|
||||
|
@ -561,9 +561,10 @@ format, whether input or output are available in the conversion dialog under the
|
||||
Convert Microsoft Word documents
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|app| does not directly convert .doc files from Microsoft Word. However, in Word, you can save the document
|
||||
|app| does not directly convert .doc/.docx files from Microsoft Word. However, in Word, you can save the document
|
||||
as HTML and then convert the resulting HTML file with |app|. When saving as HTML, be sure to use the
|
||||
"Save as Web Page, Filtered" option as this will produce clean HTML that will convert well.
|
||||
"Save as Web Page, Filtered" option as this will produce clean HTML that will convert well. Note that Word
|
||||
produces really messy HTML, converting it can take a long time, so be patient.
|
||||
|
||||
There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes
|
||||
generating the Table of Contents much simpler. It is called BookCreator and is available for free
|
||||
|
@ -182,11 +182,6 @@ If you don't want to uninstall it altogether, there are a couple of tricks you c
|
||||
simplest is to simply re-name the executable file that launches the library program. More detail
|
||||
`in the forums <http://www.mobileread.com/forums/showthread.php?t=65809>`_.
|
||||
|
||||
Can I use the collections feature of the SONY reader?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|app| has full support for collections. When you add tags to a book's metadata, those tags are turned into collections when you upload the book to the SONY reader. Also, the series information is automatically
|
||||
turned into a collection on the reader. Note that the PRS-500 does not support collections for books stored on the SD card. The PRS-505 does.
|
||||
|
||||
How do I use |app| with my iPad/iPhone/iTouch?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -375,13 +370,6 @@ Content From The Web
|
||||
:depth: 1
|
||||
:local:
|
||||
|
||||
My downloaded news content causes the reader to reset.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
This is a bug in the SONY firmware. The problem can be mitigated by switching the output format to EPUB
|
||||
in the configuration dialog. Alternatively, you can use the LRF output format and use the SONY software
|
||||
to transfer the files to the reader. The SONY software pre-paginates the LRF file,
|
||||
thereby reducing the number of resets.
|
||||
|
||||
I obtained a recipe for a news site as a .py file from somewhere, how do I use it?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Start the :guilabel:`Add custom news sources` dialog (from the :guilabel:`Fetch news` menu) and click the :guilabel:`Switch to advanced mode` button. Delete everything in the box with the recipe source code and copy paste the contents of your .py file into the box. Click :guilabel:`Add/update recipe`.
|
||||
@ -391,7 +379,7 @@ I want |app| to download news from my favorite news website.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
If you are reasonably proficient with computers, you can teach |app| to download news from any website of your choosing. To learn how to do this see :ref:`news`.
|
||||
|
||||
Otherwise, you can register a request for a particular news site by adding a comment `to this ticket <http://bugs.calibre-ebook.com/ticket/405>`_.
|
||||
Otherwise, you can request a particular news site by posting in the `calibre Recipes forum <http://www.mobileread.com/forums/forumdisplay.php?f=228>`_.
|
||||
|
||||
Can I use web2disk to download an arbitrary website?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -480,7 +468,7 @@ How do I backup |app|?
|
||||
|
||||
The most important thing to backup is the |app| library folder, that contains all your books and metadata. This is the folder you chose for your |app| library when you ran |app| for the first time. You can get the path to the library folder by clicking the |app| icon on the main toolbar. You must backup this complete folder with all its files and sub-folders.
|
||||
|
||||
You can switch |app| to using a backed up library folder by simply clicking the |app| icon on the toolbar and choosing your backup library folder.
|
||||
You can switch |app| to using a backed up library folder by simply clicking the |app| icon on the toolbar and choosing your backup library folder. A backed up library folder backs up your custom columns and saved searches as well as all your books and metadata.
|
||||
|
||||
If you want to backup the |app| configuration/plugins, you have to backup the config directory. You can find this config directory via :guilabel:`Preferences->Miscellaneous`. Note that restoring configuration directories is not officially supported, but should work in most cases. Just copy the contents of the backup directory into the current configuration directory to restore.
|
||||
|
||||
@ -491,7 +479,7 @@ Most purchased EPUB books have `DRM <http://wiki.mobileread.com/wiki/DRM>`_. Thi
|
||||
I am getting a "Permission Denied" error?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A permission denied error can occur because of many possible reasons, none of them having anything to do with |app|. You can get permission denied errors if you are using an SD card with write protect enabled. Or if you, or some program you used changed the file permissions of the files in question to read only. Or if there is a filesystem error on the device which caused your operating system to mount the filesystem in read only mode or mark a particular file as read only pending recovery. Or if the files have their owner set to a user other than you. You will need to fix the underlying cause of the permissions error before resuming to use |app|. Read the error message carefully, see what file it points to and fix the permissions on that file.
|
||||
A permission denied error can occur because of many possible reasons, none of them having anything to do with |app|. You can get permission denied errors if you are using an SD card with write protect enabled. Or if you, or some program you used changed the file permissions of the files in question to read only. Or if there is a filesystem error on the device which caused your operating system to mount the filesystem in read only mode or mark a particular file as read only pending recovery. Or if the files have their owner set to a user other than you. Or if your file is open in another program. You will need to fix the underlying cause of the permissions error before resuming to use |app|. Read the error message carefully, see what file it points to and fix the permissions on that file.
|
||||
|
||||
Can I have the comment metadata show up on my reader?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -504,7 +492,7 @@ I want some feature added to |app|. What can I do?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
You have two choices:
|
||||
1. Create a patch by hacking on |app| and send it to me for review and inclusion. See `Development <http://calibre-ebook.com/get-involved>`_.
|
||||
2. `Open a ticket <http://bugs.calibre-ebook.com/newticket>`_ (you have to register and login first) and hopefully I will find the time to implement your feature.
|
||||
2. `Open a ticket <http://bugs.calibre-ebook.com/newticket>`_ (you have to register and login first). Remember that |app| development is done by volunteers, so if you get no response to your feature request, it means no one feels like implementing it.
|
||||
|
||||
Can I include |app| on a CD to be distributed with my product/magazine?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -522,7 +510,7 @@ Why are there so many calibre-parallel processes on my system?
|
||||
|
||||
In addition to this some conversion plugins run tasks in their own pool of processes, so for example if you bulk convert comics, each comic conversion will use three separate processes to render the images. The job manager knows this so it will run only a single comic conversion simultaneously.
|
||||
|
||||
And since I'm sure someone will ask: The reason adding/saving books are in separate processes is because of PDF. PDF processing libraries can crash on reading PDFs and I dont want the crash to take down all of calibre. Also when adding EPUB books, in order to extract the cover you have to sometimes render the HTML of the first page, which means that it either has to run the GUI thread of the main process or in a separate process.
|
||||
And since I'm sure someone will ask: The reason adding/saving books are in separate processes is because of PDF. PDF processing libraries can crash on reading PDFs and I dont want the crash to take down all of calibre. Also when adding EPUB books, in order to extract the cover you have to sometimes render the HTML of the first page, which means that it either has to run in the GUI thread of the main process or in a separate process.
|
||||
|
||||
Finally, the reason calibre keep workers alive and idle instead of launching on demand is to workaround the slow startup time of python processes.
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user