diff --git a/recipes/aksiyon_derigisi.recipe b/recipes/aksiyon_derigisi.recipe
new file mode 100644
index 0000000000..f18ebd84d3
--- /dev/null
+++ b/recipes/aksiyon_derigisi.recipe
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class Aksiyon (BasicNewsRecipe):
+
+ title = u'Aksiyon Dergisi'
+ __author__ = u'thomass'
+ description = 'Haftalık haber dergisi '
+ oldest_article =13
+ max_articles_per_feed =100
+ no_stylesheets = True
+ #delay = 1
+ #use_embedded_content = False
+ encoding = 'utf-8'
+ publisher = 'Aksiyon'
+ category = 'news, haberler,TR,gazete'
+ language = 'tr'
+ publication_type = 'magazine'
+ #extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} '
+ #keep_only_tags = [dict(name='font', attrs={'class':['newsDetail','agenda2NewsSpot']}),dict(name='span', attrs={'class':['agenda2Title']}),dict(name='div', attrs={'id':['gallery']})]
+ remove_tags = [dict(name='img', attrs={'src':[ 'http://medya.aksiyon.com.tr/aksiyon/images/logo/logo.bmp','/aksiyon/images/template/green/baslik0.gif','mobile/home.jpg']}) ]
+
+ cover_img_url = 'http://www.aksiyon.com.tr/aksiyon/images/aksiyon/top-page/aksiyon_top_r2_c1.jpg'
+ masthead_url = 'http://aksiyon.com.tr/aksiyon/images/aksiyon/top-page/aksiyon_top_r2_c1.jpg'
+ remove_empty_feeds= True
+ remove_attributes = ['width','height']
+
+ feeds = [
+ ( u'ANASAYFA', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=0'),
+ ( u'KARAKUTU', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=11'),
+ ( u'EKONOMİ', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=35'),
+ ( u'EKOANALİZ', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=284'),
+ ( u'YAZARLAR', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=17'),
+ ( u'KİTAPLIK', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=13'),
+ ( u'SİNEMA', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=14'),
+ ( u'ARKA PENCERE', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=27'),
+ ( u'DÜNYA', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=32'),
+ ( u'DOSYALAR', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=34'),
+ ( u'KÜLTÜR & SANAT', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=12'),
+ ( u'KAPAK', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=26'),
+ ( u'SPOR', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=38'),
+ ( u'BİLİŞİM - TEKNOLOJİ', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=39'),
+ ( u'3. BOYUT', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=172'),
+ ( u'HAYAT BİLGİSİ', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=283'),
+ ( u'İŞ DÜNYASI', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=283'),
+
+
+ ]
+
+ def print_version(self, url):
+ return url.replace('http://www.aksiyon.com.tr/aksiyon/newsDetail_getNewsById.action?load=detay&', 'http://www.aksiyon.com.tr/aksiyon/mobile_detailn.action?')
+
diff --git a/recipes/caros_amigos.recipe b/recipes/caros_amigos.recipe
new file mode 100644
index 0000000000..48edceacba
--- /dev/null
+++ b/recipes/caros_amigos.recipe
@@ -0,0 +1,17 @@
+__copyright__ = '2011, Pablo Aldama '
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1311839910(BasicNewsRecipe):
+ title = u'Caros Amigos'
+ oldest_article = 20
+ max_articles_per_feed = 100
+ language = 'pt_BR'
+ __author__ = 'Pablo Aldama'
+
+ feeds = [(u'Caros Amigos', u'http://carosamigos.terra.com.br/index/index.php?format=feed&type=rss')]
+ keep_only_tags = [dict(name='div', attrs={'class':['blog']})
+ ,dict(name='div', attrs={'class':['blogcontent']})
+ ]
+ remove_tags = [dict(name='div', attrs={'class':'addtoany'})]
+
diff --git a/recipes/counterpunch.recipe b/recipes/counterpunch.recipe
new file mode 100644
index 0000000000..5fefc86cb4
--- /dev/null
+++ b/recipes/counterpunch.recipe
@@ -0,0 +1,40 @@
+import re
+from lxml.html import parse
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class Counterpunch(BasicNewsRecipe):
+ '''
+ Parses counterpunch.com for articles
+ '''
+ title = 'Counterpunch'
+ description = 'Daily political opinion from www.Counterpunch.com'
+ language = 'en'
+ __author__ = 'O. Emmerson'
+ keep_only_tags = [dict(name='td', attrs={'width': '522'})]
+ max_articles_per_feed = 10
+
+ def parse_index(self):
+ feeds = []
+ title, url = 'Counterpunch', 'http://www.counterpunch.com'
+ articles = self.parse_page(url)
+ if articles:
+ feeds.append((title, articles))
+ return feeds
+
+ def parse_page(self, url):
+ parsed_page = parse(url).getroot()
+ articles = []
+ unwanted_text = re.compile('Website\ of\ the|I\ urge\ you|Subscribe\ now|DONATE|\@asis\.com|donation\ button|click\ over\ to\ our')
+ parsed_articles = [a for a in parsed_page.cssselect("html>body>table tr>td>p[class='style2']") if not unwanted_text.search(a.text_content())]
+ for art in parsed_articles:
+ try:
+ author = art.text
+ title = art.cssselect("a")[0].text + ' by {0}'.format(author)
+ art_url = 'http://www.counterpunch.com/' + art.cssselect("a")[0].attrib['href']
+ articles.append({'title': title, 'url': art_url})
+ except Exception as e:
+ e
+ #print('Handler Error: ', e, 'title :', a.text_content())
+ pass
+ return articles
+
diff --git a/recipes/dnevnik_mk.recipe b/recipes/dnevnik_mk.recipe
new file mode 100644
index 0000000000..ce8656339f
--- /dev/null
+++ b/recipes/dnevnik_mk.recipe
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+
+__author__ = 'Darko Spasovski'
+__license__ = 'GPL v3'
+__copyright__ = '2011, Darko Spasovski '
+'''
+dnevnik.com.mk
+'''
+
+import re
+import datetime
+from calibre.web.feeds.news import BasicNewsRecipe
+from calibre import browser
+from calibre.ebooks.BeautifulSoup import BeautifulSoup
+
+class Dnevnik(BasicNewsRecipe):
+
+ INDEX = 'http://www.dnevnik.com.mk'
+ __author__ = 'Darko Spasovski'
+ title = 'Dnevnik - mk'
+ description = 'Daily Macedonian newspaper'
+ masthead_url = 'http://www.dnevnik.com.mk/images/re-logo.gif'
+ language = 'mk'
+ publication_type = 'newspaper'
+ category = 'news, Macedonia'
+ max_articles_per_feed = 100
+ remove_javascript = True
+ no_stylesheets = True
+ use_embedded_content = False
+
+ preprocess_regexps = [(re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
+ [
+ ## Remove anything before the start of the article.
+ (r'', lambda match: ''),
+
+ ## Remove anything after the end of the article.
+ (r'', re.DOTALL), lambda m: '')]
+ conversion_options = {
+ 'comments' : description
+ ,'tags' : category
+ ,'language' : language
+ ,'publisher' : publisher
+ ,'linearize_tables': True
+ }
+
+ remove_tags = [
+ dict(name='div', attrs={'class':'add_inf'}),
+ dict(name='div', attrs={'class':'add_f'}),
+ ]
+
+ remove_attributes = ['width','height']
+
+ feeds = [
+ ('National Geographic PL', 'http://www.national-geographic.pl/rss/'),
+ ]
+
+ def print_version(self, url):
+ return url.replace('artykuly0Cpokaz', 'drukuj-artykul')
+
diff --git a/recipes/plus_info.recipe b/recipes/plus_info.recipe
new file mode 100644
index 0000000000..e95a3e7359
--- /dev/null
+++ b/recipes/plus_info.recipe
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+
+__author__ = 'Darko Spasovski'
+__license__ = 'GPL v3'
+__copyright__ = '2011, Darko Spasovski '
+
+'''
+www.plusinfo.mk
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class PlusInfo(BasicNewsRecipe):
+
+ INDEX = 'www.plusinfo.mk'
+ title = u'+info'
+ __author__ = 'Darko Spasovski'
+ description = 'Macedonian news portal'
+ publication_type = 'newsportal'
+ category = 'news, Macedonia'
+ language = 'mk'
+ masthead_url = 'http://www.plusinfo.mk/style/images/logo.jpg'
+ remove_javascript = True
+ no_stylesheets = True
+ use_embedded_content = False
+ remove_empty_feeds = True
+ oldest_article = 1
+ max_articles_per_feed = 100
+
+ keep_only_tags = [dict(name='div', attrs={'class': 'vest'})]
+ remove_tags = [dict(name='div', attrs={'class':['komentari_holder', 'objava']})]
+
+ feeds = [(u'Македонија', u'http://www.plusinfo.mk/rss/makedonija'),
+ (u'Бизнис', u'http://www.plusinfo.mk/rss/biznis'),
+ (u'Скопје', u'http://www.plusinfo.mk/rss/skopje'),
+ (u'Култура', u'http://www.plusinfo.mk/rss/kultura'),
+ (u'Свет', u'http://www.plusinfo.mk/rss/svet'),
+ (u'Сцена', u'http://www.plusinfo.mk/rss/scena'),
+ (u'Здравје', u'http://www.plusinfo.mk/rss/zdravje'),
+ (u'Магазин', u'http://www.plusinfo.mk/rss/magazin'),
+ (u'Спорт', u'http://www.plusinfo.mk/rss/sport')]
+
+ # uncomment the following block if you want the print version (note: it lacks photos)
+# def print_version(self,url):
+# segments = url.split('/')
+# printURL = '/'.join(segments[0:3]) + '/print/' + '/'.join(segments[5:])
+# return printURL
diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py
index 65cb030f96..ad56dbcb75 100644
--- a/resources/default_tweaks.py
+++ b/resources/default_tweaks.py
@@ -31,7 +31,7 @@ defaults.
# Set the use_series_auto_increment_tweak_when_importing tweak to True to
# use the above values when importing/adding books. If this tweak is set to
# False (the default) then the series number will be set to 1 if it is not
-# explicitly set to something else during the import. If set to True, then the
+# explicitly set to during the import. If set to True, then the
# series index will be set according to the series_index_auto_increment setting.
# Note that the use_series_auto_increment_tweak_when_importing tweak is used
# only when a value is not provided during import. If the importing regular
diff --git a/setup/installer/windows/freeze.py b/setup/installer/windows/freeze.py
index 026cac99cf..69e669566d 100644
--- a/setup/installer/windows/freeze.py
+++ b/setup/installer/windows/freeze.py
@@ -373,7 +373,7 @@ class Win32Freeze(Command, WixMixIn):
src = self.j(self.src_root, 'setup', 'installer', 'windows',
'portable.c')
obj = self.j(self.obj_dir, self.b(src)+'.obj')
- cflags = '/c /EHsc /MT /W3 /Ox /nologo /D_UNICODE'.split()
+ cflags = '/c /EHsc /MT /W3 /Ox /nologo /D_UNICODE /DUNICODE'.split()
if self.newer(obj, [src]):
self.info('Compiling', obj)
@@ -386,6 +386,7 @@ class Win32Freeze(Command, WixMixIn):
cmd = [msvc.linker] + ['/INCREMENTAL:NO', '/MACHINE:X86',
'/LIBPATH:'+self.obj_dir, '/SUBSYSTEM:WINDOWS',
'/RELEASE',
+ '/ENTRY:wWinMainCRTStartup',
'/OUT:'+exe, self.embed_resources(exe),
obj, 'User32.lib']
self.run_builder(cmd)
diff --git a/setup/installer/windows/portable.c b/setup/installer/windows/portable.c
index 05763bc303..b07afea9dc 100644
--- a/setup/installer/windows/portable.c
+++ b/setup/installer/windows/portable.c
@@ -2,15 +2,21 @@
#define UNICODE
#endif
+#ifndef _UNICODE
+#define _UNICODE
+#endif
+
+
#include
#include
+#include
#include
#define BUFSIZE 4096
void show_error(LPCTSTR msg) {
MessageBeep(MB_ICONERROR);
- MessageBox(NULL, msg, TEXT("Error"), MB_OK|MB_ICONERROR);
+ MessageBox(NULL, msg, _T("Error"), MB_OK|MB_ICONERROR);
}
void show_detailed_error(LPCTSTR preamble, LPCTSTR msg, int code) {
@@ -20,7 +26,7 @@ void show_detailed_error(LPCTSTR preamble, LPCTSTR msg, int code) {
_sntprintf_s(buf,
LocalSize(buf) / sizeof(TCHAR), _TRUNCATE,
- TEXT("%s\r\n %s (Error Code: %d)\r\n"),
+ _T("%s\r\n %s (Error Code: %d)\r\n"),
preamble, msg, code);
show_error(buf);
@@ -32,7 +38,7 @@ void show_last_error_crt(LPCTSTR preamble) {
int err = 0;
_get_errno(&err);
- _wcserror_s(buf, BUFSIZE, err);
+ _tcserror_s(buf, BUFSIZE, err);
show_detailed_error(preamble, buf, err);
}
@@ -57,7 +63,7 @@ void show_last_error(LPCTSTR preamble) {
LPTSTR get_app_dir() {
LPTSTR buf, buf2, buf3;
DWORD sz;
- TCHAR drive[4] = TEXT("\0\0\0");
+ TCHAR drive[4] = _T("\0\0\0");
errno_t err;
buf = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
@@ -67,18 +73,18 @@ LPTSTR get_app_dir() {
sz = GetModuleFileName(NULL, buf, BUFSIZE);
if (sz == 0 || sz > BUFSIZE-1) {
- show_error(TEXT("Failed to get path to calibre-portable.exe"));
+ show_error(_T("Failed to get path to calibre-portable.exe"));
ExitProcess(1);
}
err = _tsplitpath_s(buf, drive, 4, buf2, BUFSIZE, NULL, 0, NULL, 0);
if (err != 0) {
- show_last_error_crt(TEXT("Failed to split path to calibre-portable.exe"));
+ show_last_error_crt(_T("Failed to split path to calibre-portable.exe"));
ExitProcess(1);
}
- _sntprintf_s(buf3, BUFSIZE-1, _TRUNCATE, TEXT("%s%s"), drive, buf2);
+ _sntprintf_s(buf3, BUFSIZE-1, _TRUNCATE, _T("%s%s"), drive, buf2);
free(buf); free(buf2);
return buf3;
}
@@ -90,18 +96,18 @@ void launch_calibre(LPCTSTR exe, LPCTSTR config_dir, LPCTSTR library_dir) {
BOOL fSuccess;
TCHAR cmdline[BUFSIZE];
- if (! SetEnvironmentVariable(TEXT("CALIBRE_CONFIG_DIRECTORY"), config_dir)) {
- show_last_error(TEXT("Failed to set environment variables"));
+ if (! SetEnvironmentVariable(_T("CALIBRE_CONFIG_DIRECTORY"), config_dir)) {
+ show_last_error(_T("Failed to set environment variables"));
ExitProcess(1);
}
- if (! SetEnvironmentVariable(TEXT("CALIBRE_PORTABLE_BUILD"), exe)) {
- show_last_error(TEXT("Failed to set environment variables"));
+ if (! SetEnvironmentVariable(_T("CALIBRE_PORTABLE_BUILD"), exe)) {
+ show_last_error(_T("Failed to set environment variables"));
ExitProcess(1);
}
dwFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP;
- _sntprintf_s(cmdline, BUFSIZE, _TRUNCATE, TEXT(" \"--with-library=%s\""), library_dir);
+ _sntprintf_s(cmdline, BUFSIZE, _TRUNCATE, _T(" \"--with-library=%s\""), library_dir);
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
@@ -119,7 +125,7 @@ void launch_calibre(LPCTSTR exe, LPCTSTR config_dir, LPCTSTR library_dir) {
);
if (fSuccess == 0) {
- show_last_error(TEXT("Failed to launch the calibre program"));
+ show_last_error(_T("Failed to launch the calibre program"));
}
// Close process and thread handles.
@@ -137,9 +143,9 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine
library_dir = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
exe = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
- _sntprintf_s(config_dir, BUFSIZE, _TRUNCATE, TEXT("%sCalibre Settings"), app_dir);
- _sntprintf_s(exe, BUFSIZE, _TRUNCATE, TEXT("%sCalibre\\calibre.exe"), app_dir);
- _sntprintf_s(library_dir, BUFSIZE, _TRUNCATE, TEXT("%sCalibre Library"), app_dir);
+ _sntprintf_s(config_dir, BUFSIZE, _TRUNCATE, _T("%sCalibre Settings"), app_dir);
+ _sntprintf_s(exe, BUFSIZE, _TRUNCATE, _T("%sCalibre\\calibre.exe"), app_dir);
+ _sntprintf_s(library_dir, BUFSIZE, _TRUNCATE, _T("%sCalibre Library"), app_dir);
launch_calibre(exe, config_dir, library_dir);
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index a79078988a..620254b1f5 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -570,7 +570,7 @@ from calibre.devices.teclast.driver import (TECLAST_K3, NEWSMY, IPAPYRUS,
from calibre.devices.sne.driver import SNE
from calibre.devices.misc import (PALMPRE, AVANT, SWEEX, PDNOVEL,
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, LUMIREAD, ALURATEK_COLOR,
- TREKSTOR, EEEREADER, NEXTBOOK, ADAM, MOOVYBOOK)
+ TREKSTOR, EEEREADER, NEXTBOOK, ADAM, MOOVYBOOK, COBY)
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK
@@ -705,7 +705,7 @@ plugins += [
EEEREADER,
NEXTBOOK,
ADAM,
- MOOVYBOOK,
+ MOOVYBOOK, COBY,
ITUNES,
BOEYE_BEX,
BOEYE_BDX,
@@ -1228,17 +1228,6 @@ class StoreEbookscomStore(StoreBase):
formats = ['EPUB', 'LIT', 'MOBI', 'PDF']
affiliate = True
-#class StoreEPubBuyDEStore(StoreBase):
-# name = 'EPUBBuy DE'
-# author = 'Charles Haley'
-# description = u'Bei EPUBBuy.com finden Sie ausschliesslich eBooks im weitverbreiteten EPUB-Format und ohne DRM. So haben Sie die freie Wahl, wo Sie Ihr eBook lesen: Tablet, eBook-Reader, Smartphone oder einfach auf Ihrem PC. So macht eBook-Lesen Spaß!'
-# actual_plugin = 'calibre.gui2.store.stores.epubbuy_de_plugin:EPubBuyDEStore'
-#
-# drm_free_only = True
-# headquarters = 'DE'
-# formats = ['EPUB']
-# affiliate = True
-
class StoreEBookShoppeUKStore(StoreBase):
name = 'ebookShoppe UK'
author = u'Charles Haley'
@@ -1266,16 +1255,7 @@ class StoreEKnigiStore(StoreBase):
headquarters = 'BG'
formats = ['EPUB', 'PDF', 'HTML']
- #affiliate = True
-
-class StoreEpubBudStore(StoreBase):
- name = 'ePub Bud'
- description = 'Well, it\'s pretty much just "YouTube for Children\'s eBooks. A not-for-profit organization devoted to brining self published childrens books to the world.'
- actual_plugin = 'calibre.gui2.store.stores.epubbud_plugin:EpubBudStore'
-
- drm_free_only = True
- headquarters = 'US'
- formats = ['EPUB']
+ affiliate = True
class StoreFeedbooksStore(StoreBase):
name = 'Feedbooks'
@@ -1311,6 +1291,7 @@ class StoreGoogleBooksStore(StoreBase):
headquarters = 'US'
formats = ['EPUB', 'PDF', 'TXT']
+ affiliate = True
class StoreGutenbergStore(StoreBase):
name = 'Project Gutenberg'
@@ -1394,6 +1375,17 @@ class StoreOReillyStore(StoreBase):
headquarters = 'US'
formats = ['APK', 'DAISY', 'EPUB', 'MOBI', 'PDF']
+class StoreOzonRUStore(StoreBase):
+ name = 'OZON.ru'
+ description = u'ebooks from OZON.ru'
+ actual_plugin = 'calibre.gui2.store.stores.ozon_ru_plugin:OzonRUStore'
+ author = 'Roman Mukhin'
+
+ drm_free_only = True
+ headquarters = 'RU'
+ formats = ['TXT', 'PDF', 'DJVU', 'RTF', 'DOC', 'JAR', 'FB2']
+ affiliate = True
+
class StorePragmaticBookshelfStore(StoreBase):
name = 'Pragmatic Bookshelf'
description = u'The Pragmatic Bookshelf\'s collection of programming and tech books avaliable as ebooks.'
@@ -1491,10 +1483,8 @@ plugins += [
StoreEbookNLStore,
StoreEbookscomStore,
StoreEBookShoppeUKStore,
-# StoreEPubBuyDEStore,
StoreEHarlequinStore,
StoreEKnigiStore,
- StoreEpubBudStore,
StoreFeedbooksStore,
StoreFoylesUKStore,
StoreGandalfStore,
@@ -1508,6 +1498,7 @@ plugins += [
StoreNextoStore,
StoreOpenBooksStore,
StoreOReillyStore,
+ StoreOzonRUStore,
StorePragmaticBookshelfStore,
StoreSmashwordsStore,
StoreVirtualoStore,
diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py
index 6c5706f039..92fce68f11 100644
--- a/src/calibre/devices/misc.py
+++ b/src/calibre/devices/misc.py
@@ -351,3 +351,29 @@ class MOOVYBOOK(USBMS):
def get_main_ebook_dir(self, for_upload=False):
return 'Books' if for_upload else self.EBOOK_DIR_MAIN
+class COBY(USBMS):
+
+ name = 'COBY MP977 device interface'
+ gui_name = 'COBY'
+ description = _('Communicate with the COBY')
+ author = 'Kovid Goyal'
+ supported_platforms = ['windows', 'osx', 'linux']
+
+ # Ordered list of supported formats
+ FORMATS = ['epub', 'pdf']
+
+ VENDOR_ID = [0x1e74]
+ PRODUCT_ID = [0x7121]
+ BCD = [0x02]
+ VENDOR_NAME = 'USB_2.0'
+ WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'MP977_DRIVER'
+
+ EBOOK_DIR_MAIN = ''
+
+ SUPPORTS_SUB_DIRS = False
+
+ def get_carda_ebook_dir(self, for_upload=False):
+ if for_upload:
+ return 'eBooks'
+ return self.EBOOK_DIR_CARD_A
+
diff --git a/src/calibre/ebooks/metadata/fb2.py b/src/calibre/ebooks/metadata/fb2.py
index 4c47d87717..765ac6d009 100644
--- a/src/calibre/ebooks/metadata/fb2.py
+++ b/src/calibre/ebooks/metadata/fb2.py
@@ -24,10 +24,9 @@ XPath = partial(etree.XPath, namespaces=NAMESPACES)
tostring = partial(etree.tostring, method='text', encoding=unicode)
def get_metadata(stream):
- """ Return fb2 metadata as a L{MetaInformation} object """
+ ''' Return fb2 metadata as a L{MetaInformation} object '''
root = _get_fbroot(stream)
-
book_title = _parse_book_title(root)
authors = _parse_authors(root)
@@ -166,7 +165,7 @@ def _parse_tags(root, mi):
break
def _parse_series(root, mi):
- #calibri supports only 1 series: use the 1-st one
+ # calibri supports only 1 series: use the 1-st one
# pick up sequence but only from 1 secrion in prefered order
# except
xp_ti = '//fb2:title-info/fb2:sequence[1]'
@@ -181,11 +180,12 @@ def _parse_series(root, mi):
def _parse_isbn(root, mi):
# some people try to put several isbn in this field, but it is not allowed. try to stick to the 1-st one in this case
isbn = XPath('normalize-space(//fb2:publish-info/fb2:isbn/text())')(root)
- # some people try to put several isbn in this field, but it is not allowed. try to stick to the 1-st one in this case
- if ',' in isbn:
- isbn = isbn[:isbn.index(',')]
- if check_isbn(isbn):
- mi.isbn = isbn
+ if isbn:
+ # some people try to put several isbn in this field, but it is not allowed. try to stick to the 1-st one in this case
+ if ',' in isbn:
+ isbn = isbn[:isbn.index(',')]
+ if check_isbn(isbn):
+ mi.isbn = isbn
def _parse_comments(root, mi):
# pick up annotation but only from 1 secrion ; fallback:
@@ -232,4 +232,3 @@ def _get_fbroot(stream):
raw = xml_to_unicode(raw, strip_encoding_pats=True)[0]
root = etree.fromstring(raw, parser=parser)
return root
-
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 7ad741848e..35fd724ddd 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -1030,8 +1030,10 @@ class OPF(object): # {{{
attrib = attrib or {}
attrib['name'] = 'calibre:' + name
name = '{%s}%s' % (self.NAMESPACES['opf'], 'meta')
+ nsmap = dict(self.NAMESPACES)
+ del nsmap['opf']
elem = etree.SubElement(self.metadata, name, attrib=attrib,
- nsmap=self.NAMESPACES)
+ nsmap=nsmap)
elem.tail = '\n'
return elem
diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py
index 97b6d15bc8..a7bcbc5a89 100644
--- a/src/calibre/ebooks/metadata/sources/identify.py
+++ b/src/calibre/ebooks/metadata/sources/identify.py
@@ -22,6 +22,7 @@ from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.date import utc_tz, as_utc
from calibre.utils.html2text import html2text
from calibre.utils.icu import lower
+from calibre.utils.date import UNDEFINED_DATE
# Download worker {{{
class Worker(Thread):
@@ -490,6 +491,8 @@ def identify(log, abort, # {{{
max_tags = msprefs['max_tags']
for r in results:
r.tags = r.tags[:max_tags]
+ if getattr(r.pubdate, 'year', 2000) <= UNDEFINED_DATE.year:
+ r.pubdate = None
if msprefs['swap_author_names']:
for r in results:
diff --git a/src/calibre/ebooks/mobi/debug.py b/src/calibre/ebooks/mobi/debug.py
index 67f20e691f..1b921860e0 100644
--- a/src/calibre/ebooks/mobi/debug.py
+++ b/src/calibre/ebooks/mobi/debug.py
@@ -73,7 +73,7 @@ class PalmDB(object):
self.ident = self.type + self.creator
if self.ident not in (b'BOOKMOBI', b'TEXTREAD'):
raise ValueError('Unknown book ident: %r'%self.ident)
- self.uid_seed = self.raw[68:72]
+ self.uid_seed, = struct.unpack(b'>I', self.raw[68:72])
self.next_rec_list_id = self.raw[72:76]
self.number_of_records, = struct.unpack(b'>H', self.raw[76:78])
@@ -182,6 +182,7 @@ class EXTHHeader(object):
self.records = []
for i in xrange(self.count):
pos = self.read_record(pos)
+ self.records.sort(key=lambda x:x.type)
def read_record(self, pos):
type_, length = struct.unpack(b'>II', self.raw[pos:pos+8])
@@ -290,7 +291,12 @@ class MOBIHeader(object): # {{{
(self.fcis_number, self.fcis_count, self.flis_number,
self.flis_count) = struct.unpack(b'>IIII',
self.raw[200:216])
- self.unknown6 = self.raw[216:240]
+ self.unknown6 = self.raw[216:224]
+ self.srcs_record_index = struct.unpack(b'>I',
+ self.raw[224:228])[0]
+ self.num_srcs_records = struct.unpack(b'>I',
+ self.raw[228:232])[0]
+ self.unknown7 = self.raw[232:240]
self.extra_data_flags = struct.unpack(b'>I',
self.raw[240:244])[0]
self.has_multibytes = bool(self.extra_data_flags & 0b1)
@@ -339,7 +345,7 @@ class MOBIHeader(object): # {{{
ans.append('Huffman record offset: %d'%self.huffman_record_offset)
ans.append('Huffman record count: %d'%self.huffman_record_count)
ans.append('Unknown2: %r'%self.unknown2)
- ans.append('EXTH flags: %r (%s)'%(self.exth_flags, self.has_exth))
+ ans.append('EXTH flags: %s (%s)'%(bin(self.exth_flags)[2:], self.has_exth))
if self.has_drm_data:
ans.append('Unknown3: %r'%self.unknown3)
ans.append('DRM Offset: %s'%self.drm_offset)
@@ -356,6 +362,9 @@ class MOBIHeader(object): # {{{
ans.append('FLIS number: %d'% self.flis_number)
ans.append('FLIS count: %d'% self.flis_count)
ans.append('Unknown6: %r'% self.unknown6)
+ ans.append('SRCS record index: %d'%self.srcs_record_index)
+ ans.append('Number of SRCS records?: %d'%self.num_srcs_records)
+ ans.append('Unknown7: %r'%self.unknown7)
ans.append(('Extra data flags: %s (has multibyte: %s) '
'(has indexing: %s) (has uncrossable breaks: %s)')%(
bin(self.extra_data_flags), self.has_multibytes,
@@ -416,12 +425,7 @@ class IndexHeader(object): # {{{
if self.index_encoding == 'unknown':
raise ValueError(
'Unknown index encoding: %d'%self.index_encoding_num)
- self.locale_raw, = struct.unpack(b'>I', raw[32:36])
- langcode = self.locale_raw
- langid = langcode & 0xFF
- sublangid = (langcode >> 10) & 0xFF
- self.language = main_language.get(langid, 'ENGLISH')
- self.sublanguage = sub_language.get(sublangid, 'NEUTRAL')
+ self.possibly_language = raw[32:36]
self.num_index_entries, = struct.unpack('>I', raw[36:40])
self.ordt_start, = struct.unpack('>I', raw[40:44])
self.ligt_start, = struct.unpack('>I', raw[44:48])
@@ -481,8 +485,7 @@ class IndexHeader(object): # {{{
a('Number of index records: %d'%self.index_count)
a('Index encoding: %s (%d)'%(self.index_encoding,
self.index_encoding_num))
- a('Index language: %s - %s (%s)'%(self.language, self.sublanguage,
- hex(self.locale_raw)))
+ a('Unknown (possibly language?): %r'%(self.possibly_language))
a('Number of index entries: %d'% self.num_index_entries)
a('ORDT start: %d'%self.ordt_start)
a('LIGT start: %d'%self.ligt_start)
@@ -602,6 +605,9 @@ class IndexEntry(object): # {{{
self.raw = raw
self.tags = []
self.entry_type_raw = entry_type
+ self.byte_size = len(raw)
+
+ orig_raw = raw
try:
self.entry_type = self.TYPES[entry_type]
@@ -639,8 +645,8 @@ class IndexEntry(object): # {{{
self.tags.append(Tag(aut_tag[0], [val], self.entry_type,
cncx))
- if raw.replace(b'\x00', b''): # There can be padding null bytes
- raise ValueError('Extra bytes in INDX table entry %d: %r'%(self.index, raw))
+ self.consumed = len(orig_raw) - len(raw)
+ self.trailing_bytes = raw
@property
def label(self):
@@ -692,13 +698,16 @@ class IndexEntry(object): # {{{
return -1
def __str__(self):
- ans = ['Index Entry(index=%s, entry_type=%s (%s), length=%d)'%(
- self.index, self.entry_type, bin(self.entry_type_raw)[2:], len(self.tags))]
+ ans = ['Index Entry(index=%s, entry_type=%s (%s), length=%d, byte_size=%d)'%(
+ self.index, self.entry_type, bin(self.entry_type_raw)[2:],
+ len(self.tags), self.byte_size)]
for tag in self.tags:
ans.append('\t'+str(tag))
if self.first_child_index != -1:
ans.append('\tNumber of children: %d'%(self.last_child_index -
self.first_child_index + 1))
+ if self.trailing_bytes:
+ ans.append('\tTrailing bytes: %r'%self.trailing_bytes)
return '\n'.join(ans)
# }}}
@@ -742,6 +751,7 @@ class IndexRecord(object): # {{{
raise ValueError('Extra bytes after IDXT table: %r'%rest)
indxt = raw[192:self.idxt_offset]
+ self.size_of_indxt_block = len(indxt)
self.indices = []
for i, off in enumerate(self.index_offsets):
try:
@@ -754,10 +764,14 @@ class IndexRecord(object): # {{{
if index_header.index_type == 6:
flags = ord(indxt[off+consumed+d])
d += 1
+ pos = off+consumed+d
self.indices.append(IndexEntry(index, entry_type,
- indxt[off+consumed+d:next_off], cncx,
+ indxt[pos:next_off], cncx,
index_header.tagx_entries, flags=flags))
- index = self.indices[-1]
+
+ rest = indxt[pos+self.indices[-1].consumed:]
+ if rest.replace(b'\0', ''): # There can be padding null bytes
+ raise ValueError('Extra bytes after IDXT table: %r'%rest)
def get_parent(self, index):
if index.depth < 1:
@@ -778,12 +792,13 @@ class IndexRecord(object): # {{{
u(self.unknown1)
a('Unknown (header type? index record number? always 1?): %d'%self.header_type)
u(self.unknown2)
- a('IDXT Offset: %d'%self.idxt_offset)
+ a('IDXT Offset (%d block size): %d'%(self.size_of_indxt_block,
+ self.idxt_offset))
a('IDXT Count: %d'%self.idxt_count)
u(self.unknown3)
u(self.unknown4)
a('Index offsets: %r'%self.index_offsets)
- a('\nIndex Entries:')
+ a('\nIndex Entries (%d entries):'%len(self.indices))
for entry in self.indices:
a(str(entry)+'\n')
@@ -829,6 +844,7 @@ class TextRecord(object): # {{{
def __init__(self, idx, record, extra_data_flags, decompress):
self.trailing_data, self.raw = get_trailing_data(record.raw, extra_data_flags)
+ raw_trailing_bytes = record.raw[len(self.raw):]
self.raw = decompress(self.raw)
if 0 in self.trailing_data:
self.trailing_data['multibyte_overlap'] = self.trailing_data.pop(0)
@@ -836,6 +852,7 @@ class TextRecord(object): # {{{
self.trailing_data['indexing'] = self.trailing_data.pop(1)
if 2 in self.trailing_data:
self.trailing_data['uncrossable_breaks'] = self.trailing_data.pop(2)
+ self.trailing_data['raw_bytes'] = raw_trailing_bytes
self.idx = idx
@@ -957,15 +974,17 @@ class TBSIndexing(object): # {{{
return str({bin4(k):v for k, v in extra.iteritems()})
tbs_type = 0
+ is_periodical = self.doc_type in (257, 258, 259)
if len(byts):
- outermost_index, extra, consumed = decode_tbs(byts)
+ outermost_index, extra, consumed = decode_tbs(byts, flag_size=4 if
+ is_periodical else 3)
byts = byts[consumed:]
for k in extra:
tbs_type |= k
ans.append('\nTBS: %d (%s)'%(tbs_type, bin4(tbs_type)))
ans.append('Outermost index: %d'%outermost_index)
ans.append('Unknown extra start bytes: %s'%repr_extra(extra))
- if self.doc_type in (257, 259): # Hierarchical periodical
+ if is_periodical: # Hierarchical periodical
byts, a = self.interpret_periodical(tbs_type, byts,
dat['geom'][0])
ans += a
@@ -1028,6 +1047,7 @@ class TBSIndexing(object): # {{{
# }}}
def read_starting_section(byts): # {{{
+ orig = byts
si, extra, consumed = decode_tbs(byts)
byts = byts[consumed:]
if len(extra) > 1 or 0b0010 in extra or 0b1000 in extra:
@@ -1044,8 +1064,8 @@ class TBSIndexing(object): # {{{
eof = extra[0b0001]
if eof != 0:
raise ValueError('Unknown eof value %s when reading'
- ' starting section'%eof)
- ans.append('This record is spanned by an article from'
+ ' starting section. All bytes: %r'%(eof, orig))
+ ans.append('??This record has more than one article from '
' the section: %d'%si.index)
return si, byts
# }}}
diff --git a/src/calibre/ebooks/mobi/kindlegen.py b/src/calibre/ebooks/mobi/kindlegen.py
new file mode 100644
index 0000000000..9696b82971
--- /dev/null
+++ b/src/calibre/ebooks/mobi/kindlegen.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+import os, subprocess, shutil
+
+from lxml import etree
+
+from calibre.constants import iswindows
+from calibre.customize.ui import plugin_for_output_format
+from calibre.ptempfile import TemporaryDirectory
+from calibre.ebooks.mobi.utils import detect_periodical
+from calibre import CurrentDir
+
+exe = 'kindlegen.exe' if iswindows else 'kindlegen'
+
+def refactor_opf(opf, is_periodical, toc):
+ with open(opf, 'rb') as f:
+ root = etree.fromstring(f.read())
+ '''
+ for spine in root.xpath('//*[local-name() = "spine" and @toc]'):
+ # Do not use the NCX toc as kindlegen requires the section structure
+ # in the TOC to be duplicated in the HTML, asinine!
+ del spine.attrib['toc']
+ '''
+ if is_periodical:
+ metadata = root.xpath('//*[local-name() = "metadata"]')[0]
+ xm = etree.SubElement(metadata, 'x-metadata')
+ xm.tail = '\n'
+ xm.text = '\n\t'
+ mobip = etree.SubElement(xm, 'output', attrib={'encoding':"utf-8",
+ 'content-type':"application/x-mobipocket-subscription-magazine"})
+ mobip.tail = '\n\t'
+ with open(opf, 'wb') as f:
+ f.write(etree.tostring(root, method='xml', encoding='utf-8',
+ xml_declaration=True))
+
+
+def refactor_guide(oeb):
+ for key in list(oeb.guide):
+ if key not in ('toc', 'start', 'masthead'):
+ oeb.guide.remove(key)
+
+def run_kindlegen(opf, log):
+ log.info('Running kindlegen on MOBIML created by calibre')
+ oname = os.path.splitext(opf)[0] + '.mobi'
+ p = subprocess.Popen([exe, opf, '-c1', '-verbose', '-o', oname],
+ stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
+ ko = p.stdout.read()
+ returncode = p.wait()
+ log.debug('kindlegen verbose output:')
+ log.debug(ko.decode('utf-8', 'replace'))
+ log.info('kindlegen returned returncode: %d'%returncode)
+ if not os.path.exists(oname) or os.stat(oname).st_size < 100:
+ raise RuntimeError('kindlegen did not produce any output. '
+ 'kindlegen return code: %d'%returncode)
+ return oname
+
+def kindlegen(oeb, opts, input_plugin, output_path):
+ is_periodical = detect_periodical(oeb.toc, oeb.log)
+ refactor_guide(oeb)
+ with TemporaryDirectory('_kindlegen_output') as tdir:
+ oeb_output = plugin_for_output_format('oeb')
+ oeb_output.convert(oeb, tdir, input_plugin, opts, oeb.log)
+ opf = [x for x in os.listdir(tdir) if x.endswith('.opf')][0]
+ refactor_opf(os.path.join(tdir, opf), is_periodical, oeb.toc)
+ try:
+ if os.path.exists('/tmp/kindlegen'):
+ shutil.rmtree('/tmp/kindlegen')
+ shutil.copytree(tdir, '/tmp/kindlegen')
+ oeb.log('kindlegen intermediate output stored in: /tmp/kindlegen')
+ except:
+ pass
+
+ with CurrentDir(tdir):
+ oname = run_kindlegen(opf, oeb.log)
+ shutil.copyfile(oname, output_path)
+
+
diff --git a/src/calibre/ebooks/mobi/output.py b/src/calibre/ebooks/mobi/output.py
index 669d41fa8f..ba88aa7779 100644
--- a/src/calibre/ebooks/mobi/output.py
+++ b/src/calibre/ebooks/mobi/output.py
@@ -50,6 +50,12 @@ class MOBIOutput(OutputFormatPlugin):
help=_('When adding the Table of Contents to the book, add it at the start of the '
'book instead of the end. Not recommended.')
),
+ OptionRecommendation(name='kindlegen',
+ recommended_value=False,
+ help=('Use kindlegen (must be in your PATH) to generate the'
+ ' binary wrapper for the MOBI format. Useful to debug '
+ ' the calibre MOBI output.')
+ ),
])
@@ -164,7 +170,11 @@ class MOBIOutput(OutputFormatPlugin):
MobiWriter
else:
from calibre.ebooks.mobi.writer import MobiWriter
- writer = MobiWriter(opts,
- write_page_breaks_after_item=write_page_breaks_after_item)
- writer(oeb, output_path)
+ if opts.kindlegen:
+ from calibre.ebooks.mobi.kindlegen import kindlegen
+ kindlegen(oeb, opts, input_plugin, output_path)
+ else:
+ writer = MobiWriter(opts,
+ write_page_breaks_after_item=write_page_breaks_after_item)
+ writer(oeb, output_path)
diff --git a/src/calibre/ebooks/mobi/utils.py b/src/calibre/ebooks/mobi/utils.py
index 37d2093066..4298276bc1 100644
--- a/src/calibre/ebooks/mobi/utils.py
+++ b/src/calibre/ebooks/mobi/utils.py
@@ -41,6 +41,9 @@ def encode_number_as_hex(num):
number.
'''
num = bytes(hex(num)[2:].upper())
+ nlen = len(num)
+ if nlen % 2 != 0:
+ num = b'0'+num
ans = bytearray(num)
ans.insert(0, len(num))
return bytes(ans)
@@ -66,11 +69,14 @@ def encint(value, forward=True):
If forward is True the bytes returned are suitable for prepending to the
output buffer, otherwise they must be append to the output buffer.
'''
+ if value < 0:
+ raise ValueError('Cannot encode negative numbers as vwi')
# Encode vwi
byts = bytearray()
while True:
b = value & 0b01111111
value >>= 7 # shift value to the right by 7 bits
+
byts.append(b)
if value == 0:
break
@@ -185,7 +191,7 @@ def encode_trailing_data(raw):
where size is a backwards encoded vwi whose value is the length of the
- entire return bytestring.
+ entire returned bytestring. data is the bytestring passed in as raw.
This is the encoding used for trailing data entries at the end of text
records. See get_trailing_data() for details.
@@ -198,24 +204,31 @@ def encode_trailing_data(raw):
lsize += 1
return raw + encoded
-def encode_fvwi(val, flags):
+def encode_fvwi(val, flags, flag_size=4):
'''
- Encode the value val and the 4 bit flags flags as a fvwi. This encoding is
+ Encode the value val and the flag_size bits from flags as a fvwi. This encoding is
used in the trailing byte sequences for indexing. Returns encoded
bytestring.
'''
- ans = (val << 4) | (flags & 0b1111)
+ ans = val << flag_size
+ for i in xrange(flag_size):
+ ans |= (flags & (1 << i))
return encint(ans)
-def decode_fvwi(byts):
+def decode_fvwi(byts, flag_size=4):
'''
Decode encoded fvwi. Returns number, flags, consumed
'''
arg, consumed = decint(bytes(byts))
- return (arg >> 4), (arg & 0b1111), consumed
+ val = arg >> flag_size
+ flags = 0
+ for i in xrange(flag_size):
+ flags |= (arg & (1 << i))
+ return val, flags, consumed
-def decode_tbs(byts):
+
+def decode_tbs(byts, flag_size=4):
'''
Trailing byte sequences for indexing consists of series of fvwi numbers.
This function reads the fvwi number and its associated flags. It them uses
@@ -226,10 +239,10 @@ def decode_tbs(byts):
data and the number of bytes consumed.
'''
byts = bytes(byts)
- val, flags, consumed = decode_fvwi(byts)
+ val, flags, consumed = decode_fvwi(byts, flag_size=flag_size)
extra = {}
byts = byts[consumed:]
- if flags & 0b1000:
+ if flags & 0b1000 and flag_size > 3:
extra[0b1000] = True
if flags & 0b0010:
x, consumed2 = decint(byts)
@@ -247,7 +260,7 @@ def decode_tbs(byts):
consumed += consumed2
return val, extra, consumed
-def encode_tbs(val, extra):
+def encode_tbs(val, extra, flag_size=4):
'''
Encode the number val and the extra data in the extra dict as an fvwi. See
decode_tbs above.
@@ -255,7 +268,7 @@ def encode_tbs(val, extra):
flags = 0
for flag in extra:
flags |= flag
- ans = encode_fvwi(val, flags)
+ ans = encode_fvwi(val, flags, flag_size=flag_size)
if 0b0010 in extra:
ans += encint(extra[0b0010])
@@ -289,5 +302,33 @@ def align_block(raw, multiple=4, pad=b'\0'):
return raw + pad*(multiple - extra)
+def detect_periodical(toc, log=None):
+ '''
+ Detect if the TOC object toc contains a periodical that conforms to the
+ structure required by kindlegen to generate a periodical.
+ '''
+ for node in toc.iterdescendants():
+ if node.depth() == 1 and node.klass != 'article':
+ if log is not None:
+ log.debug(
+ 'Not a periodical: Deepest node does not have '
+ 'class="article"')
+ return False
+ if node.depth() == 2 and node.klass != 'section':
+ if log is not None:
+ log.debug(
+ 'Not a periodical: Second deepest node does not have'
+ ' class="section"')
+ return False
+ if node.depth() == 3 and node.klass != 'periodical':
+ if log is not None:
+ log.debug('Not a periodical: Third deepest node'
+ ' does not have class="periodical"')
+ return False
+ if node.depth() > 3:
+ if log is not None:
+ log.debug('Not a periodical: Has nodes of depth > 3')
+ return False
+ return True
diff --git a/src/calibre/ebooks/mobi/writer2/indexer.py b/src/calibre/ebooks/mobi/writer2/indexer.py
index 0f7a670cff..15207c0230 100644
--- a/src/calibre/ebooks/mobi/writer2/indexer.py
+++ b/src/calibre/ebooks/mobi/writer2/indexer.py
@@ -14,8 +14,7 @@ from collections import OrderedDict, defaultdict
from calibre.ebooks.mobi.writer2 import RECORD_SIZE
from calibre.ebooks.mobi.utils import (encint, encode_number_as_hex,
- encode_trailing_data, encode_tbs, align_block, utf8_text)
-from calibre.ebooks.mobi.langcodes import iana2mobi
+ encode_tbs, align_block, utf8_text, detect_periodical)
class CNCX(object): # {{{
@@ -28,13 +27,12 @@ class CNCX(object): # {{{
MAX_STRING_LENGTH = 500
- def __init__(self, toc, opts):
+ def __init__(self, toc, is_periodical):
self.strings = OrderedDict()
- for item in toc:
- if item is self.toc: continue
+ for item in toc.iterdescendants(breadth_first=True):
self.strings[item.title] = 0
- if opts.mobi_periodical:
+ if is_periodical:
self.strings[item.klass] = 0
self.records = []
@@ -53,11 +51,10 @@ class CNCX(object): # {{{
self.records.append(buf.getvalue())
buf.truncate(0)
offset = len(self.records) * 0x10000
-
+ buf.write(raw)
self.strings[key] = offset
offset += len(raw)
- buf.write(b'\0') # CNCX must end with zero byte
self.records.append(align_block(buf.getvalue()))
def __getitem__(self, string):
@@ -91,6 +88,17 @@ class IndexEntry(object): # {{{
self.first_child_index = None
self.last_child_index = None
+ def __repr__(self):
+ return ('IndexEntry(offset=%r, depth=%r, length=%r, index=%r,'
+ ' parent_index=%r)')%(self.offset, self.depth, self.length,
+ self.index, self.parent_index)
+
+ @dynamic_property
+ def size(self):
+ def fget(self): return self.length
+ def fset(self, val): self.length = val
+ return property(fget=fget, fset=fset, doc='Alias for length')
+
@classmethod
def tagx_block(cls, for_periodical=True):
buf = bytearray()
@@ -115,7 +123,7 @@ class IndexEntry(object): # {{{
buf.append(1)
header = b'TAGX'
- header += pack(b'>I', len(buf)) # table length
+ header += pack(b'>I', 12+len(buf)) # table length
header += pack(b'>I', 1) # control byte count
return header + bytes(buf)
@@ -137,7 +145,7 @@ class IndexEntry(object): # {{{
def entry_type(self):
ans = 0
for tag in self.tag_nums:
- ans |= (1 << self.BITMASKS[tag]) # 1 << x == 2**x
+ ans |= (1 << self.BITMASKS.index(tag)) # 1 << x == 2**x
return ans
@property
@@ -152,7 +160,7 @@ class IndexEntry(object): # {{{
val = getattr(self, attr)
buf.write(encint(val))
- ans = buf.get_value()
+ ans = buf.getvalue()
return ans
# }}}
@@ -164,25 +172,35 @@ class TBS(object): # {{{
trailing byte sequence for the record.
'''
- def __init__(self, data, is_periodical, first=False, all_sections=[]):
- if not data:
- self.bytestring = encode_trailing_data(b'')
- else:
- self.section_map = OrderedDict((i.index, i) for i in
- sorted(all_sections, key=lambda x:x.offset))
-
- if is_periodical:
- # The starting bytes.
- # The value is zero which I think indicates the periodical
- # index entry. The values for the various flags seem to be
- # unused. If the 0b0100 is present, it means that the record
- # deals with section 1 (or is the final record with section
- # transitions).
- self.type_010 = encode_tbs(0, {0b0010: 0})
- self.type_011 = encode_tbs(0, {0b0010: 0, 0b0001: 0})
- self.type_110 = encode_tbs(0, {0b0100: 2, 0b0010: 0})
- self.type_111 = encode_tbs(0, {0b0100: 2, 0b0010: 0, 0b0001: 0})
+ def __init__(self, data, is_periodical, first=False, section_map={},
+ after_first=False):
+ self.section_map = section_map
+ #import pprint
+ #pprint.pprint(data)
+ #print()
+ if is_periodical:
+ # The starting bytes.
+ # The value is zero which I think indicates the periodical
+ # index entry. The values for the various flags seem to be
+ # unused. If the 0b100 is present, it means that the record
+ # deals with section 1 (or is the final record with section
+ # transitions).
+ self.type_010 = encode_tbs(0, {0b010: 0}, flag_size=3)
+ self.type_011 = encode_tbs(0, {0b010: 0, 0b001: 0},
+ flag_size=3)
+ self.type_110 = encode_tbs(0, {0b100: 2, 0b010: 0},
+ flag_size=3)
+ self.type_111 = encode_tbs(0, {0b100: 2, 0b010: 0, 0b001:
+ 0}, flag_size=3)
+ if not data:
+ byts = b''
+ if after_first:
+ # This can happen if a record contains only text between
+ # the periodical start and the first section
+ byts = self.type_011
+ self.bytestring = byts
+ else:
depth_map = defaultdict(list)
for x in ('starts', 'ends', 'completes'):
for idx in data[x]:
@@ -190,30 +208,43 @@ class TBS(object): # {{{
for l in depth_map.itervalues():
l.sort(key=lambda x:x.offset)
self.periodical_tbs(data, first, depth_map)
+ else:
+ if not data:
+ self.bytestring = b''
else:
self.book_tbs(data, first)
def periodical_tbs(self, data, first, depth_map):
buf = StringIO()
- has_section_start = (depth_map[1] and depth_map[1][0] in
- data['starts'])
+ has_section_start = (depth_map[1] and
+ set(depth_map[1]).intersection(set(data['starts'])))
spanner = data['spans']
- first_node = None
- for nodes in depth_map.values():
- for node in nodes:
- if (first_node is None or (node.offset, node.depth) <
- (first_node.offset, first_node.depth)):
- first_node = node
-
parent_section_index = -1
+
if depth_map[0]:
# We have a terminal record
+
+ # Find the first non periodical node
+ first_node = None
+ for nodes in (depth_map[1], depth_map[2]):
+ for node in nodes:
+ if (first_node is None or (node.offset, node.depth) <
+ (first_node.offset, first_node.depth)):
+ first_node = node
+
typ = (self.type_110 if has_section_start else self.type_010)
- if first_node.depth > 0:
+
+ # parent_section_index is needed for the last record
+ if first_node is not None and first_node.depth > 0:
parent_section_index = (first_node.index if first_node.depth
== 1 else first_node.parent_index)
+ else:
+ parent_section_index = max(self.section_map.iterkeys())
+
else:
+ # Non terminal record
+
if spanner is not None:
# record is spanned by a single article
parent_section_index = spanner.parent_index
@@ -221,31 +252,37 @@ class TBS(object): # {{{
self.type_010)
elif not depth_map[1]:
# has only article nodes, i.e. spanned by a section
- parent_section_index = self.depth_map[2][0].parent_index
+ parent_section_index = depth_map[2][0].parent_index
typ = (self.type_111 if parent_section_index == 1 else
self.type_010)
else:
# has section transitions
- parent_section_index = self.depth_map[2][0].parent_index
+ if depth_map[2]:
+ parent_section_index = depth_map[2][0].parent_index
+ else:
+ parent_section_index = depth_map[1][0].index
+ typ = self.type_011
buf.write(typ)
- if parent_section_index > 1:
+ if typ not in (self.type_110, self.type_111) and parent_section_index > 0:
+ extra = {}
# Write starting section information
if spanner is None:
- num_articles = len(depth_map[1])
- extra = {}
+ num_articles = len([a for a in depth_map[1] if a.parent_index
+ == parent_section_index])
+ if not depth_map[1]:
+ extra = {0b0001: 0}
if num_articles > 1:
extra = {0b0100: num_articles}
- else:
- extra = {0b0001: 0}
buf.write(encode_tbs(parent_section_index, extra))
if spanner is None:
articles = depth_map[2]
- sections = [self.section_map[a.parent_index] for a in articles]
- sections.sort(key=lambda x:x.offset)
- section_map = {s:[a for a in articles is a.parent_index ==
+ sections = set([self.section_map[a.parent_index] for a in
+ articles])
+ sections = sorted(sections, key=lambda x:x.offset)
+ section_map = {s:[a for a in articles if a.parent_index ==
s.index] for s in sections}
for i, section in enumerate(sections):
# All the articles in this record that belong to section
@@ -257,15 +294,15 @@ class TBS(object): # {{{
try:
next_sec = sections[i+1]
except:
- next_sec == None
+ next_sec = None
extra = {}
if num > 1:
extra[0b0100] = num
- if i == 0 and next_sec is not None:
+ if False and i == 0 and next_sec is not None:
# Write offset to next section from start of record
- # For some reason kindlegen only writes this offset
- # for the first section transition. Imitate it.
+ # I can't figure out exactly when Kindlegen decides to
+ # write this so I have disabled it for now.
extra[0b0001] = next_sec.offset - data['offset']
buf.write(encode_tbs(first_article.index-section.index, extra))
@@ -277,10 +314,10 @@ class TBS(object): # {{{
buf.write(encode_tbs(spanner.index - parent_section_index,
{0b0001: 0}))
- self.bytestring = encode_trailing_data(buf.getvalue())
+ self.bytestring = buf.getvalue()
def book_tbs(self, data, first):
- self.bytestring = encode_trailing_data(b'')
+ self.bytestring = b''
# }}}
class Indexer(object): # {{{
@@ -295,18 +332,18 @@ class Indexer(object): # {{{
self.log = oeb.log
self.opts = opts
- self.is_periodical = self.detect_periodical()
+ self.is_periodical = detect_periodical(self.oeb.toc, self.log)
self.log('Generating MOBI index for a %s'%('periodical' if
self.is_periodical else 'book'))
self.is_flat_periodical = False
- if opts.mobi_periodical:
+ if self.is_periodical:
periodical_node = iter(oeb.toc).next()
sections = tuple(periodical_node)
self.is_flat_periodical = len(sections) == 1
self.records = []
- self.cncx = CNCX(oeb.toc, opts)
+ self.cncx = CNCX(oeb.toc, self.is_periodical)
if self.is_periodical:
self.indices = self.create_periodical_index()
@@ -319,28 +356,6 @@ class Indexer(object): # {{{
self.calculate_trailing_byte_sequences()
- def detect_periodical(self): # {{{
- for node in self.oeb.toc.iterdescendants():
- if node.depth() == 1 and node.klass != 'article':
- self.log.debug(
- 'Not a periodical: Deepest node does not have '
- 'class="article"')
- return False
- if node.depth() == 2 and node.klass != 'section':
- self.log.debug(
- 'Not a periodical: Second deepest node does not have'
- ' class="section"')
- return False
- if node.depth() == 3 and node.klass != 'periodical':
- self.log.debug('Not a periodical: Third deepest node'
- ' does not have class="periodical"')
- return False
- if node.depth() > 3:
- self.log.debug('Not a periodical: Has nodes of depth > 3')
- return False
- return True
- # }}}
-
def create_index_record(self): # {{{
header_length = 192
buf = StringIO()
@@ -405,14 +420,13 @@ class Indexer(object): # {{{
buf.write(pack(b'>I', 0)) # Filled in later
# Number of index records 24-28
- buf.write(pack('b>I', len(self.records)))
+ buf.write(pack(b'>I', len(self.records)))
# Index Encoding 28-32
buf.write(pack(b'>I', 65001)) # utf-8
- # Index language 32-36
- buf.write(iana2mobi(
- str(self.oeb.metadata.language[0])))
+ # Unknown 32-36
+ buf.write(b'\xff'*4)
# Number of index entries 36-40
buf.write(pack(b'>I', len(self.indices)))
@@ -457,7 +471,7 @@ class Indexer(object): # {{{
idxt_offset = buf.tell()
buf.write(b'IDXT')
- buf.write(header_length + len(tagx_block))
+ buf.write(pack(b'>H', header_length + len(tagx_block)))
buf.write(b'\0')
buf.seek(20)
buf.write(pack(b'>I', idxt_offset))
@@ -481,12 +495,12 @@ class Indexer(object): # {{{
continue
seen.add(offset)
index = IndexEntry(offset, label)
- self.indices.append(index)
+ indices.append(index)
indices.sort(key=lambda x:x.offset)
# Set lengths
- for i, index in indices:
+ for i, index in enumerate(indices):
try:
next_offset = indices[i+1].offset
except:
@@ -497,11 +511,11 @@ class Indexer(object): # {{{
indices = [i for i in indices if i.length > 0]
# Set index values
- for i, index in indices:
+ for i, index in enumerate(indices):
index.index = i
# Set lengths again to close up any gaps left by filtering
- for i, index in indices:
+ for i, index in enumerate(indices):
try:
next_offset = indices[i+1].offset
except:
@@ -567,7 +581,7 @@ class Indexer(object): # {{{
for s, x in enumerate(normalized_sections):
sec, normalized_articles = x
try:
- sec.length = normalized_sections[s+1].offset - sec.offset
+ sec.length = normalized_sections[s+1][0].offset - sec.offset
except:
sec.length = self.serializer.body_end_offset - sec.offset
for i, art in enumerate(normalized_articles):
@@ -583,17 +597,18 @@ class Indexer(object): # {{{
normalized_articles))
normalized_sections[i] = (sec, normalized_articles)
- normalized_sections = list(filter(lambda x: x[0].size > 0 and x[1],
+ normalized_sections = list(filter(lambda x: x[0].length > 0 and x[1],
normalized_sections))
# Set indices
i = 0
- for sec, normalized_articles in normalized_sections:
+ for sec, articles in normalized_sections:
i += 1
sec.index = i
+ sec.parent_index = 0
- for sec, normalized_articles in normalized_sections:
- for art in normalized_articles:
+ for sec, articles in normalized_sections:
+ for art in articles:
i += 1
art.index = i
art.parent_index = sec.index
@@ -606,7 +621,7 @@ class Indexer(object): # {{{
for s, x in enumerate(normalized_sections):
sec, articles = x
try:
- next_offset = normalized_sections[s+1].offset
+ next_offset = normalized_sections[s+1][0].offset
except:
next_offset = self.serializer.body_end_offset
sec.length = next_offset - sec.offset
@@ -622,7 +637,7 @@ class Indexer(object): # {{{
for s, x in enumerate(normalized_sections):
sec, articles = x
try:
- next_sec = normalized_sections[s+1]
+ next_sec = normalized_sections[s+1][0]
except:
if (sec.length == 0 or sec.next_offset !=
self.serializer.body_end_offset):
@@ -659,15 +674,24 @@ class Indexer(object): # {{{
self.tbs_map = {}
found_node = False
sections = [i for i in self.indices if i.depth == 1]
+ section_map = OrderedDict((i.index, i) for i in
+ sorted(sections, key=lambda x:x.offset))
+
+ deepest = max(i.depth for i in self.indices)
+
for i in xrange(self.number_of_text_records):
offset = i * RECORD_SIZE
next_offset = offset + RECORD_SIZE
- data = OrderedDict([('ends',[]), ('completes',[]), ('starts',[]),
- ('spans', None), ('offset', offset)])
+ data = {'ends':[], 'completes':[], 'starts':[],
+ 'spans':None, 'offset':offset, 'record_number':i+1}
+
for index in self.indices:
if index.offset >= next_offset:
# Node starts after current record
- break
+ if index.depth == deepest:
+ break
+ else:
+ continue
if index.next_offset <= offset:
# Node ends before current record
continue
@@ -683,15 +707,17 @@ class Indexer(object): # {{{
if index.next_offset <= next_offset:
# Node ends in current record
data['ends'].append(index)
- else:
+ elif index.depth == deepest:
data['spans'] = index
+
if (data['ends'] or data['completes'] or data['starts'] or
data['spans'] is not None):
self.tbs_map[i+1] = TBS(data, self.is_periodical, first=not
- found_node, all_sections=sections)
+ found_node, section_map=section_map)
found_node = True
else:
- self.tbs_map[i+1] = TBS({}, self.is_periodical, first=False)
+ self.tbs_map[i+1] = TBS({}, self.is_periodical, first=False,
+ after_first=found_node, section_map=section_map)
def get_trailing_byte_sequence(self, num):
return self.tbs_map[num].bytestring
diff --git a/src/calibre/ebooks/mobi/writer2/main.py b/src/calibre/ebooks/mobi/writer2/main.py
index 06572f48c4..2c57a9e461 100644
--- a/src/calibre/ebooks/mobi/writer2/main.py
+++ b/src/calibre/ebooks/mobi/writer2/main.py
@@ -19,7 +19,7 @@ from calibre.ebooks.mobi.langcodes import iana2mobi
from calibre.utils.filenames import ascii_filename
from calibre.ebooks.mobi.writer2 import (PALMDOC, UNCOMPRESSED, RECORD_SIZE)
from calibre.ebooks.mobi.utils import (rescale_image, encint,
- encode_trailing_data)
+ encode_trailing_data, align_block)
from calibre.ebooks.mobi.writer2.indexer import Indexer
EXTH_CODES = {
@@ -29,7 +29,6 @@ EXTH_CODES = {
'identifier': 104,
'subject': 105,
'pubdate': 106,
- 'date': 106,
'review': 107,
'contributor': 108,
'rights': 109,
@@ -55,6 +54,7 @@ class MobiWriter(object):
self.last_text_record_idx = 1
def __call__(self, oeb, path_or_stream):
+ self.log = oeb.log
if hasattr(path_or_stream, 'write'):
return self.dump_stream(oeb, path_or_stream)
with open(path_or_stream, 'w+b') as stream:
@@ -90,6 +90,7 @@ class MobiWriter(object):
self.primary_index_record_idx = None
try:
self.indexer = Indexer(self.serializer, self.last_text_record_idx,
+ len(self.records[self.last_text_record_idx]),
self.opts, self.oeb)
except:
self.log.exception('Failed to generate MOBI index:')
@@ -98,9 +99,13 @@ class MobiWriter(object):
for i in xrange(len(self.records)):
if i == 0: continue
tbs = self.indexer.get_trailing_byte_sequence(i)
- self.records[i] += tbs
+ self.records[i] += encode_trailing_data(tbs)
self.records.extend(self.indexer.records)
+ @property
+ def is_periodical(self):
+ return (self.primary_index_record_idx is None or not
+ self.indexer.is_periodical)
# }}}
@@ -193,7 +198,6 @@ class MobiWriter(object):
self.serializer = Serializer(self.oeb, self.images,
write_page_breaks_after_item=self.write_page_breaks_after_item)
text = self.serializer()
- self.content_length = len(text)
self.text_length = len(text)
text = StringIO(text)
nrecords = 0
@@ -201,21 +205,16 @@ class MobiWriter(object):
if self.compression != UNCOMPRESSED:
self.oeb.logger.info(' Compressing markup content...')
- data, overlap = self.read_text_record(text)
-
- while len(data) > 0:
+ while text.tell() < self.text_length:
+ data, overlap = self.read_text_record(text)
if self.compression == PALMDOC:
data = compress_doc(data)
- record = StringIO()
- record.write(data)
- self.records.append(record.getvalue())
+ data += overlap
+ data += pack(b'>B', len(overlap))
+
+ self.records.append(data)
nrecords += 1
- data, overlap = self.read_text_record(text)
-
- # Write information about the mutibyte character overlap, if any
- record.write(overlap)
- record.write(pack(b'>B', len(overlap)))
self.last_text_record_idx = nrecords
@@ -276,8 +275,19 @@ class MobiWriter(object):
exth = self.build_exth()
last_content_record = len(self.records) - 1
+ # FCIS/FLIS (Seem to server no purpose)
+ flis_number = len(self.records)
+ self.records.append(
+ b'FLIS\0\0\0\x08\0\x41\0\0\0\0\0\0\xff\xff\xff\xff\0\x01\0\x03\0\0\0\x03\0\0\0\x01'+
+ b'\xff'*4)
+ fcis = b'FCIS\x00\x00\x00\x14\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x00\x00'
+ fcis += pack(b'>I', self.text_length)
+ fcis += b'\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00\x08\x00\x01\x00\x01\x00\x00\x00\x00'
+ fcis_number = len(self.records)
+ self.records.append(fcis)
+
# EOF record
- self.records.append('\xE9\x8E\x0D\x0A')
+ self.records.append(b'\xE9\x8E\x0D\x0A')
record0 = StringIO()
# The MOBI Header
@@ -307,8 +317,15 @@ class MobiWriter(object):
# 0x10 - 0x13 : UID
# 0x14 - 0x17 : Generator version
+ bt = 0x002
+ if self.primary_index_record_idx is not None:
+ if self.indexer.is_flat_periodical:
+ bt = 0x102
+ elif self.indexer.is_periodical:
+ bt = 0x103
+
record0.write(pack(b'>IIIII',
- 0xe8, 0x002, 65001, uid, 6))
+ 0xe8, bt, 65001, uid, 6))
# 0x18 - 0x1f : Unknown
record0.write(b'\xff' * 8)
@@ -337,7 +354,8 @@ class MobiWriter(object):
# 0x58 - 0x5b : Format version
# 0x5c - 0x5f : First image record number
record0.write(pack(b'>II',
- 6, self.first_image_record if self.first_image_record else 0))
+ 6, self.first_image_record if self.first_image_record else
+ len(self.records)-1))
# 0x60 - 0x63 : First HUFF/CDIC record number
# 0x64 - 0x67 : Number of HUFF/CDIC records
@@ -346,7 +364,12 @@ class MobiWriter(object):
record0.write(b'\0' * 16)
# 0x70 - 0x73 : EXTH flags
- record0.write(pack(b'>I', 0x50))
+ # Bit 6 (0b1000000) being set indicates the presence of an EXTH header
+ # The purpose of the other bits is unknown
+ exth_flags = 0b1010000
+ if self.is_periodical:
+ exth_flags |= 0b1000
+ record0.write(pack(b'>I', exth_flags))
# 0x74 - 0x93 : Unknown
record0.write(b'\0' * 32)
@@ -371,13 +394,13 @@ class MobiWriter(object):
record0.write(b'\0\0\0\x01')
# 0xb8 - 0xbb : FCIS record number
- record0.write(pack(b'>I', 0xffffffff))
+ record0.write(pack(b'>I', fcis_number))
# 0xbc - 0xbf : Unknown (FCIS record count?)
- record0.write(pack(b'>I', 0xffffffff))
+ record0.write(pack(b'>I', 1))
# 0xc0 - 0xc3 : FLIS record number
- record0.write(pack(b'>I', 0xffffffff))
+ record0.write(pack(b'>I', flis_number))
# 0xc4 - 0xc7 : Unknown (FLIS record count?)
record0.write(pack(b'>I', 1))
@@ -411,7 +434,7 @@ class MobiWriter(object):
# Add some buffer so that Amazon can add encryption information if this
# MOBI is submitted for publication
record0 += (b'\0' * (1024*8))
- self.records[0] = record0
+ self.records[0] = align_block(record0)
# }}}
def build_exth(self): # EXTH Header {{{
@@ -469,25 +492,32 @@ class MobiWriter(object):
nrecs += 1
# Write cdetype
- if not self.opts.mobi_periodical:
+ if self.is_periodical:
data = b'EBOK'
exth.write(pack(b'>II', 501, len(data)+8))
exth.write(data)
nrecs += 1
# Add a publication date entry
- if oeb.metadata['date'] != [] :
+ if oeb.metadata['date']:
datestr = str(oeb.metadata['date'][0])
- elif oeb.metadata['timestamp'] != [] :
+ elif oeb.metadata['timestamp']:
datestr = str(oeb.metadata['timestamp'][0])
if datestr is not None:
+ datestr = bytes(datestr)
exth.write(pack(b'>II', EXTH_CODES['pubdate'], len(datestr) + 8))
exth.write(datestr)
nrecs += 1
else:
raise NotImplementedError("missing date or timestamp needed for mobi_periodical")
+ # Write the same creator info as kindlegen 1.2
+ for code, val in [(204, 201), (205, 1), (206, 2), (207, 33307)]:
+ exth.write(pack(b'>II', code, 12))
+ exth.write(pack(b'>I', val))
+ nrecs += 1
+
if (oeb.metadata.cover and
unicode(oeb.metadata.cover[0]) in oeb.manifest.ids):
id = unicode(oeb.metadata.cover[0])
@@ -514,7 +544,8 @@ class MobiWriter(object):
'''
Write the PalmDB header
'''
- title = ascii_filename(unicode(self.oeb.metadata.title[0]))
+ title = ascii_filename(unicode(self.oeb.metadata.title[0])).replace(
+ ' ', '_')
title = title + (b'\0' * (32 - len(title)))
now = int(time.time())
nrecords = len(self.records)
diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py
index fb1910d717..56f4a3ee96 100644
--- a/src/calibre/ebooks/oeb/base.py
+++ b/src/calibre/ebooks/oeb/base.py
@@ -1680,11 +1680,18 @@ class TOC(object):
return True
return False
- def iterdescendants(self):
+ def iterdescendants(self, breadth_first=False):
"""Iterate over all descendant nodes in depth-first order."""
- for child in self.nodes:
- for node in child.iter():
- yield node
+ if breadth_first:
+ for child in self.nodes:
+ yield child
+ for child in self.nodes:
+ for node in child.iterdescendants(breadth_first=True):
+ yield node
+ else:
+ for child in self.nodes:
+ for node in child.iter():
+ yield node
def __iter__(self):
"""Iterate over all immediate child nodes."""
diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py
index 516509fdd7..dc7f2edba9 100644
--- a/src/calibre/ebooks/pdf/writer.py
+++ b/src/calibre/ebooks/pdf/writer.py
@@ -165,6 +165,7 @@ class PDFWriter(QObject): # {{{
printer = get_pdf_printer(self.opts)
printer.setOutputFileName(item_path)
self.view.print_(printer)
+ printer.abort()
self._render_book()
def _delete_tmpdir(self):
@@ -186,6 +187,7 @@ class PDFWriter(QObject): # {{{
draw_image_page(printer, painter, p,
preserve_aspect_ratio=self.opts.preserve_cover_aspect_ratio)
painter.end()
+ printer.abort()
def _write(self):
diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py
index 3538f019ab..45eb09f066 100644
--- a/src/calibre/gui2/add.py
+++ b/src/calibre/gui2/add.py
@@ -8,7 +8,8 @@ from functools import partial
from PyQt4.Qt import QThread, QObject, Qt, QProgressDialog, pyqtSignal, QTimer
from calibre.gui2.dialogs.progress import ProgressDialog
-from calibre.gui2 import question_dialog, error_dialog, info_dialog, gprefs
+from calibre.gui2 import (question_dialog, error_dialog, info_dialog, gprefs,
+ warning_dialog)
from calibre.ebooks.metadata.opf2 import OPF
from calibre.ebooks.metadata import MetaInformation
from calibre.constants import preferred_encoding, filesystem_encoding, DEBUG
@@ -275,6 +276,24 @@ class Adder(QObject): # {{{
_('No books found'), show=True)
return self.canceled()
books = [[b] if isinstance(b, basestring) else b for b in books]
+ restricted = set()
+ for i in xrange(len(books)):
+ files = books[i]
+ restrictedi = set(f for f in files if not os.access(f, os.R_OK))
+ if restrictedi:
+ files = [f for f in files if os.access(f, os.R_OK)]
+ books[i] = files
+ restricted |= restrictedi
+ if restrictedi:
+ det_msg = u'\n'.join(restrictedi)
+ warning_dialog(self.pd, _('No permission'),
+ _('Cannot add some files as you do not have '
+ ' permission to access them. Click Show'
+ ' Details to see the list of such files.'),
+ det_msg=det_msg, show=True)
+ books = list(filter(None, books))
+ if not books:
+ return self.canceled()
self.rfind = None
from calibre.ebooks.metadata.worker import read_metadata
self.rq = Queue()
diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py
index 65a6a2f8c0..ca108a592e 100644
--- a/src/calibre/gui2/cover_flow.py
+++ b/src/calibre/gui2/cover_flow.py
@@ -29,12 +29,14 @@ if pictureflow is not None:
pictureflow.FlowImages.__init__(self)
self.images = []
self.captions = []
+ self.subtitles = []
for f in os.listdir(dirpath):
f = os.path.join(dirpath, f)
img = QImage(f)
if not img.isNull():
self.images.append(img)
self.captions.append(os.path.basename(f))
+ self.subtitles.append('%d bytes'%os.stat(f).st_size)
def count(self):
return len(self.images)
@@ -45,6 +47,9 @@ if pictureflow is not None:
def caption(self, index):
return self.captions[index]
+ def subtitle(self, index):
+ return self.subtitles[index]
+
def currentChanged(self, index):
print 'current changed:', index
diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py
index 27939c3519..f8838f8ce2 100644
--- a/src/calibre/gui2/library/views.py
+++ b/src/calibre/gui2/library/views.py
@@ -477,6 +477,8 @@ class BooksView(QTableView): # {{{
# arbitrary: scroll bar + header + some
max_width = self.width() - (self.verticalScrollBar().width() +
self.verticalHeader().width() + 10)
+ if max_width < 200:
+ max_width = 200
if new_size > max_width:
self.column_header.blockSignals(True)
self.setColumnWidth(col, max_width)
@@ -567,7 +569,8 @@ class BooksView(QTableView): # {{{
if md.hasFormat('text/uri-list') and not \
md.hasFormat('application/calibre+from_library'):
urls = [unicode(u.toLocalFile()) for u in md.urls()]
- return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
+ return [u for u in urls if os.path.splitext(u)[1] and
+ os.path.exists(u)]
def drag_icon(self, cover, multiple):
cover = cover.scaledToHeight(120, Qt.SmoothTransformation)
diff --git a/src/calibre/gui2/pictureflow/pictureflow.cpp b/src/calibre/gui2/pictureflow/pictureflow.cpp
index e18e287106..b82747841c 100644
--- a/src/calibre/gui2/pictureflow/pictureflow.cpp
+++ b/src/calibre/gui2/pictureflow/pictureflow.cpp
@@ -99,6 +99,8 @@ typedef unsigned short QRgb565;
#define PFREAL_ONE (1 << PFREAL_SHIFT)
#define PFREAL_HALF (PFREAL_ONE >> 1)
+#define TEXT_FLAGS (Qt::TextWordWrap|Qt::TextWrapAnywhere|Qt::TextHideMnemonic|Qt::AlignCenter)
+
inline PFreal fmul(PFreal a, PFreal b)
{
return ((long long)(a))*((long long)(b)) >> PFREAL_SHIFT;
@@ -401,6 +403,7 @@ private:
QImage* surface(int slideIndex);
void triggerRender();
void resetSlides();
+ void render_text(QPainter*, int);
};
PictureFlowPrivate::PictureFlowPrivate(PictureFlow* w, int queueLength_)
@@ -663,6 +666,34 @@ void PictureFlowPrivate::triggerRender()
triggerTimer.start();
}
+void PictureFlowPrivate::render_text(QPainter *painter, int index) {
+ QRect brect, brect2;
+ int buffer_width, buffer_height;
+ QString caption, subtitle;
+
+ caption = slideImages->caption(index);
+ subtitle = slideImages->subtitle(index);
+ buffer_width = buffer.width(); buffer_height = buffer.height();
+
+ brect = painter->boundingRect(QRect(0, 0, buffer_width, fontSize), TEXT_FLAGS, caption);
+ brect2 = painter->boundingRect(QRect(0, 0, buffer_width, fontSize), TEXT_FLAGS, subtitle);
+
+ // So that if there is no subtitle, the caption is not flush with the bottom
+ if (brect2.height() < fontSize) brect2.setHeight(fontSize);
+
+ // So that the text does not occupy more than the lower half of the buffer
+ if (brect.height() > ((int)(buffer.height()/3.0)) - fontSize*2)
+ brect.setHeight(((int)buffer.height()/3.0) - fontSize*2);
+
+ brect.moveTop(buffer_height - (brect.height() + brect2.height()));
+ //printf("top: %d, height: %d\n", brect.top(), brect.height());
+ //
+ painter->drawText(brect, TEXT_FLAGS, caption);
+
+ brect2.moveTop(buffer_height - brect2.height());
+ painter->drawText(brect2, TEXT_FLAGS, slideImages->subtitle(index));
+}
+
// Render the slides. Updates only the offscreen buffer.
void PictureFlowPrivate::render()
{
@@ -708,10 +739,7 @@ void PictureFlowPrivate::render()
//painter.setPen(QColor(255,255,255,127));
if (centerIndex < slideCount() && centerIndex > -1) {
- painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*4),
- Qt::AlignCenter, slideImages->caption(centerIndex));
- painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*2),
- Qt::AlignCenter, slideImages->subtitle(centerIndex));
+ render_text(&painter, centerIndex);
}
painter.end();
@@ -764,20 +792,12 @@ void PictureFlowPrivate::render()
painter.setPen(QColor(255,255,255, (255-fade) ));
if (leftTextIndex < sc && leftTextIndex > -1) {
- painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*4),
- Qt::AlignCenter, slideImages->caption(leftTextIndex));
- painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*2),
- Qt::AlignCenter, slideImages->subtitle(leftTextIndex));
-
+ render_text(&painter, leftTextIndex);
}
painter.setPen(QColor(255,255,255, fade));
if (leftTextIndex+1 < sc && leftTextIndex > -2) {
- painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*4),
- Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
- painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*2),
- Qt::AlignCenter, slideImages->subtitle(leftTextIndex+1));
-
+ render_text(&painter, leftTextIndex+1);
}
painter.end();
diff --git a/src/calibre/gui2/store/search/models.py b/src/calibre/gui2/store/search/models.py
index 1a2327fc45..e6ef147861 100644
--- a/src/calibre/gui2/store/search/models.py
+++ b/src/calibre/gui2/store/search/models.py
@@ -22,12 +22,16 @@ from calibre.utils.icu import sort_key
from calibre.utils.search_query_parser import SearchQueryParser
def comparable_price(text):
- text = re.sub(r'[^0-9.,]', '', text)
- if len(text) < 3 or text[-3] not in ('.', ','):
- text += '00'
- text = re.sub(r'\D', '', text)
- text = text.rjust(6, '0')
- return text
+ # this keep thousand and fraction separators
+ match = re.search(r'(?:\d|[,.](?=\d))(?:\d*(?:[,.\' ](?=\d))?)+', text)
+ if match:
+ # replace all separators with '.'
+ m = re.sub(r'[.,\' ]', '.', match.group())
+ # remove all separators accept fraction,
+ # leave only 2 digits in fraction
+ m = re.sub(r'\.(?!\d*$)', r'', m)
+ text = '{0:0>8.0f}'.format(float(m) * 100.)
+ return text
class Matches(QAbstractItemModel):
@@ -334,6 +338,11 @@ class SearchFilter(SearchQueryParser):
}
for x in ('author', 'download', 'format'):
q[x+'s'] = q[x]
+
+ # make the price in query the same format as result
+ if location == 'price':
+ query = comparable_price(query)
+
for sr in self.srs:
for locvalue in locations:
accessor = q[locvalue]
diff --git a/src/calibre/gui2/store/stores/amazon_de_plugin.py b/src/calibre/gui2/store/stores/amazon_de_plugin.py
index 88ccbdbded..33f681ab52 100644
--- a/src/calibre/gui2/store/stores/amazon_de_plugin.py
+++ b/src/calibre/gui2/store/stores/amazon_de_plugin.py
@@ -45,24 +45,26 @@ class AmazonDEKindleStore(StorePlugin):
doc = html.fromstring(f.read())
# Amazon has two results pages.
- is_shot = doc.xpath('boolean(//div[@id="shotgunMainResults"])')
- # Horizontal grid of books.
- if is_shot:
- data_xpath = '//div[contains(@class, "result")]'
- format_xpath = './/div[@class="productTitle"]/text()'
- cover_xpath = './/div[@class="productTitle"]//img/@src'
- # Vertical list of books.
- else:
- data_xpath = '//div[@class="productData"]'
- format_xpath = './/span[@class="format"]/text()'
- cover_xpath = '../div[@class="productImage"]/a/img/@src'
+ # 20110725: seems that is_shot is gone.
+# is_shot = doc.xpath('boolean(//div[@id="shotgunMainResults"])')
+# # Horizontal grid of books.
+# if is_shot:
+# data_xpath = '//div[contains(@class, "result")]'
+# format_xpath = './/div[@class="productTitle"]/text()'
+# cover_xpath = './/div[@class="productTitle"]//img/@src'
+# # Vertical list of books.
+# else:
+ data_xpath = '//div[contains(@class, "result") and contains(@class, "product")]'
+ format_xpath = './/span[@class="format"]/text()'
+ cover_xpath = './/img[@class="productImage"]/@src'
+# end is_shot else
for data in doc.xpath(data_xpath):
if counter <= 0:
break
# Even though we are searching digital-text only Amazon will still
- # put in results for non Kindle books (author pages). Se we need
+ # put in results for non Kindle books (author pages). So we need
# to explicitly check if the item is a Kindle book and ignore it
# if it isn't.
format = ''.join(data.xpath(format_xpath))
@@ -71,28 +73,18 @@ class AmazonDEKindleStore(StorePlugin):
# We must have an asin otherwise we can't easily reference the
# book later.
- asin_href = None
- asin_a = data.xpath('.//div[@class="productTitle"]/a[1]')
- if asin_a:
- asin_href = asin_a[0].get('href', '')
- m = re.search(r'/dp/(?P.+?)(/|$)', asin_href)
- if m:
- asin = m.group('asin')
- else:
- continue
- else:
- continue
+ asin = ''.join(data.xpath("@name"))
cover_url = ''.join(data.xpath(cover_xpath))
- title = ''.join(data.xpath('.//div[@class="productTitle"]/a/text()'))
+ title = ''.join(data.xpath('.//div[@class="title"]/a/text()'))
price = ''.join(data.xpath('.//div[@class="newPrice"]/span/text()'))
- if is_shot:
- author = format.split(' von ')[-1]
- else:
- author = ''.join(data.xpath('.//div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
- author = author.split(' von ')[-1]
+# if is_shot:
+# author = format.split(' von ')[-1]
+# else:
+ author = ''.join(data.xpath('.//div[@class="title"]/span[@class="ptBrand"]/text()'))
+ author = author.split('von ')[-1]
counter -= 1
diff --git a/src/calibre/gui2/store/stores/amazon_uk_plugin.py b/src/calibre/gui2/store/stores/amazon_uk_plugin.py
index f8686d19fe..86603f3fc3 100644
--- a/src/calibre/gui2/store/stores/amazon_uk_plugin.py
+++ b/src/calibre/gui2/store/stores/amazon_uk_plugin.py
@@ -42,49 +42,56 @@ class AmazonUKKindleStore(StorePlugin):
doc = html.fromstring(f.read())
# Amazon has two results pages.
- is_shot = doc.xpath('boolean(//div[@id="shotgunMainResults"])')
- # Horizontal grid of books.
- if is_shot:
- data_xpath = '//div[contains(@class, "result")]'
- cover_xpath = './/div[@class="productTitle"]//img/@src'
- # Vertical list of books.
- else:
- data_xpath = '//div[contains(@class, "product")]'
- cover_xpath = './div[@class="productImage"]/a/img/@src'
+ # 20110725: seems that is_shot is gone.
+# is_shot = doc.xpath('boolean(//div[@id="shotgunMainResults"])')
+# # Horizontal grid of books.
+# if is_shot:
+# data_xpath = '//div[contains(@class, "result")]'
+# format_xpath = './/div[@class="productTitle"]/text()'
+# cover_xpath = './/div[@class="productTitle"]//img/@src'
+# # Vertical list of books.
+# else:
+ data_xpath = '//div[contains(@class, "result") and contains(@class, "product")]'
+ format_xpath = './/span[@class="format"]/text()'
+ cover_xpath = './/img[@class="productImage"]/@src'
+# end is_shot else
for data in doc.xpath(data_xpath):
if counter <= 0:
break
+ # Even though we are searching digital-text only Amazon will still
+ # put in results for non Kindle books (author pages). So we need
+ # to explicitly check if the item is a Kindle book and ignore it
+ # if it isn't.
+ format = ''.join(data.xpath(format_xpath))
+ if 'kindle' not in format.lower():
+ continue
+
# We must have an asin otherwise we can't easily reference the
# book later.
- asin = ''.join(data.xpath('./@name'))
- if not asin:
- continue
+ asin = ''.join(data.xpath("@name"))
+
cover_url = ''.join(data.xpath(cover_xpath))
- title = ''.join(data.xpath('.//div[@class="productTitle"]/a/text()'))
+ title = ''.join(data.xpath('.//div[@class="title"]/a/text()'))
price = ''.join(data.xpath('.//div[@class="newPrice"]/span/text()'))
+# if is_shot:
+# author = format.split(' von ')[-1]
+# else:
+ author = ''.join(data.xpath('.//div[@class="title"]/span[@class="ptBrand"]/text()'))
+ author = author.split('by ')[-1]
+
counter -= 1
s = SearchResult()
s.cover_url = cover_url.strip()
s.title = title.strip()
+ s.author = author.strip()
s.price = price.strip()
s.detail_item = asin.strip()
- s.formats = ''
-
- if is_shot:
- # Amazon UK does not include the author on the grid layout
- s.author = ''
- self.get_details(s, timeout)
- if s.formats != 'Kindle':
- continue
- else:
- author = ''.join(data.xpath('.//div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
- s.author = author.split(' by ')[-1].strip()
- s.formats = 'Kindle'
+ s.formats = 'Kindle'
yield s
diff --git a/src/calibre/gui2/store/stores/epubbud_plugin.py b/src/calibre/gui2/store/stores/epubbud_plugin.py
deleted file mode 100644
index 029b2b3fc9..0000000000
--- a/src/calibre/gui2/store/stores/epubbud_plugin.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import (unicode_literals, division, absolute_import, print_function)
-
-__license__ = 'GPL 3'
-__copyright__ = '2011, John Schember '
-__docformat__ = 'restructuredtext en'
-
-from calibre.gui2.store.basic_config import BasicStoreConfig
-from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore
-from calibre.gui2.store.search_result import SearchResult
-
-class EpubBudStore(BasicStoreConfig, OpenSearchOPDSStore):
-
- open_search_url = 'http://www.epubbud.com/feeds/opensearch.xml'
- web_url = 'http://www.epubbud.com/'
-
- # http://www.epubbud.com/feeds/catalog.atom
-
- def search(self, query, max_results=10, timeout=60):
- for s in OpenSearchOPDSStore.search(self, query, max_results, timeout):
- s.price = '$0.00'
- s.drm = SearchResult.DRM_UNLOCKED
- s.formats = 'EPUB'
- # Download links are broken for this store.
- s.downloads = {}
- yield s
diff --git a/src/calibre/gui2/store/stores/epubbuy_de_plugin.py b/src/calibre/gui2/store/stores/epubbuy_de_plugin.py
deleted file mode 100644
index 242ef76793..0000000000
--- a/src/calibre/gui2/store/stores/epubbuy_de_plugin.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import (unicode_literals, division, absolute_import, print_function)
-
-__license__ = 'GPL 3'
-__copyright__ = '2011, John Schember '
-__docformat__ = 'restructuredtext en'
-
-import urllib2
-from contextlib import closing
-
-from lxml import html
-
-from PyQt4.Qt import QUrl
-
-from calibre import browser
-from calibre.gui2 import open_url
-from calibre.gui2.store import StorePlugin
-from calibre.gui2.store.basic_config import BasicStoreConfig
-from calibre.gui2.store.search_result import SearchResult
-from calibre.gui2.store.web_store_dialog import WebStoreDialog
-
-class EPubBuyDEStore(BasicStoreConfig, StorePlugin):
-
- def open(self, parent=None, detail_item=None, external=False):
- url = 'http://klick.affiliwelt.net/klick.php?bannerid=47653&pid=32307&prid=2627'
- url_details = ('http://klick.affiliwelt.net/klick.php?bannerid=47653'
- '&pid=32307&prid=2627&prodid={0}')
-
- if external or self.config.get('open_external', False):
- if detail_item:
- url = url_details.format(detail_item)
- open_url(QUrl(url))
- else:
- detail_url = None
- if detail_item:
- detail_url = url_details.format(detail_item)
- d = WebStoreDialog(self.gui, url, parent, detail_url)
- d.setWindowTitle(self.name)
- d.set_tags(self.config.get('tags', ''))
- d.exec_()
-
- def search(self, query, max_results=10, timeout=60):
- url = 'http://www.epubbuy.com/search.php?search_query=' + urllib2.quote(query)
- br = browser()
-
- counter = max_results
- with closing(br.open(url, timeout=timeout)) as f:
- doc = html.fromstring(f.read())
- for data in doc.xpath('//li[contains(@class, "ajax_block_product")]'):
- if counter <= 0:
- break
-
- id = ''.join(data.xpath('./div[@class="center_block"]'
- '/p[contains(text(), "artnr:")]/text()')).strip()
- if not id:
- continue
- id = id[6:].strip()
- if not id:
- continue
- cover_url = ''.join(data.xpath('./div[@class="center_block"]'
- '/a[@class="product_img_link"]/img/@src'))
- if cover_url:
- cover_url = 'http://www.epubbuy.com' + cover_url
- title = ''.join(data.xpath('./div[@class="center_block"]'
- '/a[@class="product_img_link"]/@title'))
- author = ''.join(data.xpath('./div[@class="center_block"]/a[2]/text()'))
- price = ''.join(data.xpath('.//span[@class="price"]/text()'))
- counter -= 1
-
- s = SearchResult()
- s.cover_url = cover_url
- s.title = title.strip()
- s.author = author.strip()
- s.price = price
- s.drm = SearchResult.DRM_UNLOCKED
- s.detail_item = id
- s.formats = 'ePub'
-
- yield s
diff --git a/src/calibre/gui2/store/stores/google_books_plugin.py b/src/calibre/gui2/store/stores/google_books_plugin.py
index 938ca70664..4819509c3f 100644
--- a/src/calibre/gui2/store/stores/google_books_plugin.py
+++ b/src/calibre/gui2/store/stores/google_books_plugin.py
@@ -6,6 +6,7 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember '
__docformat__ = 'restructuredtext en'
+import random
import urllib
from contextlib import closing
@@ -23,7 +24,24 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class GoogleBooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
- url = 'http://books.google.com/'
+ aff_id = {
+ 'lid': '41000000033185143',
+ 'pubid': '21000000000352219',
+ 'ganpub': 'k352219',
+ 'ganclk': 'GOOG_1335334761',
+ }
+ # Use Kovid's affiliate id 30% of the time.
+ if random.randint(1, 10) in (1, 2, 3):
+ aff_id = {
+ 'lid': '41000000031855266',
+ 'pubid': '21000000000352583',
+ 'ganpub': 'k352583',
+ 'ganclk': 'GOOG_1335335464',
+ }
+
+ url = 'http://gan.doubleclick.net/gan_click?lid=%(lid)s&pubid=%(pubid)s' % aff_id
+ if detail_item:
+ detail_item += '&ganpub=%(ganpub)s&ganclk=%(ganclk)s' % aff_id
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
diff --git a/src/calibre/gui2/store/stores/libri_de_plugin.py b/src/calibre/gui2/store/stores/libri_de_plugin.py
index ed93eeff0e..912ae668e8 100644
--- a/src/calibre/gui2/store/stores/libri_de_plugin.py
+++ b/src/calibre/gui2/store/stores/libri_de_plugin.py
@@ -24,7 +24,7 @@ class LibreDEStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
url = 'http://ad.zanox.com/ppc/?18817073C15644254T'
- url_details = ('http://ad.zanox.com/ppc/?18845780C1371495675T&ULP=[['
+ url_details = ('http://ad.zanox.com/ppc/?18848208C1197627693T&ULP=[['
'http://www.libri.de/shop/action/productDetails?artiId={0}]]')
if external or self.config.get('open_external', False):
diff --git a/src/calibre/gui2/store/stores/ozon_ru_plugin.py b/src/calibre/gui2/store/stores/ozon_ru_plugin.py
new file mode 100644
index 0000000000..0d513f3dfa
--- /dev/null
+++ b/src/calibre/gui2/store/stores/ozon_ru_plugin.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, Roman Mukhin '
+__docformat__ = 'restructuredtext en'
+
+import random
+import re
+import urllib2
+
+from contextlib import closing
+from lxml import etree, html
+from PyQt4.Qt import QUrl
+
+from calibre import browser, url_slash_cleaner
+from calibre.ebooks.chardet import xml_to_unicode
+from calibre.gui2 import open_url
+from calibre.gui2.store import StorePlugin
+from calibre.gui2.store.basic_config import BasicStoreConfig
+from calibre.gui2.store.search_result import SearchResult
+from calibre.gui2.store.web_store_dialog import WebStoreDialog
+
+class OzonRUStore(BasicStoreConfig, StorePlugin):
+ shop_url = 'http://www.ozon.ru'
+
+ def open(self, parent=None, detail_item=None, external=False):
+
+ aff_id = '?partner=romuk'
+ # Use Kovid's affiliate id 30% of the time.
+ if random.randint(1, 10) in (1, 2, 3):
+ aff_id = '?partner=kovidgoyal'
+
+ url = self.shop_url + aff_id
+ detail_url = None
+ if detail_item:
+ # http://www.ozon.ru/context/detail/id/3037277/
+ detail_url = self.shop_url + '/context/detail/id/' + urllib2.quote(detail_item) + aff_id
+
+ if external or self.config.get('open_external', False):
+ open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
+ else:
+ d = WebStoreDialog(self.gui, url, parent, detail_url)
+ d.setWindowTitle(self.name)
+ d.set_tags(self.config.get('tags', ''))
+ d.exec_()
+
+
+ def search(self, query, max_results=10, timeout=60):
+ search_url = self.shop_url + '/webservice/webservice.asmx/SearchWebService?'\
+ 'searchText=%s&searchContext=ebook' % urllib2.quote(query)
+
+ counter = max_results
+ br = browser()
+ with closing(br.open(search_url, timeout=timeout)) as f:
+ raw = xml_to_unicode(f.read(), strip_encoding_pats=True, assume_utf8=True)[0]
+ doc = etree.fromstring(raw)
+ for data in doc.xpath('//*[local-name() = "SearchItems"]'):
+ if counter <= 0:
+ break
+ counter -= 1
+
+ xp_template = 'normalize-space(./*[local-name() = "{0}"]/text())'
+
+ s = SearchResult()
+ s.detail_item = data.xpath(xp_template.format('ID'))
+ s.title = data.xpath(xp_template.format('Name'))
+ s.author = data.xpath(xp_template.format('Author'))
+ s.price = data.xpath(xp_template.format('Price'))
+ s.cover_url = data.xpath(xp_template.format('Picture'))
+ if re.match("^\d+?\.\d+?$", s.price):
+ s.price = u'{:.2F} руб.'.format(float(s.price))
+ yield s
+
+ def get_details(self, search_result, timeout=60):
+ url = self.shop_url + '/context/detail/id/' + urllib2.quote(search_result.detail_item)
+ br = browser()
+
+ result = False
+ with closing(br.open(url, timeout=timeout)) as f:
+ doc = html.fromstring(f.read())
+
+ # example where we are going to find formats
+ #
+ # ...
+ #
Доступные форматы:
+ #
.epub, .fb2, .pdf, .pdf, .txt
+ # ...
+ #
+ xpt = u'normalize-space(//div[@class="box"]//*[contains(normalize-space(text()), "Доступные форматы:")][1]/following-sibling::div[1]/text())'
+ formats = doc.xpath(xpt)
+ if formats:
+ result = True
+ search_result.drm = SearchResult.DRM_UNLOCKED
+ search_result.formats = ', '.join(_parse_ebook_formats(formats))
+ # unfortunately no direct links to download books (only buy link)
+ # search_result.downloads['BF2'] = self.shop_url + '/order/digitalorder.aspx?id=' + + urllib2.quote(search_result.detail_item)
+ return result
+
+def _parse_ebook_formats(formatsStr):
+ '''
+ Creates a list with displayable names of the formats
+
+ :param formatsStr: string with comma separated book formats
+ as it provided by ozon.ru
+ :return: a list with displayable book formats
+ '''
+
+ formatsUnstruct = formatsStr.lower()
+ formats = []
+ if 'epub' in formatsUnstruct:
+ formats.append('ePub')
+ if 'pdf' in formatsUnstruct:
+ formats.append('PDF')
+ if 'fb2' in formatsUnstruct:
+ formats.append('FB2')
+ if 'rtf' in formatsUnstruct:
+ formats.append('RTF')
+ if 'txt' in formatsUnstruct:
+ formats.append('TXT')
+ if 'djvu' in formatsUnstruct:
+ formats.append('DjVu')
+ if 'doc' in formatsUnstruct:
+ formats.append('DOC')
+ return formats
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 9ae8f0569b..7e44ccc8bc 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -1892,7 +1892,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
yield r[iindex]
def get_next_series_num_for(self, series):
- series_id = self.conn.get('SELECT id from series WHERE name=?',
+ series_id = None
+ if series:
+ series_id = self.conn.get('SELECT id from series WHERE name=?',
(series,), all=False)
if series_id is None:
if isinstance(tweaks['series_index_auto_increment'], (int, float)):
diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py
index 853ec7829c..ad28c532a4 100644
--- a/src/calibre/library/server/content.py
+++ b/src/calibre/library/server/content.py
@@ -10,13 +10,14 @@ import re, os, posixpath
import cherrypy
from calibre import fit_image, guess_type
-from calibre.utils.date import fromtimestamp, utcnow
+from calibre.utils.date import fromtimestamp
from calibre.library.caches import SortKeyGenerator
from calibre.library.save_to_disk import find_plugboard
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.magick.draw import (save_cover_data_to, Image,
thumbnail as generate_thumbnail)
from calibre.utils.filenames import ascii_filename
+from calibre.ebooks.metadata.opf2 import metadata_to_opf
plugboard_content_server_value = 'content_server'
plugboard_content_server_formats = ['epub']
@@ -32,7 +33,7 @@ class CSSortKeyGenerator(SortKeyGenerator):
class ContentServer(object):
'''
- Handles actually serving content files/covers. Also has
+ Handles actually serving content files/covers/metadata. Also has
a few utility methods.
'''
@@ -68,9 +69,8 @@ class ContentServer(object):
# }}}
-
def get(self, what, id):
- 'Serves files, covers, thumbnails from the calibre database'
+ 'Serves files, covers, thumbnails, metadata from the calibre database'
try:
id = int(id)
except ValueError:
@@ -90,6 +90,8 @@ class ContentServer(object):
thumb_height=height)
if what == 'cover':
return self.get_cover(id)
+ if what == 'opf':
+ return self.get_metadata_as_opf(id)
return self.get_format(id, what)
def static(self, name):
@@ -180,6 +182,17 @@ class ContentServer(object):
cherrypy.log.error(traceback.print_exc())
raise cherrypy.HTTPError(404, 'Failed to generate cover: %r'%err)
+ def get_metadata_as_opf(self, id_):
+ cherrypy.response.headers['Content-Type'] = \
+ 'application/oebps-package+xml; charset=UTF-8'
+ mi = self.db.get_metadata(id_, index_is_id=True)
+ data = metadata_to_opf(mi)
+ cherrypy.response.timeout = 3600
+ cherrypy.response.headers['Last-Modified'] = \
+ self.last_modified(mi.last_modified)
+
+ return data
+
def get_format(self, id, format):
format = format.upper()
fmt = self.db.format(id, format, index_is_id=True, as_file=True,
@@ -217,7 +230,8 @@ class ContentServer(object):
cherrypy.response.headers['Content-Disposition'] = \
b'attachment; filename="%s"'%fname
cherrypy.response.timeout = 3600
- cherrypy.response.headers['Last-Modified'] = self.last_modified(utcnow())
+ cherrypy.response.headers['Last-Modified'] = \
+ self.last_modified(self.db.format_last_modified(id, format))
return fmt
# }}}
diff --git a/src/calibre/translations/ru.po b/src/calibre/translations/ru.po
index fb90bfc5e0..f437b7975a 100644
--- a/src/calibre/translations/ru.po
+++ b/src/calibre/translations/ru.po
@@ -5541,23 +5541,23 @@ msgstr "Книги с такими же тегами"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:20
msgid "Get books"
-msgstr ""
+msgstr "Загрузить книги"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:29
msgid "Search for ebooks"
-msgstr ""
+msgstr "Поиск книг..."
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:30
msgid "Search for this author"
-msgstr ""
+msgstr "Поиск по автору"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:31
msgid "Search for this title"
-msgstr ""
+msgstr "Поиск по названию"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:32
msgid "Search for this book"
-msgstr ""
+msgstr "Поиск по книге"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:34
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search_ui.py:135
@@ -5569,21 +5569,21 @@ msgstr "Магазины"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/chooser_dialog.py:18
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search.py:285
msgid "Choose stores"
-msgstr ""
+msgstr "Выбрать магазины"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:83
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:102
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:111
msgid "Cannot search"
-msgstr ""
+msgstr "Поиск не может быть произведён"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:130
msgid ""
"Calibre helps you find the ebooks you want by searching the websites of "
"various commercial and public domain book sources for you."
msgstr ""
-"Calibre помогает вам отыскать книги, которые вы хотите найти, предлагая вам "
-"найденные веб-сайты различных коммерческих и публичных источников книг."
+"Calibre поможет Вам найти книги, предлагая "
+"веб-сайты различных коммерческих и публичных источников книг."
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:134
msgid ""
@@ -5591,6 +5591,8 @@ msgid ""
"are looking for, at the best price. You also get DRM status and other useful "
"information."
msgstr ""
+"Используя встроенный поиск Вы можете легко найти магазин предлагающий выгодную цену "
+"для интересующей Вас книги. Также Вы получите другу полезную инфрмацию"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:138
msgid ""
@@ -5608,7 +5610,7 @@ msgstr "Показать снова данное сообщение"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:149
msgid "About Get Books"
-msgstr ""
+msgstr "О 'Загрузить книги'"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/tweak_epub.py:17
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tweak_epub_ui.py:60
@@ -5617,7 +5619,7 @@ msgstr "Tweak EPUB"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/tweak_epub.py:18
msgid "Make small changes to ePub format books"
-msgstr ""
+msgstr "Внести небольшие изненения ePub в формат книги"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/tweak_epub.py:19
msgid "T"
@@ -5704,7 +5706,7 @@ msgstr "Не могу открыть папку"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:220
msgid "This book no longer exists in your library"
-msgstr ""
+msgstr "Эта книга больше не находится в Вашей библиотеке"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:227
#, python-format
@@ -9167,11 +9169,11 @@ msgstr "&Показать пароль"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:122
msgid "Restart required"
-msgstr ""
+msgstr "Требуется перезапуск"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:123
msgid "You must restart Calibre before using this plugin!"
-msgstr ""
+msgstr "Для использования плагина Вам нужно перезапустить Calibre!"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:164
#, python-format
@@ -9183,17 +9185,17 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search_ui.py:136
#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:111
msgid "All"
-msgstr ""
+msgstr "Всё"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:184
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:302
msgid "Installed"
-msgstr ""
+msgstr "Установленные"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:184
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:397
msgid "Not installed"
-msgstr ""
+msgstr "Не установленные"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:184
msgid "Update available"
@@ -9201,7 +9203,7 @@ msgstr "Доступно обновление"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:302
msgid "Plugin Name"
-msgstr ""
+msgstr "Название плагина"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:302
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:63
@@ -13317,7 +13319,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins_ui.py:114
msgid "&Load plugin from file"
-msgstr ""
+msgstr "Загрузить плагин из файла"
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/save_template.py:33
msgid "Any custom field"
@@ -13579,11 +13581,11 @@ msgstr "Сбой запуска контент-сервера"
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server.py:106
msgid "Error log:"
-msgstr "Лог ошибок:"
+msgstr "Журнал ошибок:"
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server.py:113
msgid "Access log:"
-msgstr "Лог доступа:"
+msgstr "Журнал доступа:"
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server.py:128
msgid "You need to restart the server for changes to take effect"
@@ -14053,7 +14055,7 @@ msgstr "Ничего"
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:59
msgid "Press a key..."
-msgstr ""
+msgstr "Нажмите клавишу..."
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:80
msgid "Already assigned"
@@ -14108,19 +14110,19 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/basic_config_widget_ui.py:38
msgid "Added Tags:"
-msgstr ""
+msgstr "Добавленные тэги:"
#: /home/kovid/work/calibre/src/calibre/gui2/store/basic_config_widget_ui.py:39
msgid "Open store in external web browswer"
-msgstr ""
+msgstr "Открыть сайт магазина в интернет броузере"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/adv_search_builder_ui.py:219
msgid "&Name:"
-msgstr ""
+msgstr "&Название"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/adv_search_builder_ui.py:221
msgid "&Description:"
-msgstr ""
+msgstr "&Описание"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/adv_search_builder_ui.py:222
msgid "&Headquarters:"
@@ -14140,7 +14142,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/adv_search_builder_ui.py:217
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/adv_search_builder_ui.py:220
msgid "true"
-msgstr ""
+msgstr "да"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/adv_search_builder_ui.py:229
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/adv_search_builder_ui.py:231
@@ -14148,41 +14150,41 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/adv_search_builder_ui.py:218
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/adv_search_builder_ui.py:221
msgid "false"
-msgstr ""
+msgstr "нет"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/adv_search_builder_ui.py:232
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/adv_search_builder_ui.py:216
msgid "Affiliate:"
-msgstr ""
+msgstr "Партнёрство:"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/adv_search_builder_ui.py:235
msgid "Nam&e/Description ..."
-msgstr ""
+msgstr "Названи&е/Описание"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/chooser_widget_ui.py:78
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search_ui.py:132
#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:108
msgid "Query:"
-msgstr ""
+msgstr "Запрос:"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/chooser_widget_ui.py:81
msgid "Enable"
-msgstr ""
+msgstr "Включить"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/chooser_widget_ui.py:84
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search_ui.py:137
#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:112
msgid "Invert"
-msgstr ""
+msgstr "Инвертировать"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:21
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:37
msgid "Affiliate"
-msgstr ""
+msgstr "Партнерство"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:21
msgid "Enabled"
-msgstr ""
+msgstr "Включено"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:21
msgid "Headquarters"
@@ -14190,7 +14192,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:21
msgid "No DRM"
-msgstr ""
+msgstr "Без DRM"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:129
msgid ""
@@ -14205,13 +14207,14 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:136
msgid "This store only distributes ebooks without DRM."
-msgstr ""
+msgstr "Этот магазин распространяет электронные книги исключительно без DRM"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:138
msgid ""
"This store distributes ebooks with DRM. It may have some titles without DRM, "
"but you will need to check on a per title basis."
-msgstr ""
+msgstr "Этот магазин распространяет электронные книги с DRM. Возможно, некоторые издания"
+" доступны без DRM, но для этого надо проверять каждую книгу в отдельности."
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:140
#, python-format
@@ -14225,46 +14228,46 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:211
#, python-format
msgid "Buying from this store supports the calibre developer: %s."
-msgstr ""
+msgstr "Покупая в этом магазине Вы поддерживаете проект calibre и разработчика: %s."
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:145
#, python-format
msgid "This store distributes ebooks in the following formats: %s"
-msgstr ""
+msgstr "Магазин распространяет эл. книги в следующих фотрматах"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/results_view.py:47
msgid "Configure..."
-msgstr ""
+msgstr "Настроить..."
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search/search_widget_ui.py:99
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search_widget_ui.py:99
msgid "Time"
-msgstr ""
+msgstr "Время"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search/search_widget_ui.py:100
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search_widget_ui.py:100
msgid "Number of seconds to wait for a store to respond"
-msgstr ""
+msgstr "Время ожидания ответа магазина (в секундах)"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search/search_widget_ui.py:101
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search_widget_ui.py:101
msgid "Number of seconds to let a store process results"
-msgstr ""
+msgstr "Допустипое время обработки результата магазином (в секундах)"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search/search_widget_ui.py:102
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search_widget_ui.py:102
msgid "Display"
-msgstr ""
+msgstr "Показать"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search/search_widget_ui.py:103
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search_widget_ui.py:103
msgid "Maximum number of results to show per store"
-msgstr ""
+msgstr "Максимальное количество результатов для показа (по каждому магазину)"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search/search_widget_ui.py:104
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search_widget_ui.py:104
msgid "Open search result in system browser"
-msgstr ""
+msgstr "Показывать результаты поиска в системном интернет броузере"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search/search_widget_ui.py:105
msgid "Threads"
@@ -14288,11 +14291,11 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search_widget_ui.py:105
msgid "Performance"
-msgstr ""
+msgstr "Производительность"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search_widget_ui.py:106
msgid "Number of simultaneous searches"
-msgstr ""
+msgstr "Количество одновременно выполняемых поисков"
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/search_widget_ui.py:107
msgid "Number of simultaneous cache updates"
@@ -14308,13 +14311,13 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_store_dialog_ui.py:62
msgid "Search:"
-msgstr ""
+msgstr "Поиск:"
#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_store_dialog_ui.py:63
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search_ui.py:142
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/store_dialog_ui.py:77
msgid "Books:"
-msgstr ""
+msgstr "Книги:"
#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_store_dialog_ui.py:65
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search_ui.py:144
@@ -14323,20 +14326,20 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:63
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:661
msgid "Close"
-msgstr ""
+msgstr "Закрыть"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/adv_search_builder_ui.py:212
msgid "&Price:"
-msgstr ""
+msgstr "&Цена:"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/adv_search_builder_ui.py:219
msgid "Download:"
-msgstr ""
+msgstr "Скачать"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/adv_search_builder_ui.py:222
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/adv_search_builder_ui.py:187
msgid "Titl&e/Author/Price ..."
-msgstr ""
+msgstr "Названи&е/Автор/Цена ..."
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:37
msgid "DRM"
@@ -14344,11 +14347,11 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:37
msgid "Download"
-msgstr ""
+msgstr "Скачать"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:37
msgid "Price"
-msgstr ""
+msgstr "Цена"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:196
#, python-format
@@ -14383,90 +14386,90 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:208
#, python-format
msgid "The following formats can be downloaded directly: %s."
-msgstr ""
+msgstr "Форматы доступные для непосредственного скачивания: %s."
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/results_view.py:41
msgid "Download..."
-msgstr ""
+msgstr "Скачать..."
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/results_view.py:45
msgid "Goto in store..."
-msgstr ""
+msgstr "Перейти в магазин..."
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search.py:114
#, python-format
msgid "Buying from this store supports the calibre developer: %s
"
-msgstr ""
+msgstr "Покупая в этом магазине Вы поддерживаете проект calibre и разработчика: %s"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search.py:276
msgid "Customize get books search"
-msgstr ""
+msgstr "Перенастроить под себя поиск книг для скачивания"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search.py:286
msgid "Configure search"
-msgstr ""
+msgstr "Настроить поиск"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search.py:336
msgid "Couldn't find any books matching your query."
-msgstr "Ну удалось найти ни одной кники, соотвествующей вашему запросу."
+msgstr "Не удалось найти ни одной книги, соотвествующей вашему запросу."
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search.py:350
msgid "Choose format to download to your library."
-msgstr ""
+msgstr "Выберите формат для скачивания в библиотеку"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search_ui.py:131
#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:107
msgid "Get Books"
-msgstr ""
+msgstr "Скачать книги"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search_ui.py:140
msgid "Open a selected book in the system's web browser"
-msgstr ""
+msgstr "Показать выбранную книгу в системном интернет броузере"
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/search_ui.py:141
msgid "Open in &external browser"
-msgstr ""
+msgstr "Показывать в системном интернет броузере"
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/ebooks_com_plugin.py:96
msgid "Not Available"
-msgstr ""
+msgstr "Недоступно"
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/adv_search_builder_ui.py:179
msgid ""
"See the User Manual for more help"
msgstr ""
-"Смотри Пользовательский мануал для помощи"
+"Смотрите Руководство пользователя для помощи"
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/cache_progress_dialog_ui.py:51
msgid "Updating book cache"
-msgstr ""
+msgstr "Обноволяется кэш книг"
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/cache_update_thread.py:42
msgid "Checking last download date."
-msgstr ""
+msgstr "Проверяется врема последнего скачивания"
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/cache_update_thread.py:48
msgid "Downloading book list from MobileRead."
-msgstr ""
+msgstr "Загружается список книг с MobileRead."
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/cache_update_thread.py:61
msgid "Processing books."
-msgstr ""
+msgstr "Книги обрабатываются"
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/cache_update_thread.py:71
#, python-format
msgid "%(num)s of %(tot)s books processed."
-msgstr ""
+msgstr "обработано %(num)s из %(tot)."
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/mobileread_plugin.py:62
msgid "Updating MobileRead book cache..."
-msgstr ""
+msgstr "Обноволяется кэщ MobileRead книг..."
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/store_dialog_ui.py:74
msgid "&Query:"
-msgstr ""
+msgstr "&Запрос:"
#: /home/kovid/work/calibre/src/calibre/gui2/store/web_control.py:73
msgid ""
@@ -14480,15 +14483,15 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/web_control.py:86
msgid "File is not a supported ebook type. Save to disk?"
-msgstr ""
+msgstr "Файл содержит неподдерживаемый формат эл. книги. Сохранить на диске?"
#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:59
msgid "Home"
-msgstr ""
+msgstr "Главная страница"
#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:60
msgid "Reload"
-msgstr ""
+msgstr "Перегрузить"
#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:61
msgid "%p%"
@@ -14502,22 +14505,24 @@ msgstr ""
msgid ""
"Changing the authors for several books can take a while. Are you sure?"
msgstr ""
+"Изменить автора нескольких книг займёт некоторое время. Вы согласны"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:729
msgid ""
"Changing the metadata for that many books can take a while. Are you sure?"
msgstr ""
+"Изменить мета-данные нескольких книг займёт некоторое время. Вы согласны"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:816
#: /home/kovid/work/calibre/src/calibre/library/database2.py:449
msgid "Searches"
-msgstr ""
+msgstr "Поиски"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:881
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:901
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:910
msgid "Rename user category"
-msgstr ""
+msgstr "Переименовать пользовательскую категорию"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:882
msgid "You cannot use periods in the name when renaming user categories"
@@ -14540,30 +14545,30 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:48
msgid "Manage Authors"
-msgstr ""
+msgstr "Упорядочнить авторов"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:50
msgid "Manage Series"
-msgstr ""
+msgstr "Упорядочнить серии"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:52
msgid "Manage Publishers"
-msgstr ""
+msgstr "Упорядочнить издателей"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:54
msgid "Manage Tags"
-msgstr ""
+msgstr "Упорядочнить тэги"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:56
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:465
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:469
msgid "Manage User Categories"
-msgstr "Управление пользовательскими категориями"
+msgstr "Упорядочнить пользовательские категории"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:58
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:457
msgid "Manage Saved Searches"
-msgstr "Управление сохраненными поисками"
+msgstr "Упорядочнить сохраненные поиски"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:66
msgid "Invalid search restriction"
@@ -14580,17 +14585,17 @@ msgstr "Новая категория"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:134
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:137
msgid "Delete user category"
-msgstr ""
+msgstr "Удалить пользовательскую категорию"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:135
#, python-format
msgid "%s is not a user category"
-msgstr ""
+msgstr "%s не является пользовательской категорией"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:138
#, python-format
msgid "%s contains items. Do you really want to delete it?"
-msgstr ""
+msgstr "%s содержит элементы. Вы действительно хотете её удалить?"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:159
msgid "Remove category"
@@ -14599,16 +14604,16 @@ msgstr "Удалить категорию"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:160
#, python-format
msgid "User category %s does not exist"
-msgstr ""
+msgstr "Пользовательская категория %s не существует"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:179
msgid "Add to user category"
-msgstr ""
+msgstr "Добавить в пользовательские категории"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:180
#, python-format
msgid "A user category %s does not exist"
-msgstr ""
+msgstr "Пользовательская категория %s не существует"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/ui.py:305
msgid "Find item in tag browser"
@@ -14701,7 +14706,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:359
#, python-format
msgid "Add %s to user category"
-msgstr ""
+msgstr "Добавить %s в пользовательские категории"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:372
#, python-format
@@ -14711,7 +14716,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:382
#, python-format
msgid "Delete search %s"
-msgstr ""
+msgstr "Удалить поиск %s"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:387
#, python-format
@@ -14721,27 +14726,27 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:394
#, python-format
msgid "Search for %s"
-msgstr ""
+msgstr "Искать %s"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:399
#, python-format
msgid "Search for everything but %s"
-msgstr ""
+msgstr "Искать всё кроме %s"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:411
#, python-format
msgid "Add sub-category to %s"
-msgstr ""
+msgstr "Добавить подкатегорию в %s"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:415
#, python-format
msgid "Delete user category %s"
-msgstr ""
+msgstr "Удалить пользовательскую категорию %s"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:420
#, python-format
msgid "Hide category %s"
-msgstr ""
+msgstr "Скрыть категорию %s"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:424
msgid "Show category"
@@ -14750,12 +14755,12 @@ msgstr "Показать категорию"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:434
#, python-format
msgid "Search for books in category %s"
-msgstr ""
+msgstr "Искать книги в категории %s"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:440
#, python-format
msgid "Search for books not in category %s"
-msgstr ""
+msgstr "Искать книги НЕ в категории %s"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:449
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/view.py:454
@@ -14837,7 +14842,7 @@ msgstr "Извлечь подключенное устройство"
#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:347
msgid "Debug mode"
-msgstr ""
+msgstr "Резим отладки"
#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:348
#, python-format
@@ -14875,7 +14880,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:630
msgid "Active jobs"
-msgstr ""
+msgstr "Активные задания"
#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:698
msgid ""
@@ -14898,11 +14903,11 @@ msgstr "Доступно обновление!"
#: /home/kovid/work/calibre/src/calibre/gui2/update.py:84
msgid "Show this notification for future updates"
-msgstr ""
+msgstr "Показвать сообщение о доступности новой версии (обнивления)"
#: /home/kovid/work/calibre/src/calibre/gui2/update.py:89
msgid "&Get update"
-msgstr ""
+msgstr "&Скачать обнивление"
#: /home/kovid/work/calibre/src/calibre/gui2/update.py:93
msgid "Update &plugins"
@@ -14929,11 +14934,11 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/update.py:187
#, python-format
msgid "There are %d plugin updates available"
-msgstr ""
+msgstr "Доступны обновления для %d плагинов"
#: /home/kovid/work/calibre/src/calibre/gui2/update.py:191
msgid "Install and configure user plugins"
-msgstr ""
+msgstr "Установка и настройка пользовательских плагинов"
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:43
msgid "Edit bookmark"