mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
KG updates with new database backend
This commit is contained in:
commit
b8b7b2d8c5
78
recipes/daytona_beach.recipe
Normal file
78
recipes/daytona_beach.recipe
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class DaytonBeachNewsJournal(BasicNewsRecipe):
|
||||||
|
title ='Daytona Beach News Journal'
|
||||||
|
__author__ = 'BRGriff'
|
||||||
|
pubisher = 'News-JournalOnline.com'
|
||||||
|
description = 'Daytona Beach, Florida, Newspaper'
|
||||||
|
category = 'News, Daytona Beach, Florida'
|
||||||
|
oldest_article = 1
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'en'
|
||||||
|
filterDuplicates = True
|
||||||
|
remove_attributes = ['style']
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'class':'page-header'}),
|
||||||
|
dict(name='div', attrs={'class':'asset-body'})
|
||||||
|
]
|
||||||
|
remove_tags = [dict(name='div', attrs={'class':['byline-section', 'asset-meta']})
|
||||||
|
]
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
#####NEWS#####
|
||||||
|
(u"News", u"http://www.news-journalonline.com/rss.xml"),
|
||||||
|
(u"Breaking News", u"http://www.news-journalonline.com/breakingnews/rss.xml"),
|
||||||
|
(u"Local - East Volusia", u"http://www.news-journalonline.com/news/local/east-volusia/rss.xml"),
|
||||||
|
(u"Local - West Volusia", u"http://www.news-journalonline.com/news/local/west-volusia/rss.xml"),
|
||||||
|
(u"Local - Southeast", u"http://www.news-journalonline.com/news/local/southeast-volusia/rss.xml"),
|
||||||
|
(u"Local - Flagler", u"http://www.news-journalonline.com/news/local/flagler/rss.xml"),
|
||||||
|
(u"Florida", u"http://www.news-journalonline.com/news/florida/rss.xml"),
|
||||||
|
(u"National/World", u"http://www.news-journalonline.com/news/nationworld/rss.xml"),
|
||||||
|
(u"Politics", u"http://www.news-journalonline.com/news/politics/rss.xml"),
|
||||||
|
(u"News of Record", u"http://www.news-journalonline.com/news/news-of-record/rss.xml"),
|
||||||
|
####BUSINESS####
|
||||||
|
(u"Business", u"http://www.news-journalonline.com/business/rss.xml"),
|
||||||
|
#(u"Jobs", u"http://www.news-journalonline.com/business/jobs/rss.xml"),
|
||||||
|
#(u"Markets", u"http://www.news-journalonline.com/business/markets/rss.xml"),
|
||||||
|
#(u"Real Estate", u"http://www.news-journalonline.com/business/real-estate/rss.xml"),
|
||||||
|
#(u"Technology", u"http://www.news-journalonline.com/business/technology/rss.xml"),
|
||||||
|
####SPORTS####
|
||||||
|
(u"Sports", u"http://www.news-journalonline.com/sports/rss.xml"),
|
||||||
|
(u"Racing", u"http://www.news-journalonline.com/racing/rss.xml"),
|
||||||
|
(u"Highschool", u"http://www.news-journalonline.com/sports/highschool/rss.xml"),
|
||||||
|
(u"College", u"http://www.news-journalonline.com/sports/college/rss.xml"),
|
||||||
|
(u"Basketball", u"http://www.news-journalonline.com/sports/basketball/rss.xml"),
|
||||||
|
(u"Football", u"http://www.news-journalonline.com/sports/football/rss.xml"),
|
||||||
|
(u"Golf", u"http://www.news-journalonline.com/sports/golf/rss.xml"),
|
||||||
|
(u"Other Sports", u"http://www.news-journalonline.com/sports/other/rss.xml"),
|
||||||
|
####LIFESTYLE####
|
||||||
|
(u"Lifestyle", u"http://www.news-journalonline.com/lifestyle/rss.xml"),
|
||||||
|
#(u"Fashion", u"http://www.news-journalonline.com/lifestyle/fashion/rss.xml"),
|
||||||
|
(u"Food", u"http://www.news-journalonline.com/lifestyle/food/rss.xml"),
|
||||||
|
#(u"Health", u"http://www.news-journalonline.com/lifestyle/health/rss.xml"),
|
||||||
|
(u"Home and Garden", u"http://www.news-journalonline.com/lifestyle/home-and-garden/rss.xml"),
|
||||||
|
(u"Living", u"http://www.news-journalonline.com/lifestyle/living/rss.xml"),
|
||||||
|
(u"Religion", u"http://www.news-journalonline.com/lifestyle/religion/rss.xml"),
|
||||||
|
#(u"Travel", u"http://www.news-journalonline.com/lifestyle/travel/rss.xml"),
|
||||||
|
####OPINION####
|
||||||
|
#(u"Opinion", u"http://www.news-journalonline.com/opinion/rss.xml"),
|
||||||
|
#(u"Letters to Editor", u"http://www.news-journalonline.com/opinion/letters-to-the-editor/rss.xml"),
|
||||||
|
#(u"Columns", u"http://www.news-journalonline.com/columns/rss.xml"),
|
||||||
|
#(u"Podcasts", u"http://www.news-journalonline.com/podcasts/rss.xml"),
|
||||||
|
####ENTERTAINMENT#### ##Weekly Feature##
|
||||||
|
(u"Entertainment", u"http://www.go386.com/rss.xml"),
|
||||||
|
(u"Go Out", u"http://www.go386.com/go/rss.xml"),
|
||||||
|
(u"Music", u"http://www.go386.com/music/rss.xml"),
|
||||||
|
(u"Movies", u"http://www.go386.com/movies/rss.xml"),
|
||||||
|
#(u"Culture", u"http://www.go386.com/culture/rss.xml"),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
.page-header{font-family:Arial,Helvetica,sans-serif; font-style:bold;font-size:22pt;}
|
||||||
|
.asset-body{font-family:Helvetica,Arial,sans-serif; font-size:16pt;}
|
||||||
|
|
||||||
|
'''
|
61
recipes/elclubdelebook.recipe
Normal file
61
recipes/elclubdelebook.recipe
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
www.clubdelebook.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class ElClubDelEbook(BasicNewsRecipe):
|
||||||
|
title = 'El club del ebook'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'El Club del eBook, es la primera fuente de informacion sobre ebooks de Argentina. Aca vas a encontrar noticias, tips, tutoriales, recursos y opiniones sobre el mundo de los libros electronicos.'
|
||||||
|
tags = 'ebook, libro electronico, e-book, ebooks, libros electronicos, e-books'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
language = 'es_AR'
|
||||||
|
encoding = 'utf-8'
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = True
|
||||||
|
publication_type = 'blog'
|
||||||
|
masthead_url = 'http://dl.dropbox.com/u/2845131/elclubdelebook.png'
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: Arial,Helvetica,sans-serif}
|
||||||
|
img{ margin-bottom: 0.8em;
|
||||||
|
border: 1px solid #333333;
|
||||||
|
padding: 4px; display: block
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : tags
|
||||||
|
, 'publisher': title
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_tags = [dict(attrs={'id':'crp_related'})]
|
||||||
|
remove_tags_after = dict(attrs={'id':'crp_related'})
|
||||||
|
|
||||||
|
feeds = [(u'Articulos', u'http://feeds.feedburner.com/ElClubDelEbook')]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll('a'):
|
||||||
|
limg = item.find('img')
|
||||||
|
if item.string is not None:
|
||||||
|
str = item.string
|
||||||
|
item.replaceWith(str)
|
||||||
|
else:
|
||||||
|
if limg:
|
||||||
|
item.name = 'div'
|
||||||
|
item.attrs = []
|
||||||
|
else:
|
||||||
|
str = self.tag_to_string(item)
|
||||||
|
item.replaceWith(str)
|
||||||
|
for item in soup.findAll('img'):
|
||||||
|
if not item.has_key('alt'):
|
||||||
|
item['alt'] = 'image'
|
||||||
|
return soup
|
81
recipes/frontlineonnet.recipe
Normal file
81
recipes/frontlineonnet.recipe
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
frontlineonnet.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
import re
|
||||||
|
from calibre import strftime
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Frontlineonnet(BasicNewsRecipe):
|
||||||
|
title = 'Frontline'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = "India's national magazine"
|
||||||
|
publisher = 'Frontline'
|
||||||
|
category = 'news, politics, India'
|
||||||
|
no_stylesheets = True
|
||||||
|
delay = 1
|
||||||
|
INDEX = 'http://frontlineonnet.com/'
|
||||||
|
use_embedded_content = False
|
||||||
|
encoding = 'cp1252'
|
||||||
|
language = 'en_IN'
|
||||||
|
publication_type = 'magazine'
|
||||||
|
masthead_url = 'http://frontlineonnet.com/images/newfline.jpg'
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: Verdana,Arial,Helvetica,sans-serif}
|
||||||
|
img{margin-top:0.5em; margin-bottom: 0.7em; display: block}
|
||||||
|
"""
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
, 'linearize_tables' : True
|
||||||
|
}
|
||||||
|
|
||||||
|
preprocess_regexps = [
|
||||||
|
(re.compile(r'.*?<base', re.DOTALL|re.IGNORECASE),lambda match: '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html dir="ltr" xml:lang="en-IN"><head><title>title</title><base')
|
||||||
|
,(re.compile(r'<base .*?>', re.DOTALL|re.IGNORECASE),lambda match: '</head><body>')
|
||||||
|
,(re.compile(r'<byline>', re.DOTALL|re.IGNORECASE),lambda match: '<div class="byline">')
|
||||||
|
,(re.compile(r'</byline>', re.DOTALL|re.IGNORECASE),lambda match: '</div>')
|
||||||
|
,(re.compile(r'<center>', re.DOTALL|re.IGNORECASE),lambda match: '<div class="ctr">')
|
||||||
|
,(re.compile(r'</center>', re.DOTALL|re.IGNORECASE),lambda match: '</div>')
|
||||||
|
]
|
||||||
|
|
||||||
|
keep_only_tags= [
|
||||||
|
dict(name='font', attrs={'class':'storyhead'})
|
||||||
|
,dict(attrs={'class':'byline'})
|
||||||
|
]
|
||||||
|
remove_attributes=['size','noshade','border']
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll('img'):
|
||||||
|
if not item.has_key('alt'):
|
||||||
|
item['alt'] = 'image'
|
||||||
|
return soup
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
articles = []
|
||||||
|
soup = self.index_to_soup(self.INDEX)
|
||||||
|
for feed_link in soup.findAll('a',href=True):
|
||||||
|
if feed_link['href'].startswith('stories/'):
|
||||||
|
url = self.INDEX + feed_link['href']
|
||||||
|
title = self.tag_to_string(feed_link)
|
||||||
|
date = strftime(self.timefmt)
|
||||||
|
articles.append({
|
||||||
|
'title' :title
|
||||||
|
,'date' :date
|
||||||
|
,'url' :url
|
||||||
|
,'description':''
|
||||||
|
})
|
||||||
|
return [('Frontline', articles)]
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return "http://www.hinduonnet.com/thehindu/thscrip/print.pl?prd=fline&file=" + url.rpartition('/')[2]
|
||||||
|
|
||||||
|
def image_url_processor(self, baseurl, url):
|
||||||
|
return url.replace('../images/', self.INDEX + 'images/').strip()
|
BIN
recipes/icons/elclubdelebook.png
Normal file
BIN
recipes/icons/elclubdelebook.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
@ -13,8 +13,10 @@ CREATE TABLE books ( id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||||||
isbn TEXT DEFAULT "" COLLATE NOCASE,
|
isbn TEXT DEFAULT "" COLLATE NOCASE,
|
||||||
lccn TEXT DEFAULT "" COLLATE NOCASE,
|
lccn TEXT DEFAULT "" COLLATE NOCASE,
|
||||||
path TEXT NOT NULL DEFAULT "",
|
path TEXT NOT NULL DEFAULT "",
|
||||||
flags INTEGER NOT NULL DEFAULT 1
|
flags INTEGER NOT NULL DEFAULT 1,
|
||||||
, uuid TEXT, has_cover BOOL DEFAULT 0, last_modified TIMESTAMP NOT NULL DEFAULT "2000-01-01 00:00:00+00:00");
|
uuid TEXT,
|
||||||
|
has_cover BOOL DEFAULT 0,
|
||||||
|
last_modified TIMESTAMP NOT NULL DEFAULT "2000-01-01 00:00:00+00:00");
|
||||||
CREATE TABLE books_authors_link ( id INTEGER PRIMARY KEY,
|
CREATE TABLE books_authors_link ( id INTEGER PRIMARY KEY,
|
||||||
book INTEGER NOT NULL,
|
book INTEGER NOT NULL,
|
||||||
author INTEGER NOT NULL,
|
author INTEGER NOT NULL,
|
||||||
|
@ -578,6 +578,7 @@ def url_slash_cleaner(url):
|
|||||||
def get_download_filename(url, cookie_file=None):
|
def get_download_filename(url, cookie_file=None):
|
||||||
'''
|
'''
|
||||||
Get a local filename for a URL using the content disposition header
|
Get a local filename for a URL using the content disposition header
|
||||||
|
Returns empty string if no content disposition header present
|
||||||
'''
|
'''
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from urllib2 import unquote as urllib2_unquote
|
from urllib2 import unquote as urllib2_unquote
|
||||||
@ -591,8 +592,10 @@ def get_download_filename(url, cookie_file=None):
|
|||||||
cj.load(cookie_file)
|
cj.load(cookie_file)
|
||||||
br.set_cookiejar(cj)
|
br.set_cookiejar(cj)
|
||||||
|
|
||||||
|
last_part_name = ''
|
||||||
try:
|
try:
|
||||||
with closing(br.open(url)) as r:
|
with closing(br.open(url)) as r:
|
||||||
|
last_part_name = r.geturl().split('/')[-1]
|
||||||
disposition = r.info().get('Content-disposition', '')
|
disposition = r.info().get('Content-disposition', '')
|
||||||
for p in disposition.split(';'):
|
for p in disposition.split(';'):
|
||||||
if 'filename' in p:
|
if 'filename' in p:
|
||||||
@ -612,7 +615,7 @@ def get_download_filename(url, cookie_file=None):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
if not filename:
|
if not filename:
|
||||||
filename = r.geturl().split('/')[-1]
|
filename = last_part_name
|
||||||
|
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
@ -762,99 +762,132 @@ plugins += input_profiles + output_profiles
|
|||||||
class ActionAdd(InterfaceActionBase):
|
class ActionAdd(InterfaceActionBase):
|
||||||
name = 'Add Books'
|
name = 'Add Books'
|
||||||
actual_plugin = 'calibre.gui2.actions.add:AddAction'
|
actual_plugin = 'calibre.gui2.actions.add:AddAction'
|
||||||
|
description = _('Add books to calibre or the connected device')
|
||||||
|
|
||||||
class ActionFetchAnnotations(InterfaceActionBase):
|
class ActionFetchAnnotations(InterfaceActionBase):
|
||||||
name = 'Fetch Annotations'
|
name = 'Fetch Annotations'
|
||||||
actual_plugin = 'calibre.gui2.actions.annotate:FetchAnnotationsAction'
|
actual_plugin = 'calibre.gui2.actions.annotate:FetchAnnotationsAction'
|
||||||
|
description = _('Fetch annotations from a connected Kindle (experimental)')
|
||||||
|
|
||||||
class ActionGenerateCatalog(InterfaceActionBase):
|
class ActionGenerateCatalog(InterfaceActionBase):
|
||||||
name = 'Generate Catalog'
|
name = 'Generate Catalog'
|
||||||
actual_plugin = 'calibre.gui2.actions.catalog:GenerateCatalogAction'
|
actual_plugin = 'calibre.gui2.actions.catalog:GenerateCatalogAction'
|
||||||
|
description = _('Generate a catalog of the books in your calibre library')
|
||||||
|
|
||||||
class ActionConvert(InterfaceActionBase):
|
class ActionConvert(InterfaceActionBase):
|
||||||
name = 'Convert Books'
|
name = 'Convert Books'
|
||||||
actual_plugin = 'calibre.gui2.actions.convert:ConvertAction'
|
actual_plugin = 'calibre.gui2.actions.convert:ConvertAction'
|
||||||
|
description = _('Convert books to various ebook formats')
|
||||||
|
|
||||||
class ActionDelete(InterfaceActionBase):
|
class ActionDelete(InterfaceActionBase):
|
||||||
name = 'Remove Books'
|
name = 'Remove Books'
|
||||||
actual_plugin = 'calibre.gui2.actions.delete:DeleteAction'
|
actual_plugin = 'calibre.gui2.actions.delete:DeleteAction'
|
||||||
|
description = _('Delete books from your calibre library or connected device')
|
||||||
|
|
||||||
class ActionEditMetadata(InterfaceActionBase):
|
class ActionEditMetadata(InterfaceActionBase):
|
||||||
name = 'Edit Metadata'
|
name = 'Edit Metadata'
|
||||||
actual_plugin = 'calibre.gui2.actions.edit_metadata:EditMetadataAction'
|
actual_plugin = 'calibre.gui2.actions.edit_metadata:EditMetadataAction'
|
||||||
|
description = _('Edit the metadata of books in your calibre library')
|
||||||
|
|
||||||
class ActionView(InterfaceActionBase):
|
class ActionView(InterfaceActionBase):
|
||||||
name = 'View'
|
name = 'View'
|
||||||
actual_plugin = 'calibre.gui2.actions.view:ViewAction'
|
actual_plugin = 'calibre.gui2.actions.view:ViewAction'
|
||||||
|
description = _('Read books in your calibre library')
|
||||||
|
|
||||||
class ActionFetchNews(InterfaceActionBase):
|
class ActionFetchNews(InterfaceActionBase):
|
||||||
name = 'Fetch News'
|
name = 'Fetch News'
|
||||||
actual_plugin = 'calibre.gui2.actions.fetch_news:FetchNewsAction'
|
actual_plugin = 'calibre.gui2.actions.fetch_news:FetchNewsAction'
|
||||||
|
description = _('Download news from the internet in ebook form')
|
||||||
|
|
||||||
|
class ActionQuickview(InterfaceActionBase):
|
||||||
|
name = 'Show Quickview'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.show_quickview:ShowQuickviewAction'
|
||||||
|
description = _('Show a list of related books quickly')
|
||||||
|
|
||||||
class ActionSaveToDisk(InterfaceActionBase):
|
class ActionSaveToDisk(InterfaceActionBase):
|
||||||
name = 'Save To Disk'
|
name = 'Save To Disk'
|
||||||
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
|
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
|
||||||
|
description = _('Export books from your calibre library to the hard disk')
|
||||||
|
|
||||||
class ActionShowBookDetails(InterfaceActionBase):
|
class ActionShowBookDetails(InterfaceActionBase):
|
||||||
name = 'Show Book Details'
|
name = 'Show Book Details'
|
||||||
actual_plugin = 'calibre.gui2.actions.show_book_details:ShowBookDetailsAction'
|
actual_plugin = 'calibre.gui2.actions.show_book_details:ShowBookDetailsAction'
|
||||||
|
description = _('Show book details in a separate popup')
|
||||||
|
|
||||||
class ActionRestart(InterfaceActionBase):
|
class ActionRestart(InterfaceActionBase):
|
||||||
name = 'Restart'
|
name = 'Restart'
|
||||||
actual_plugin = 'calibre.gui2.actions.restart:RestartAction'
|
actual_plugin = 'calibre.gui2.actions.restart:RestartAction'
|
||||||
|
description = _('Restart calibre')
|
||||||
|
|
||||||
class ActionOpenFolder(InterfaceActionBase):
|
class ActionOpenFolder(InterfaceActionBase):
|
||||||
name = 'Open Folder'
|
name = 'Open Folder'
|
||||||
actual_plugin = 'calibre.gui2.actions.open:OpenFolderAction'
|
actual_plugin = 'calibre.gui2.actions.open:OpenFolderAction'
|
||||||
|
description = _('Open the folder that contains the book files in your'
|
||||||
|
' calibre library')
|
||||||
|
|
||||||
class ActionSendToDevice(InterfaceActionBase):
|
class ActionSendToDevice(InterfaceActionBase):
|
||||||
name = 'Send To Device'
|
name = 'Send To Device'
|
||||||
actual_plugin = 'calibre.gui2.actions.device:SendToDeviceAction'
|
actual_plugin = 'calibre.gui2.actions.device:SendToDeviceAction'
|
||||||
|
description = _('Send books to the connected device')
|
||||||
|
|
||||||
class ActionConnectShare(InterfaceActionBase):
|
class ActionConnectShare(InterfaceActionBase):
|
||||||
name = 'Connect Share'
|
name = 'Connect Share'
|
||||||
actual_plugin = 'calibre.gui2.actions.device:ConnectShareAction'
|
actual_plugin = 'calibre.gui2.actions.device:ConnectShareAction'
|
||||||
|
description = _('Send books via email or the web also connect to iTunes or'
|
||||||
|
' folders on your computer as if they are devices')
|
||||||
|
|
||||||
class ActionHelp(InterfaceActionBase):
|
class ActionHelp(InterfaceActionBase):
|
||||||
name = 'Help'
|
name = 'Help'
|
||||||
actual_plugin = 'calibre.gui2.actions.help:HelpAction'
|
actual_plugin = 'calibre.gui2.actions.help:HelpAction'
|
||||||
|
description = _('Browse the calibre User Manual')
|
||||||
|
|
||||||
class ActionPreferences(InterfaceActionBase):
|
class ActionPreferences(InterfaceActionBase):
|
||||||
name = 'Preferences'
|
name = 'Preferences'
|
||||||
actual_plugin = 'calibre.gui2.actions.preferences:PreferencesAction'
|
actual_plugin = 'calibre.gui2.actions.preferences:PreferencesAction'
|
||||||
|
description = _('Customize calibre')
|
||||||
|
|
||||||
class ActionSimilarBooks(InterfaceActionBase):
|
class ActionSimilarBooks(InterfaceActionBase):
|
||||||
name = 'Similar Books'
|
name = 'Similar Books'
|
||||||
actual_plugin = 'calibre.gui2.actions.similar_books:SimilarBooksAction'
|
actual_plugin = 'calibre.gui2.actions.similar_books:SimilarBooksAction'
|
||||||
|
description = _('Easily find books similar to the currently selected one')
|
||||||
|
|
||||||
class ActionChooseLibrary(InterfaceActionBase):
|
class ActionChooseLibrary(InterfaceActionBase):
|
||||||
name = 'Choose Library'
|
name = 'Choose Library'
|
||||||
actual_plugin = 'calibre.gui2.actions.choose_library:ChooseLibraryAction'
|
actual_plugin = 'calibre.gui2.actions.choose_library:ChooseLibraryAction'
|
||||||
|
description = _('Switch between different calibre libraries and perform'
|
||||||
|
' maintenance on them')
|
||||||
|
|
||||||
class ActionAddToLibrary(InterfaceActionBase):
|
class ActionAddToLibrary(InterfaceActionBase):
|
||||||
name = 'Add To Library'
|
name = 'Add To Library'
|
||||||
actual_plugin = 'calibre.gui2.actions.add_to_library:AddToLibraryAction'
|
actual_plugin = 'calibre.gui2.actions.add_to_library:AddToLibraryAction'
|
||||||
|
description = _('Copy books from the devce to your calibre library')
|
||||||
|
|
||||||
class ActionEditCollections(InterfaceActionBase):
|
class ActionEditCollections(InterfaceActionBase):
|
||||||
name = 'Edit Collections'
|
name = 'Edit Collections'
|
||||||
actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction'
|
actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction'
|
||||||
|
description = _('Edit the collections in which books are placed on your device')
|
||||||
|
|
||||||
class ActionCopyToLibrary(InterfaceActionBase):
|
class ActionCopyToLibrary(InterfaceActionBase):
|
||||||
name = 'Copy To Library'
|
name = 'Copy To Library'
|
||||||
actual_plugin = 'calibre.gui2.actions.copy_to_library:CopyToLibraryAction'
|
actual_plugin = 'calibre.gui2.actions.copy_to_library:CopyToLibraryAction'
|
||||||
|
description = _('Copy a book from one calibre library to another')
|
||||||
|
|
||||||
class ActionTweakEpub(InterfaceActionBase):
|
class ActionTweakEpub(InterfaceActionBase):
|
||||||
name = 'Tweak ePub'
|
name = 'Tweak ePub'
|
||||||
actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction'
|
actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction'
|
||||||
|
description = _('Make small twekas to epub files in your calibre library')
|
||||||
|
|
||||||
class ActionNextMatch(InterfaceActionBase):
|
class ActionNextMatch(InterfaceActionBase):
|
||||||
name = 'Next Match'
|
name = 'Next Match'
|
||||||
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
||||||
|
description = _('Find the next or previous match when searching in '
|
||||||
|
'your calibre library in highlight mode')
|
||||||
|
|
||||||
class ActionStore(InterfaceActionBase):
|
class ActionStore(InterfaceActionBase):
|
||||||
name = 'Store'
|
name = 'Store'
|
||||||
author = 'John Schember'
|
author = 'John Schember'
|
||||||
actual_plugin = 'calibre.gui2.actions.store:StoreAction'
|
actual_plugin = 'calibre.gui2.actions.store:StoreAction'
|
||||||
|
description = _('Search for books from different book sellers')
|
||||||
|
|
||||||
def customization_help(self, gui=False):
|
def customization_help(self, gui=False):
|
||||||
return 'Customize the behavior of the store search.'
|
return 'Customize the behavior of the store search.'
|
||||||
@ -870,13 +903,13 @@ class ActionStore(InterfaceActionBase):
|
|||||||
class ActionPluginUpdater(InterfaceActionBase):
|
class ActionPluginUpdater(InterfaceActionBase):
|
||||||
name = 'Plugin Updater'
|
name = 'Plugin Updater'
|
||||||
author = 'Grant Drake'
|
author = 'Grant Drake'
|
||||||
description = 'Queries the MobileRead forums for updates to plugins to install'
|
description = _('Get new calibre plugins or update your existing ones')
|
||||||
actual_plugin = 'calibre.gui2.actions.plugin_updates:PluginUpdaterAction'
|
actual_plugin = 'calibre.gui2.actions.plugin_updates:PluginUpdaterAction'
|
||||||
|
|
||||||
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||||
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
||||||
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
|
ActionFetchNews, ActionSaveToDisk, ActionQuickview,
|
||||||
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
ActionShowBookDetails,ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||||
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||||
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
||||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore,
|
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore,
|
||||||
@ -1307,6 +1340,16 @@ class StoreLegimiStore(StoreBase):
|
|||||||
headquarters = 'PL'
|
headquarters = 'PL'
|
||||||
formats = ['EPUB']
|
formats = ['EPUB']
|
||||||
|
|
||||||
|
class StoreLibreDEStore(StoreBase):
|
||||||
|
name = 'Libri DE'
|
||||||
|
author = 'Charles Haley'
|
||||||
|
description = u'Sicher Bücher, Hörbücher und Downloads online bestellen.'
|
||||||
|
actual_plugin = 'calibre.gui2.store.libri_de_plugin:LibreDEStore'
|
||||||
|
|
||||||
|
headquarters = 'DE'
|
||||||
|
formats = ['EPUB', 'PDF']
|
||||||
|
affiliate = True
|
||||||
|
|
||||||
class StoreManyBooksStore(StoreBase):
|
class StoreManyBooksStore(StoreBase):
|
||||||
name = 'ManyBooks'
|
name = 'ManyBooks'
|
||||||
description = u'Public domain and creative commons works from many sources.'
|
description = u'Public domain and creative commons works from many sources.'
|
||||||
@ -1457,6 +1500,7 @@ plugins += [
|
|||||||
StoreGutenbergStore,
|
StoreGutenbergStore,
|
||||||
StoreKoboStore,
|
StoreKoboStore,
|
||||||
StoreLegimiStore,
|
StoreLegimiStore,
|
||||||
|
StoreLibreDEStore,
|
||||||
StoreManyBooksStore,
|
StoreManyBooksStore,
|
||||||
StoreMobileReadStore,
|
StoreMobileReadStore,
|
||||||
StoreNextoStore,
|
StoreNextoStore,
|
||||||
|
@ -259,6 +259,10 @@ class OutputFormatPlugin(Plugin):
|
|||||||
#: (option_name, recommended_value, recommendation_level)
|
#: (option_name, recommended_value, recommendation_level)
|
||||||
recommendations = set([])
|
recommendations = set([])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return _('Convert ebooks to the %s format'%self.file_type)
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
Plugin.__init__(self, *args)
|
Plugin.__init__(self, *args)
|
||||||
self.report_progress = DummyReporter()
|
self.report_progress = DummyReporter()
|
||||||
|
67
src/calibre/db/__init__.py
Normal file
67
src/calibre/db/__init__.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#!/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 <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
Rewrite of the calibre database backend.
|
||||||
|
|
||||||
|
Broad Objectives:
|
||||||
|
|
||||||
|
* Use the sqlite db only as a datastore. i.e. do not do
|
||||||
|
sorting/searching/concatenation or anything else in sqlite. Instead
|
||||||
|
mirror the sqlite tables in memory, create caches and lookup maps from
|
||||||
|
them and create a set_* API that updates the memory caches and the sqlite
|
||||||
|
correctly.
|
||||||
|
|
||||||
|
* Move from keeping a list of books in memory as a cache to a per table
|
||||||
|
cache. This allows much faster search and sort operations at the expense
|
||||||
|
of slightly slower lookup operations. That slowdown can be mitigated by
|
||||||
|
keeping lots of maps and updating them in the set_* API. Also
|
||||||
|
get_categories becomes blazingly fast.
|
||||||
|
|
||||||
|
* Separate the database layer from the cache layer more cleanly. Rather
|
||||||
|
than having the db layer refer to the cache layer and vice versa, the
|
||||||
|
cache layer will refer to the db layer only and the new API will be
|
||||||
|
defined on the cache layer.
|
||||||
|
|
||||||
|
* Get rid of index_is_id and other poor design decisions
|
||||||
|
|
||||||
|
* Minimize the API as much as possible and define it cleanly
|
||||||
|
|
||||||
|
* Do not change the on disk format of metadata.db at all (this is for
|
||||||
|
backwards compatibility)
|
||||||
|
|
||||||
|
* Get rid of the need for a separate db access thread by switching to apsw
|
||||||
|
to access sqlite, which is thread safe
|
||||||
|
|
||||||
|
* The new API will have methods to efficiently do bulk operations and will
|
||||||
|
use shared/exclusive/pending locks to serialize access to the in-mem data
|
||||||
|
structures. Use the same locking scheme as sqlite itself does.
|
||||||
|
|
||||||
|
How this will proceed:
|
||||||
|
|
||||||
|
1. Create the new API
|
||||||
|
2. Create a test suite for it
|
||||||
|
3. Write a replacement for LibraryDatabase2 that uses the new API
|
||||||
|
internally
|
||||||
|
4. Lots of testing of calibre with the new LibraryDatabase2
|
||||||
|
5. Gradually migrate code to use the (much faster) new api wherever possible (the new api
|
||||||
|
will be exposed via db.new_api)
|
||||||
|
|
||||||
|
I plan to work on this slowly, in parallel to normal calibre development
|
||||||
|
work.
|
||||||
|
|
||||||
|
Various things that require other things before they can be migrated:
|
||||||
|
1. From initialize_dynamic(): set_saved_searches,
|
||||||
|
load_user_template_functions. Also add custom
|
||||||
|
columns/categories/searches info into
|
||||||
|
self.field_metadata. Finally, implement metadata dirtied
|
||||||
|
functionality.
|
||||||
|
|
||||||
|
'''
|
443
src/calibre/db/backend.py
Normal file
443
src/calibre/db/backend.py
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
#!/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 <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
# Imports {{{
|
||||||
|
import os, shutil, uuid, json
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
import apsw
|
||||||
|
|
||||||
|
from calibre import isbytestring, force_unicode, prints
|
||||||
|
from calibre.constants import (iswindows, filesystem_encoding,
|
||||||
|
preferred_encoding)
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.library.schema_upgrades import SchemaUpgrade
|
||||||
|
from calibre.library.field_metadata import FieldMetadata
|
||||||
|
from calibre.ebooks.metadata import title_sort, author_to_author_sort
|
||||||
|
from calibre.utils.icu import strcmp
|
||||||
|
from calibre.utils.config import to_json, from_json, prefs, tweaks
|
||||||
|
from calibre.utils.date import utcfromtimestamp
|
||||||
|
from calibre.db.tables import (OneToOneTable, ManyToOneTable, ManyToManyTable,
|
||||||
|
SizeTable, FormatsTable, AuthorsTable, IdentifiersTable)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
'''
|
||||||
|
Differences in semantics from pysqlite:
|
||||||
|
|
||||||
|
1. execute/executemany/executescript operate in autocommit mode
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
class DynamicFilter(object): # {{{
|
||||||
|
|
||||||
|
'No longer used, present for legacy compatibility'
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.ids = frozenset([])
|
||||||
|
|
||||||
|
def __call__(self, id_):
|
||||||
|
return int(id_ in self.ids)
|
||||||
|
|
||||||
|
def change(self, ids):
|
||||||
|
self.ids = frozenset(ids)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class DBPrefs(dict): # {{{
|
||||||
|
|
||||||
|
'Store preferences as key:value pairs in the db'
|
||||||
|
|
||||||
|
def __init__(self, db):
|
||||||
|
dict.__init__(self)
|
||||||
|
self.db = db
|
||||||
|
self.defaults = {}
|
||||||
|
self.disable_setting = False
|
||||||
|
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
|
||||||
|
try:
|
||||||
|
val = self.raw_to_object(val)
|
||||||
|
except:
|
||||||
|
prints('Failed to read value for:', key, 'from db')
|
||||||
|
continue
|
||||||
|
dict.__setitem__(self, key, val)
|
||||||
|
|
||||||
|
def raw_to_object(self, raw):
|
||||||
|
if not isinstance(raw, unicode):
|
||||||
|
raw = raw.decode(preferred_encoding)
|
||||||
|
return json.loads(raw, object_hook=from_json)
|
||||||
|
|
||||||
|
def to_raw(self, val):
|
||||||
|
return json.dumps(val, indent=2, default=to_json)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
try:
|
||||||
|
return dict.__getitem__(self, key)
|
||||||
|
except KeyError:
|
||||||
|
return self.defaults[key]
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
dict.__delitem__(self, key)
|
||||||
|
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
||||||
|
|
||||||
|
def __setitem__(self, key, val):
|
||||||
|
if self.disable_setting:
|
||||||
|
return
|
||||||
|
raw = self.to_raw(val)
|
||||||
|
self.db.conn.execute('INSERT OR REPLACE INTO preferences (key,val) VALUES (?,?)', (key,
|
||||||
|
raw))
|
||||||
|
dict.__setitem__(self, key, val)
|
||||||
|
|
||||||
|
def set(self, key, val):
|
||||||
|
self.__setitem__(key, val)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Extra collators {{{
|
||||||
|
def pynocase(one, two, encoding='utf-8'):
|
||||||
|
if isbytestring(one):
|
||||||
|
try:
|
||||||
|
one = one.decode(encoding, 'replace')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if isbytestring(two):
|
||||||
|
try:
|
||||||
|
two = two.decode(encoding, 'replace')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return cmp(one.lower(), two.lower())
|
||||||
|
|
||||||
|
def _author_to_author_sort(x):
|
||||||
|
if not x: return ''
|
||||||
|
return author_to_author_sort(x.replace('|', ','))
|
||||||
|
|
||||||
|
def icu_collator(s1, s2):
|
||||||
|
return strcmp(force_unicode(s1, 'utf-8'), force_unicode(s2, 'utf-8'))
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class Connection(apsw.Connection): # {{{
|
||||||
|
|
||||||
|
BUSY_TIMEOUT = 2000 # milliseconds
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
apsw.Connection.__init__(self, path)
|
||||||
|
|
||||||
|
self.setbusytimeout(self.BUSY_TIMEOUT)
|
||||||
|
self.execute('pragma cache_size=5000')
|
||||||
|
self.conn.execute('pragma temp_store=2')
|
||||||
|
|
||||||
|
encoding = self.execute('pragma encoding').fetchone()[0]
|
||||||
|
self.conn.create_collation('PYNOCASE', partial(pynocase,
|
||||||
|
encoding=encoding))
|
||||||
|
|
||||||
|
self.conn.create_function('title_sort', 1, title_sort)
|
||||||
|
self.conn.create_function('author_to_author_sort', 1,
|
||||||
|
_author_to_author_sort)
|
||||||
|
|
||||||
|
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
|
||||||
|
|
||||||
|
# Dummy functions for dynamically created filters
|
||||||
|
self.conn.create_function('books_list_filter', 1, lambda x: 1)
|
||||||
|
self.conn.create_collation('icucollate', icu_collator)
|
||||||
|
|
||||||
|
def create_dynamic_filter(self, name):
|
||||||
|
f = DynamicFilter(name)
|
||||||
|
self.conn.create_function(name, 1, f)
|
||||||
|
|
||||||
|
def get(self, *args, **kw):
|
||||||
|
ans = self.cursor().execute(*args)
|
||||||
|
if kw.get('all', True):
|
||||||
|
return ans.fetchall()
|
||||||
|
for row in ans:
|
||||||
|
return ans[0]
|
||||||
|
|
||||||
|
def execute(self, sql, bindings=None):
|
||||||
|
cursor = self.cursor()
|
||||||
|
return cursor.execute(sql, bindings)
|
||||||
|
|
||||||
|
def executemany(self, sql, sequence_of_bindings):
|
||||||
|
return self.cursor().executemany(sql, sequence_of_bindings)
|
||||||
|
|
||||||
|
def executescript(self, sql):
|
||||||
|
with self:
|
||||||
|
# Use an explicit savepoint so that even if this is called
|
||||||
|
# while a transaction is active, it is atomic
|
||||||
|
return self.cursor().execute(sql)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class DB(object, SchemaUpgrade):
|
||||||
|
|
||||||
|
PATH_LIMIT = 40 if iswindows else 100
|
||||||
|
WINDOWS_LIBRARY_PATH_LIMIT = 75
|
||||||
|
|
||||||
|
# Initialize database {{{
|
||||||
|
|
||||||
|
def __init__(self, library_path, default_prefs=None, read_only=False):
|
||||||
|
try:
|
||||||
|
if isbytestring(library_path):
|
||||||
|
library_path = library_path.decode(filesystem_encoding)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
self.field_metadata = FieldMetadata()
|
||||||
|
|
||||||
|
self.library_path = os.path.abspath(library_path)
|
||||||
|
self.dbpath = os.path.join(library_path, 'metadata.db')
|
||||||
|
self.dbpath = os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH',
|
||||||
|
self.dbpath)
|
||||||
|
|
||||||
|
if iswindows and len(self.library_path) + 4*self.PATH_LIMIT + 10 > 259:
|
||||||
|
raise ValueError(_(
|
||||||
|
'Path to library too long. Must be less than'
|
||||||
|
' %d characters.')%(259-4*self.PATH_LIMIT-10))
|
||||||
|
exists = self._exists = os.path.exists(self.dbpath)
|
||||||
|
if not exists:
|
||||||
|
# Be more strict when creating new libraries as the old calculation
|
||||||
|
# allowed for max path lengths of 265 chars.
|
||||||
|
if (iswindows and len(self.library_path) >
|
||||||
|
self.WINDOWS_LIBRARY_PATH_LIMIT):
|
||||||
|
raise ValueError(_(
|
||||||
|
'Path to library too long. Must be less than'
|
||||||
|
' %d characters.')%self.WINDOWS_LIBRARY_PATH_LIMIT)
|
||||||
|
|
||||||
|
if read_only and os.path.exists(self.dbpath):
|
||||||
|
# Work on only a copy of metadata.db to ensure that
|
||||||
|
# metadata.db is not changed
|
||||||
|
pt = PersistentTemporaryFile('_metadata_ro.db')
|
||||||
|
pt.close()
|
||||||
|
shutil.copyfile(self.dbpath, pt.name)
|
||||||
|
self.dbpath = pt.name
|
||||||
|
|
||||||
|
self.is_case_sensitive = (not iswindows and
|
||||||
|
not os.path.exists(self.dbpath.replace('metadata.db',
|
||||||
|
'MeTAdAtA.dB')))
|
||||||
|
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
if self.user_version == 0:
|
||||||
|
self.initialize_database()
|
||||||
|
|
||||||
|
with self.conn:
|
||||||
|
SchemaUpgrade.__init__(self)
|
||||||
|
|
||||||
|
# Guarantee that the library_id is set
|
||||||
|
self.library_id
|
||||||
|
|
||||||
|
self.initialize_prefs(default_prefs)
|
||||||
|
|
||||||
|
# Fix legacy triggers and columns
|
||||||
|
self.conn.executescript('''
|
||||||
|
DROP TRIGGER IF EXISTS author_insert_trg;
|
||||||
|
CREATE TEMP TRIGGER author_insert_trg
|
||||||
|
AFTER INSERT ON authors
|
||||||
|
BEGIN
|
||||||
|
UPDATE authors SET sort=author_to_author_sort(NEW.name) WHERE id=NEW.id;
|
||||||
|
END;
|
||||||
|
DROP TRIGGER IF EXISTS author_update_trg;
|
||||||
|
CREATE TEMP TRIGGER author_update_trg
|
||||||
|
BEFORE UPDATE ON authors
|
||||||
|
BEGIN
|
||||||
|
UPDATE authors SET sort=author_to_author_sort(NEW.name)
|
||||||
|
WHERE id=NEW.id AND name <> NEW.name;
|
||||||
|
END;
|
||||||
|
UPDATE authors SET sort=author_to_author_sort(name) WHERE sort IS NULL;
|
||||||
|
''')
|
||||||
|
|
||||||
|
def initialize_prefs(self, default_prefs):
|
||||||
|
self.prefs = DBPrefs(self)
|
||||||
|
|
||||||
|
if default_prefs is not None and not self._exists:
|
||||||
|
# Only apply default prefs to a new database
|
||||||
|
for key in default_prefs:
|
||||||
|
# be sure that prefs not to be copied are listed below
|
||||||
|
if key not in frozenset(['news_to_be_synced']):
|
||||||
|
self.prefs[key] = default_prefs[key]
|
||||||
|
if 'field_metadata' in default_prefs:
|
||||||
|
fmvals = [f for f in default_prefs['field_metadata'].values()
|
||||||
|
if f['is_custom']]
|
||||||
|
for f in fmvals:
|
||||||
|
self.create_custom_column(f['label'], f['name'],
|
||||||
|
f['datatype'], f['is_multiple'] is not None,
|
||||||
|
f['is_editable'], f['display'])
|
||||||
|
|
||||||
|
defs = self.prefs.defaults
|
||||||
|
defs['gui_restriction'] = defs['cs_restriction'] = ''
|
||||||
|
defs['categories_using_hierarchy'] = []
|
||||||
|
defs['column_color_rules'] = []
|
||||||
|
|
||||||
|
# Migrate the bool tristate tweak
|
||||||
|
defs['bools_are_tristate'] = \
|
||||||
|
tweaks.get('bool_custom_columns_are_tristate', 'yes') == 'yes'
|
||||||
|
if self.prefs.get('bools_are_tristate') is None:
|
||||||
|
self.prefs.set('bools_are_tristate', defs['bools_are_tristate'])
|
||||||
|
|
||||||
|
# Migrate column coloring rules
|
||||||
|
if self.prefs.get('column_color_name_1', None) is not None:
|
||||||
|
from calibre.library.coloring import migrate_old_rule
|
||||||
|
old_rules = []
|
||||||
|
for i in range(1, 6):
|
||||||
|
col = self.prefs.get('column_color_name_'+str(i), None)
|
||||||
|
templ = self.prefs.get('column_color_template_'+str(i), None)
|
||||||
|
if col and templ:
|
||||||
|
try:
|
||||||
|
del self.prefs['column_color_name_'+str(i)]
|
||||||
|
rules = migrate_old_rule(self.field_metadata, templ)
|
||||||
|
for templ in rules:
|
||||||
|
old_rules.append((col, templ))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if old_rules:
|
||||||
|
self.prefs['column_color_rules'] += old_rules
|
||||||
|
|
||||||
|
# Migrate saved search and user categories to db preference scheme
|
||||||
|
def migrate_preference(key, default):
|
||||||
|
oldval = prefs[key]
|
||||||
|
if oldval != default:
|
||||||
|
self.prefs[key] = oldval
|
||||||
|
prefs[key] = default
|
||||||
|
if key not in self.prefs:
|
||||||
|
self.prefs[key] = default
|
||||||
|
|
||||||
|
migrate_preference('user_categories', {})
|
||||||
|
migrate_preference('saved_searches', {})
|
||||||
|
|
||||||
|
# migrate grouped_search_terms
|
||||||
|
if self.prefs.get('grouped_search_terms', None) is None:
|
||||||
|
try:
|
||||||
|
ogst = tweaks.get('grouped_search_terms', {})
|
||||||
|
ngst = {}
|
||||||
|
for t in ogst:
|
||||||
|
ngst[icu_lower(t)] = ogst[t]
|
||||||
|
self.prefs.set('grouped_search_terms', ngst)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Rename any user categories with names that differ only in case
|
||||||
|
user_cats = self.prefs.get('user_categories', [])
|
||||||
|
catmap = {}
|
||||||
|
for uc in user_cats:
|
||||||
|
ucl = icu_lower(uc)
|
||||||
|
if ucl not in catmap:
|
||||||
|
catmap[ucl] = []
|
||||||
|
catmap[ucl].append(uc)
|
||||||
|
cats_changed = False
|
||||||
|
for uc in catmap:
|
||||||
|
if len(catmap[uc]) > 1:
|
||||||
|
prints('found user category case overlap', catmap[uc])
|
||||||
|
cat = catmap[uc][0]
|
||||||
|
suffix = 1
|
||||||
|
while icu_lower((cat + unicode(suffix))) in catmap:
|
||||||
|
suffix += 1
|
||||||
|
prints('Renaming user category %s to %s'%(cat, cat+unicode(suffix)))
|
||||||
|
user_cats[cat + unicode(suffix)] = user_cats[cat]
|
||||||
|
del user_cats[cat]
|
||||||
|
cats_changed = True
|
||||||
|
if cats_changed:
|
||||||
|
self.prefs.set('user_categories', user_cats)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def conn(self):
|
||||||
|
if self._conn is None:
|
||||||
|
self._conn = apsw.Connection(self.dbpath)
|
||||||
|
if self._exists and self.user_version == 0:
|
||||||
|
self._conn.close()
|
||||||
|
os.remove(self.dbpath)
|
||||||
|
self._conn = apsw.Connection(self.dbpath)
|
||||||
|
return self._conn
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def user_version(self):
|
||||||
|
doc = 'The user version of this database'
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
return self.conn.get('pragma user_version;', all=False)
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
self.conn.execute('pragma user_version=%d'%int(val))
|
||||||
|
|
||||||
|
return property(doc=doc, fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def initialize_database(self):
|
||||||
|
metadata_sqlite = P('metadata_sqlite.sql', data=True,
|
||||||
|
allow_user_override=False).decode('utf-8')
|
||||||
|
self.conn.executescript(metadata_sqlite)
|
||||||
|
if self.user_version == 0:
|
||||||
|
self.user_version = 1
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Database layer API {{{
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def exists_at(cls, path):
|
||||||
|
return path and os.path.exists(os.path.join(path, 'metadata.db'))
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def library_id(self):
|
||||||
|
doc = ('The UUID for this library. As long as the user only operates'
|
||||||
|
' on libraries with calibre, it will be unique')
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
if getattr(self, '_library_id_', None) is None:
|
||||||
|
ans = self.conn.get('SELECT uuid FROM library_id', all=False)
|
||||||
|
if ans is None:
|
||||||
|
ans = str(uuid.uuid4())
|
||||||
|
self.library_id = ans
|
||||||
|
else:
|
||||||
|
self._library_id_ = ans
|
||||||
|
return self._library_id_
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
self._library_id_ = unicode(val)
|
||||||
|
self.conn.execute('''
|
||||||
|
DELETE FROM library_id;
|
||||||
|
INSERT INTO library_id (uuid) VALUES (?);
|
||||||
|
''', self._library_id_)
|
||||||
|
|
||||||
|
return property(doc=doc, fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def last_modified(self):
|
||||||
|
''' Return last modified time as a UTC datetime object '''
|
||||||
|
return utcfromtimestamp(os.stat(self.dbpath).st_mtime)
|
||||||
|
|
||||||
|
def read_tables(self):
|
||||||
|
tables = {}
|
||||||
|
for col in ('title', 'sort', 'author_sort', 'series_index', 'comments',
|
||||||
|
'timestamp', 'published', 'uuid', 'path', 'cover',
|
||||||
|
'last_modified'):
|
||||||
|
metadata = self.field_metadata[col].copy()
|
||||||
|
if metadata['table'] is None:
|
||||||
|
metadata['table'], metadata['column'] == 'books', ('has_cover'
|
||||||
|
if col == 'cover' else col)
|
||||||
|
tables[col] = OneToOneTable(col, metadata)
|
||||||
|
|
||||||
|
for col in ('series', 'publisher', 'rating'):
|
||||||
|
tables[col] = ManyToOneTable(col, self.field_metadata[col].copy())
|
||||||
|
|
||||||
|
for col in ('authors', 'tags', 'formats', 'identifiers'):
|
||||||
|
cls = {
|
||||||
|
'authors':AuthorsTable,
|
||||||
|
'formats':FormatsTable,
|
||||||
|
'identifiers':IdentifiersTable,
|
||||||
|
}.get(col, ManyToManyTable)
|
||||||
|
tables[col] = cls(col, self.field_metadata[col].copy())
|
||||||
|
|
||||||
|
tables['size'] = SizeTable('size', self.field_metadata['size'].copy())
|
||||||
|
|
||||||
|
with self.conn: # Use a single transaction, to ensure nothing modifies
|
||||||
|
# the db while we are reading
|
||||||
|
for table in tables.itervalues():
|
||||||
|
try:
|
||||||
|
table.read()
|
||||||
|
except:
|
||||||
|
prints('Failed to read table:', table.name)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return tables
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
13
src/calibre/db/errors.py
Normal file
13
src/calibre/db/errors.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#!/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 <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
class NoSuchFormat(ValueError):
|
||||||
|
pass
|
||||||
|
|
143
src/calibre/db/tables.py
Normal file
143
src/calibre/db/tables.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
#!/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 <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from dateutil.tz import tzoffset
|
||||||
|
|
||||||
|
from calibre.constants import plugins
|
||||||
|
from calibre.utils.date import parse_date, local_tz
|
||||||
|
from calibre.ebooks.metadata import author_to_author_sort
|
||||||
|
|
||||||
|
_c_speedup = plugins['speedup'][0]
|
||||||
|
|
||||||
|
def _c_convert_timestamp(val):
|
||||||
|
if not val:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
ret = _c_speedup.parse_date(val.strip())
|
||||||
|
except:
|
||||||
|
ret = None
|
||||||
|
if ret is None:
|
||||||
|
return parse_date(val, as_utc=False)
|
||||||
|
year, month, day, hour, minutes, seconds, tzsecs = ret
|
||||||
|
return datetime(year, month, day, hour, minutes, seconds,
|
||||||
|
tzinfo=tzoffset(None, tzsecs)).astimezone(local_tz)
|
||||||
|
|
||||||
|
class Table(object):
|
||||||
|
|
||||||
|
def __init__(self, name, metadata):
|
||||||
|
self.name, self.metadata = name, metadata
|
||||||
|
|
||||||
|
# self.adapt() maps values from the db to python objects
|
||||||
|
self.adapt = \
|
||||||
|
{
|
||||||
|
'datetime': _c_convert_timestamp,
|
||||||
|
'bool': bool
|
||||||
|
}.get(
|
||||||
|
metadata['datatype'], lambda x: x)
|
||||||
|
if name == 'authors':
|
||||||
|
# Legacy
|
||||||
|
self.adapt = lambda x: x.replace('|', ',') if x else None
|
||||||
|
|
||||||
|
class OneToOneTable(Table):
|
||||||
|
|
||||||
|
def read(self, db):
|
||||||
|
self.book_col_map = {}
|
||||||
|
idcol = 'id' if self.metadata['table'] == 'books' else 'book'
|
||||||
|
for row in db.conn.execute('SELECT {0}, {1} FROM {2}'.format(idcol,
|
||||||
|
self.metadata['column'], self.metadata['table'])):
|
||||||
|
self.book_col_map[row[0]] = self.adapt(row[1])
|
||||||
|
|
||||||
|
class SizeTable(OneToOneTable):
|
||||||
|
|
||||||
|
def read(self, db):
|
||||||
|
self.book_col_map = {}
|
||||||
|
for row in db.conn.execute(
|
||||||
|
'SELECT books.id, (SELECT MAX(uncompressed_size) FROM data '
|
||||||
|
'WHERE data.book=books.id) FROM books'):
|
||||||
|
self.book_col_map[row[0]] = self.adapt(row[1])
|
||||||
|
|
||||||
|
class ManyToOneTable(Table):
|
||||||
|
|
||||||
|
def read(self, db):
|
||||||
|
self.id_map = {}
|
||||||
|
self.extra_map = {}
|
||||||
|
self.col_book_map = {}
|
||||||
|
self.book_col_map = {}
|
||||||
|
self.read_id_maps(db)
|
||||||
|
self.read_maps(db)
|
||||||
|
|
||||||
|
def read_id_maps(self, db):
|
||||||
|
for row in db.conn.execute('SELECT id, {0} FROM {1}'.format(
|
||||||
|
self.metadata['name'], self.metadata['table'])):
|
||||||
|
if row[1]:
|
||||||
|
self.id_map[row[0]] = self.adapt(row[1])
|
||||||
|
|
||||||
|
def read_maps(self, db):
|
||||||
|
for row in db.conn.execute(
|
||||||
|
'SELECT book, {0} FROM books_{1}_link'.format(
|
||||||
|
self.metadata['link_column'], self.metadata['table'])):
|
||||||
|
if row[1] not in self.col_book_map:
|
||||||
|
self.col_book_map[row[1]] = []
|
||||||
|
self.col_book_map.append(row[0])
|
||||||
|
self.book_col_map[row[0]] = row[1]
|
||||||
|
|
||||||
|
class ManyToManyTable(ManyToOneTable):
|
||||||
|
|
||||||
|
def read_maps(self, db):
|
||||||
|
for row in db.conn.execute(
|
||||||
|
'SELECT book, {0} FROM books_{1}_link'.format(
|
||||||
|
self.metadata['link_column'], self.metadata['table'])):
|
||||||
|
if row[1] not in self.col_book_map:
|
||||||
|
self.col_book_map[row[1]] = []
|
||||||
|
self.col_book_map.append(row[0])
|
||||||
|
if row[0] not in self.book_col_map:
|
||||||
|
self.book_col_map[row[0]] = []
|
||||||
|
self.book_col_map[row[0]].append(row[1])
|
||||||
|
|
||||||
|
class AuthorsTable(ManyToManyTable):
|
||||||
|
|
||||||
|
def read_id_maps(self, db):
|
||||||
|
for row in db.conn.execute(
|
||||||
|
'SELECT id, name, sort FROM authors'):
|
||||||
|
self.id_map[row[0]] = row[1]
|
||||||
|
self.extra_map[row[0]] = (row[2] if row[2] else
|
||||||
|
author_to_author_sort(row[1]))
|
||||||
|
|
||||||
|
class FormatsTable(ManyToManyTable):
|
||||||
|
|
||||||
|
def read_id_maps(self, db):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_maps(self, db):
|
||||||
|
for row in db.conn.execute('SELECT book, format, name FROM data'):
|
||||||
|
if row[1] is not None:
|
||||||
|
if row[1] not in self.col_book_map:
|
||||||
|
self.col_book_map[row[1]] = []
|
||||||
|
self.col_book_map.append(row[0])
|
||||||
|
if row[0] not in self.book_col_map:
|
||||||
|
self.book_col_map[row[0]] = []
|
||||||
|
self.book_col_map[row[0]].append((row[1], row[2]))
|
||||||
|
|
||||||
|
class IdentifiersTable(ManyToManyTable):
|
||||||
|
|
||||||
|
def read_id_maps(self, db):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_maps(self, db):
|
||||||
|
for row in db.conn.execute('SELECT book, type, val FROM identifiers'):
|
||||||
|
if row[1] is not None and row[2] is not None:
|
||||||
|
if row[1] not in self.col_book_map:
|
||||||
|
self.col_book_map[row[1]] = []
|
||||||
|
self.col_book_map.append(row[0])
|
||||||
|
if row[0] not in self.book_col_map:
|
||||||
|
self.book_col_map[row[0]] = []
|
||||||
|
self.book_col_map[row[0]].append((row[1], row[2]))
|
||||||
|
|
@ -107,6 +107,7 @@ class DriverBase(DeviceConfig, DevicePlugin):
|
|||||||
# Needed for config_widget to work
|
# Needed for config_widget to work
|
||||||
FORMATS = ['epub', 'pdf']
|
FORMATS = ['epub', 'pdf']
|
||||||
USER_CAN_ADD_NEW_FORMATS = False
|
USER_CAN_ADD_NEW_FORMATS = False
|
||||||
|
KEEP_TEMP_FILES_AFTER_UPLOAD = True
|
||||||
|
|
||||||
# Hide the standard customization widgets
|
# Hide the standard customization widgets
|
||||||
SUPPORTS_SUB_DIRS = False
|
SUPPORTS_SUB_DIRS = False
|
||||||
|
@ -327,12 +327,7 @@ class DevicePlugin(Plugin):
|
|||||||
free space on the device. The text of the FreeSpaceError must contain the
|
free space on the device. The text of the FreeSpaceError must contain the
|
||||||
word "card" if ``on_card`` is not None otherwise it must contain the word "memory".
|
word "card" if ``on_card`` is not None otherwise it must contain the word "memory".
|
||||||
|
|
||||||
:param files: A list of paths and/or file-like objects. If they are paths and
|
:param files: A list of paths
|
||||||
the paths point to temporary files, they may have an additional
|
|
||||||
attribute, original_file_path pointing to the originals. They may have
|
|
||||||
another optional attribute, deleted_after_upload which if True means
|
|
||||||
that the file pointed to by original_file_path will be deleted after
|
|
||||||
being uploaded to the device.
|
|
||||||
:param names: A list of file names that the books should have
|
:param names: A list of file names that the books should have
|
||||||
once uploaded to the device. len(names) == len(files)
|
once uploaded to the device. len(names) == len(files)
|
||||||
:param metadata: If not None, it is a list of :class:`Metadata` objects.
|
:param metadata: If not None, it is a list of :class:`Metadata` objects.
|
||||||
|
@ -100,7 +100,7 @@ class KOBO(USBMS):
|
|||||||
for idx,b in enumerate(bl):
|
for idx,b in enumerate(bl):
|
||||||
bl_cache[b.lpath] = idx
|
bl_cache[b.lpath] = idx
|
||||||
|
|
||||||
def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType):
|
def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType, expired, favouritesindex):
|
||||||
changed = False
|
changed = False
|
||||||
try:
|
try:
|
||||||
lpath = path.partition(self.normalize_path(prefix))[2]
|
lpath = path.partition(self.normalize_path(prefix))[2]
|
||||||
@ -111,12 +111,23 @@ class KOBO(USBMS):
|
|||||||
|
|
||||||
playlist_map = {}
|
playlist_map = {}
|
||||||
|
|
||||||
|
if lpath not in playlist_map:
|
||||||
|
playlist_map[lpath] = []
|
||||||
|
|
||||||
if readstatus == 1:
|
if readstatus == 1:
|
||||||
playlist_map[lpath]= "Im_Reading"
|
playlist_map[lpath].append('Im_Reading')
|
||||||
elif readstatus == 2:
|
elif readstatus == 2:
|
||||||
playlist_map[lpath]= "Read"
|
playlist_map[lpath].append('Read')
|
||||||
elif readstatus == 3:
|
elif readstatus == 3:
|
||||||
playlist_map[lpath]= "Closed"
|
playlist_map[lpath].append('Closed')
|
||||||
|
|
||||||
|
# Related to a bug in the Kobo firmware that leaves an expired row for deleted books
|
||||||
|
# this shows an expired Collection so the user can decide to delete the book
|
||||||
|
if expired == 3:
|
||||||
|
playlist_map[lpath].append('Expired')
|
||||||
|
# Favourites are supported on the touch but the data field is there on most earlier models
|
||||||
|
if favouritesindex == 1:
|
||||||
|
playlist_map[lpath].append('Favourite')
|
||||||
|
|
||||||
path = self.normalize_path(path)
|
path = self.normalize_path(path)
|
||||||
# print "Normalized FileName: " + path
|
# print "Normalized FileName: " + path
|
||||||
@ -126,7 +137,13 @@ class KOBO(USBMS):
|
|||||||
bl_cache[lpath] = None
|
bl_cache[lpath] = None
|
||||||
if ImageID is not None:
|
if ImageID is not None:
|
||||||
imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed')
|
imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed')
|
||||||
|
if not os.path.exists(imagename):
|
||||||
|
# Try the Touch version if the image does not exist
|
||||||
|
imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - N3_LIBRARY_FULL.parsed')
|
||||||
|
|
||||||
#print "Image name Normalized: " + imagename
|
#print "Image name Normalized: " + imagename
|
||||||
|
if not os.path.exists(imagename):
|
||||||
|
debug_print("Strange - The image name does not exist - title: ", title)
|
||||||
if imagename is not None:
|
if imagename is not None:
|
||||||
bl[idx].thumbnail = ImageWrapper(imagename)
|
bl[idx].thumbnail = ImageWrapper(imagename)
|
||||||
if (ContentType != '6' and MimeType != 'Shortcover'):
|
if (ContentType != '6' and MimeType != 'Shortcover'):
|
||||||
@ -138,7 +155,7 @@ class KOBO(USBMS):
|
|||||||
debug_print(" Strange: The file: ", prefix, lpath, " does mot exist!")
|
debug_print(" Strange: The file: ", prefix, lpath, " does mot exist!")
|
||||||
if lpath in playlist_map and \
|
if lpath in playlist_map and \
|
||||||
playlist_map[lpath] not in bl[idx].device_collections:
|
playlist_map[lpath] not in bl[idx].device_collections:
|
||||||
bl[idx].device_collections.append(playlist_map[lpath])
|
bl[idx].device_collections = playlist_map.get(lpath,[])
|
||||||
else:
|
else:
|
||||||
if ContentType == '6' and MimeType == 'Shortcover':
|
if ContentType == '6' and MimeType == 'Shortcover':
|
||||||
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
|
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
|
||||||
@ -157,7 +174,7 @@ class KOBO(USBMS):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
# print 'Update booklist'
|
# print 'Update booklist'
|
||||||
book.device_collections = [playlist_map[lpath]] if lpath in playlist_map else []
|
book.device_collections = playlist_map.get(lpath,[])# if lpath in playlist_map else []
|
||||||
|
|
||||||
if bl.add_book(book, replace_metadata=False):
|
if bl.add_book(book, replace_metadata=False):
|
||||||
changed = True
|
changed = True
|
||||||
@ -186,24 +203,30 @@ class KOBO(USBMS):
|
|||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
self.dbversion = result[0]
|
self.dbversion = result[0]
|
||||||
|
|
||||||
|
if self.dbversion >= 14:
|
||||||
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
|
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
|
||||||
'ImageID, ReadStatus from content where BookID is Null'
|
'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex from content where BookID is Null'
|
||||||
|
else:
|
||||||
|
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
|
||||||
|
'ImageID, ReadStatus, ___ExpirationStatus, "-1" as FavouritesIndex from content where BookID is Null'
|
||||||
|
|
||||||
cursor.execute (query)
|
cursor.execute (query)
|
||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
for i, row in enumerate(cursor):
|
for i, row in enumerate(cursor):
|
||||||
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
|
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
|
||||||
|
if row[3].startswith("file:///usr/local/Kobo/help/"):
|
||||||
|
# These are internal to the Kobo device and do not exist
|
||||||
|
continue
|
||||||
path = self.path_from_contentid(row[3], row[5], row[4], oncard)
|
path = self.path_from_contentid(row[3], row[5], row[4], oncard)
|
||||||
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
|
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
|
||||||
# debug_print("mime:", mime)
|
# debug_print("mime:", mime)
|
||||||
|
|
||||||
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
|
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
|
||||||
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4])
|
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9])
|
||||||
# print "shortbook: " + path
|
# print "shortbook: " + path
|
||||||
elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"):
|
elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"):
|
||||||
changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4])
|
changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9])
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
need_sync = True
|
need_sync = True
|
||||||
@ -267,8 +290,12 @@ class KOBO(USBMS):
|
|||||||
cursor.execute('delete from content_keys where volumeid = ?', t)
|
cursor.execute('delete from content_keys where volumeid = ?', t)
|
||||||
|
|
||||||
# Delete the chapters associated with the book next
|
# Delete the chapters associated with the book next
|
||||||
t = (ContentID,ContentID,)
|
t = (ContentID,)
|
||||||
cursor.execute('delete from content where BookID = ? or ContentID = ?', t)
|
# Kobo does not delete the Book row (ie the row where the BookID is Null)
|
||||||
|
# The next server sync should remove the row
|
||||||
|
cursor.execute('delete from content where BookID = ?', t)
|
||||||
|
cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0, ___ExpirationStatus=3 ' \
|
||||||
|
'where BookID is Null and ContentID =?',t)
|
||||||
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
@ -286,7 +313,7 @@ class KOBO(USBMS):
|
|||||||
path_prefix = '.kobo/images/'
|
path_prefix = '.kobo/images/'
|
||||||
path = self._main_prefix + path_prefix + ImageID
|
path = self._main_prefix + path_prefix + ImageID
|
||||||
|
|
||||||
file_endings = (' - iPhoneThumbnail.parsed', ' - bbMediumGridList.parsed', ' - NickelBookCover.parsed',)
|
file_endings = (' - iPhoneThumbnail.parsed', ' - bbMediumGridList.parsed', ' - NickelBookCover.parsed', ' - N3_LIBRARY_FULL.parsed', ' - N3_LIBRARY_GRID.parsed', ' - N3_LIBRARY_LIST.parsed', ' - N3_SOCIAL_CURRENTREAD.parsed',)
|
||||||
|
|
||||||
for ending in file_endings:
|
for ending in file_endings:
|
||||||
fpath = path + ending
|
fpath = path + ending
|
||||||
@ -450,6 +477,9 @@ class KOBO(USBMS):
|
|||||||
path = self._main_prefix + path + '.kobo'
|
path = self._main_prefix + path + '.kobo'
|
||||||
# print "Path: " + path
|
# print "Path: " + path
|
||||||
elif (ContentType == "6" or ContentType == "10") and MimeType == 'application/x-kobo-epub+zip':
|
elif (ContentType == "6" or ContentType == "10") and MimeType == 'application/x-kobo-epub+zip':
|
||||||
|
if path.startswith("file:///mnt/onboard/"):
|
||||||
|
path = self._main_prefix + path.replace("file:///mnt/onboard/", '')
|
||||||
|
else:
|
||||||
path = self._main_prefix + '.kobo/kepub/' + path
|
path = self._main_prefix + '.kobo/kepub/' + path
|
||||||
# print "Internal: " + path
|
# print "Internal: " + path
|
||||||
else:
|
else:
|
||||||
|
@ -26,6 +26,10 @@ class Epubcheck(ePubFixer):
|
|||||||
'significant changes to your epub, complain to the epubcheck '
|
'significant changes to your epub, complain to the epubcheck '
|
||||||
'project.')
|
'project.')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return self.long_description
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fix_name(self):
|
def fix_name(self):
|
||||||
return 'epubcheck'
|
return 'epubcheck'
|
||||||
|
@ -22,6 +22,10 @@ class Unmanifested(ePubFixer):
|
|||||||
'the manifest or delete them as specified by the '
|
'the manifest or delete them as specified by the '
|
||||||
'delete unmanifested option.')
|
'delete unmanifested option.')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return self.long_description
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fix_name(self):
|
def fix_name(self):
|
||||||
return 'unmanifested'
|
return 'unmanifested'
|
||||||
|
@ -15,7 +15,7 @@ from calibre.utils.ipc.server import Server
|
|||||||
from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory
|
from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory
|
||||||
from calibre import prints, isbytestring
|
from calibre import prints, isbytestring
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding
|
||||||
|
from calibre.db.errors import NoSuchFormat
|
||||||
|
|
||||||
def debug(*args):
|
def debug(*args):
|
||||||
prints(*args)
|
prints(*args)
|
||||||
@ -201,24 +201,32 @@ class SaveWorker(Thread):
|
|||||||
self.spare_server = spare_server
|
self.spare_server = spare_server
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def collect_data(self, ids):
|
def collect_data(self, ids, tdir):
|
||||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||||
data = {}
|
data = {}
|
||||||
for i in set(ids):
|
for i in set(ids):
|
||||||
mi = self.db.get_metadata(i, index_is_id=True, get_cover=True)
|
mi = self.db.get_metadata(i, index_is_id=True, get_cover=True,
|
||||||
|
cover_as_data=True)
|
||||||
opf = metadata_to_opf(mi)
|
opf = metadata_to_opf(mi)
|
||||||
if isbytestring(opf):
|
if isbytestring(opf):
|
||||||
opf = opf.decode('utf-8')
|
opf = opf.decode('utf-8')
|
||||||
cpath = None
|
cpath = None
|
||||||
if mi.cover:
|
if mi.cover_data and mi.cover_data[1]:
|
||||||
cpath = mi.cover
|
cpath = os.path.join(tdir, 'cover_%s.jpg'%i)
|
||||||
|
with lopen(cpath, 'wb') as f:
|
||||||
|
f.write(mi.cover_data[1])
|
||||||
if isbytestring(cpath):
|
if isbytestring(cpath):
|
||||||
cpath = cpath.decode(filesystem_encoding)
|
cpath = cpath.decode(filesystem_encoding)
|
||||||
formats = {}
|
formats = {}
|
||||||
if mi.formats:
|
if mi.formats:
|
||||||
for fmt in mi.formats:
|
for fmt in mi.formats:
|
||||||
fpath = self.db.format_abspath(i, fmt, index_is_id=True)
|
fpath = os.path.join(tdir, 'fmt_%s.%s'%(i, fmt.lower()))
|
||||||
if fpath is not None:
|
with lopen(fpath, 'wb') as f:
|
||||||
|
try:
|
||||||
|
self.db.copy_format_to(i, fmt, f, index_is_id=True)
|
||||||
|
except NoSuchFormat:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
if isbytestring(fpath):
|
if isbytestring(fpath):
|
||||||
fpath = fpath.decode(filesystem_encoding)
|
fpath = fpath.decode(filesystem_encoding)
|
||||||
formats[fmt.lower()] = fpath
|
formats[fmt.lower()] = fpath
|
||||||
@ -244,7 +252,7 @@ class SaveWorker(Thread):
|
|||||||
|
|
||||||
for i, task in enumerate(tasks):
|
for i, task in enumerate(tasks):
|
||||||
tids = [x[-1] for x in task]
|
tids = [x[-1] for x in task]
|
||||||
data = self.collect_data(tids)
|
data = self.collect_data(tids, tdir)
|
||||||
dpath = os.path.join(tdir, '%d.json'%i)
|
dpath = os.path.join(tdir, '%d.json'%i)
|
||||||
with open(dpath, 'wb') as f:
|
with open(dpath, 'wb') as f:
|
||||||
f.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
f.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
||||||
|
@ -53,13 +53,18 @@ class Worker(Thread): # {{{
|
|||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
newdb = LibraryDatabase2(self.loc)
|
newdb = LibraryDatabase2(self.loc)
|
||||||
for i, x in enumerate(self.ids):
|
for i, x in enumerate(self.ids):
|
||||||
mi = self.db.get_metadata(x, index_is_id=True, get_cover=True)
|
mi = self.db.get_metadata(x, index_is_id=True, get_cover=True,
|
||||||
|
cover_as_data=True)
|
||||||
self.progress(i, mi.title)
|
self.progress(i, mi.title)
|
||||||
fmts = self.db.formats(x, index_is_id=True)
|
fmts = self.db.formats(x, index_is_id=True)
|
||||||
if not fmts: fmts = []
|
if not fmts: fmts = []
|
||||||
else: fmts = fmts.split(',')
|
else: fmts = fmts.split(',')
|
||||||
paths = [self.db.format_abspath(x, fmt, index_is_id=True) for fmt in
|
paths = []
|
||||||
fmts]
|
for fmt in fmts:
|
||||||
|
p = self.db.format(x, fmt, index_is_id=True,
|
||||||
|
as_path=True)
|
||||||
|
if p:
|
||||||
|
paths.append(p)
|
||||||
added = False
|
added = False
|
||||||
if prefs['add_formats_to_existing']:
|
if prefs['add_formats_to_existing']:
|
||||||
identical_book_list = newdb.find_identical_books(mi)
|
identical_book_list = newdb.find_identical_books(mi)
|
||||||
@ -75,6 +80,11 @@ class Worker(Thread): # {{{
|
|||||||
if co is not None:
|
if co is not None:
|
||||||
newdb.set_conversion_options(x, 'PIPE', co)
|
newdb.set_conversion_options(x, 'PIPE', co)
|
||||||
self.processed.add(x)
|
self.processed.add(x)
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class CopyToLibraryAction(InterfaceAction):
|
class CopyToLibraryAction(InterfaceAction):
|
||||||
|
@ -17,6 +17,7 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
|||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
|
from calibre.db.errors import NoSuchFormat
|
||||||
|
|
||||||
class EditMetadataAction(InterfaceAction):
|
class EditMetadataAction(InterfaceAction):
|
||||||
|
|
||||||
@ -265,7 +266,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
+'</p>', 'merge_too_many_books', self.gui):
|
+'</p>', 'merge_too_many_books', self.gui):
|
||||||
return
|
return
|
||||||
|
|
||||||
dest_id, src_books, src_ids = self.books_to_merge(rows)
|
dest_id, src_ids = self.books_to_merge(rows)
|
||||||
title = self.gui.library_view.model().db.title(dest_id, index_is_id=True)
|
title = self.gui.library_view.model().db.title(dest_id, index_is_id=True)
|
||||||
if safe_merge:
|
if safe_merge:
|
||||||
if not confirm('<p>'+_(
|
if not confirm('<p>'+_(
|
||||||
@ -277,7 +278,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
'Please confirm you want to proceed.')%title
|
'Please confirm you want to proceed.')%title
|
||||||
+'</p>', 'merge_books_safe', self.gui):
|
+'</p>', 'merge_books_safe', self.gui):
|
||||||
return
|
return
|
||||||
self.add_formats(dest_id, src_books)
|
self.add_formats(dest_id, self.formats_for_books(rows))
|
||||||
self.merge_metadata(dest_id, src_ids)
|
self.merge_metadata(dest_id, src_ids)
|
||||||
elif merge_only_formats:
|
elif merge_only_formats:
|
||||||
if not confirm('<p>'+_(
|
if not confirm('<p>'+_(
|
||||||
@ -293,7 +294,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
'Are you <b>sure</b> you want to proceed?')%title
|
'Are you <b>sure</b> you want to proceed?')%title
|
||||||
+'</p>', 'merge_only_formats', self.gui):
|
+'</p>', 'merge_only_formats', self.gui):
|
||||||
return
|
return
|
||||||
self.add_formats(dest_id, src_books)
|
self.add_formats(dest_id, self.formats_for_books(rows))
|
||||||
self.delete_books_after_merge(src_ids)
|
self.delete_books_after_merge(src_ids)
|
||||||
else:
|
else:
|
||||||
if not confirm('<p>'+_(
|
if not confirm('<p>'+_(
|
||||||
@ -308,7 +309,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
'Are you <b>sure</b> you want to proceed?')%title
|
'Are you <b>sure</b> you want to proceed?')%title
|
||||||
+'</p>', 'merge_books', self.gui):
|
+'</p>', 'merge_books', self.gui):
|
||||||
return
|
return
|
||||||
self.add_formats(dest_id, src_books)
|
self.add_formats(dest_id, self.formats_for_books(rows))
|
||||||
self.merge_metadata(dest_id, src_ids)
|
self.merge_metadata(dest_id, src_ids)
|
||||||
self.delete_books_after_merge(src_ids)
|
self.delete_books_after_merge(src_ids)
|
||||||
# leave the selection highlight on first selected book
|
# leave the selection highlight on first selected book
|
||||||
@ -329,8 +330,22 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
self.gui.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True,
|
self.gui.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True,
|
||||||
notify=False, replace=replace)
|
notify=False, replace=replace)
|
||||||
|
|
||||||
|
def formats_for_books(self, rows):
|
||||||
|
m = self.gui.library_view.model()
|
||||||
|
ans = []
|
||||||
|
for id_ in map(m.id, rows):
|
||||||
|
dbfmts = m.db.formats(id_, index_is_id=True)
|
||||||
|
if dbfmts:
|
||||||
|
for fmt in dbfmts.split(','):
|
||||||
|
try:
|
||||||
|
path = m.db.format(id_, fmt, index_is_id=True,
|
||||||
|
as_path=True)
|
||||||
|
ans.append(path)
|
||||||
|
except NoSuchFormat:
|
||||||
|
continue
|
||||||
|
return ans
|
||||||
|
|
||||||
def books_to_merge(self, rows):
|
def books_to_merge(self, rows):
|
||||||
src_books = []
|
|
||||||
src_ids = []
|
src_ids = []
|
||||||
m = self.gui.library_view.model()
|
m = self.gui.library_view.model()
|
||||||
for i, row in enumerate(rows):
|
for i, row in enumerate(rows):
|
||||||
@ -339,22 +354,19 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
dest_id = id_
|
dest_id = id_
|
||||||
else:
|
else:
|
||||||
src_ids.append(id_)
|
src_ids.append(id_)
|
||||||
dbfmts = m.db.formats(id_, index_is_id=True)
|
return [dest_id, src_ids]
|
||||||
if dbfmts:
|
|
||||||
for fmt in dbfmts.split(','):
|
|
||||||
src_books.append(m.db.format_abspath(id_, fmt,
|
|
||||||
index_is_id=True))
|
|
||||||
return [dest_id, src_books, src_ids]
|
|
||||||
|
|
||||||
def delete_books_after_merge(self, ids_to_delete):
|
def delete_books_after_merge(self, ids_to_delete):
|
||||||
self.gui.library_view.model().delete_books_by_id(ids_to_delete)
|
self.gui.library_view.model().delete_books_by_id(ids_to_delete)
|
||||||
|
|
||||||
def merge_metadata(self, dest_id, src_ids):
|
def merge_metadata(self, dest_id, src_ids):
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
dest_mi = db.get_metadata(dest_id, index_is_id=True, get_cover=True)
|
dest_mi = db.get_metadata(dest_id, index_is_id=True)
|
||||||
orig_dest_comments = dest_mi.comments
|
orig_dest_comments = dest_mi.comments
|
||||||
|
dest_cover = db.cover(dest_id, index_is_id=True)
|
||||||
|
had_orig_cover = bool(dest_cover)
|
||||||
for src_id in src_ids:
|
for src_id in src_ids:
|
||||||
src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True)
|
src_mi = db.get_metadata(src_id, index_is_id=True)
|
||||||
if src_mi.comments and orig_dest_comments != src_mi.comments:
|
if src_mi.comments and orig_dest_comments != src_mi.comments:
|
||||||
if not dest_mi.comments:
|
if not dest_mi.comments:
|
||||||
dest_mi.comments = src_mi.comments
|
dest_mi.comments = src_mi.comments
|
||||||
@ -372,8 +384,10 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
dest_mi.tags = src_mi.tags
|
dest_mi.tags = src_mi.tags
|
||||||
else:
|
else:
|
||||||
dest_mi.tags.extend(src_mi.tags)
|
dest_mi.tags.extend(src_mi.tags)
|
||||||
if src_mi.cover and not dest_mi.cover:
|
if not dest_cover:
|
||||||
dest_mi.cover = src_mi.cover
|
src_cover = db.cover(src_id, index_is_id=True)
|
||||||
|
if src_cover:
|
||||||
|
dest_cover = src_cover
|
||||||
if not dest_mi.publisher:
|
if not dest_mi.publisher:
|
||||||
dest_mi.publisher = src_mi.publisher
|
dest_mi.publisher = src_mi.publisher
|
||||||
if not dest_mi.rating:
|
if not dest_mi.rating:
|
||||||
@ -382,6 +396,8 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
dest_mi.series = src_mi.series
|
dest_mi.series = src_mi.series
|
||||||
dest_mi.series_index = src_mi.series_index
|
dest_mi.series_index = src_mi.series_index
|
||||||
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
|
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
|
||||||
|
if not had_orig_cover and dest_cover:
|
||||||
|
db.set_cover(dest_id, dest_cover)
|
||||||
|
|
||||||
for key in db.field_metadata: #loop thru all defined fields
|
for key in db.field_metadata: #loop thru all defined fields
|
||||||
if db.field_metadata[key]['is_custom']:
|
if db.field_metadata[key]['is_custom']:
|
||||||
|
41
src/calibre/gui2/actions/show_quickview.py
Normal file
41
src/calibre/gui2/actions/show_quickview.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.gui2.dialogs.quickview import Quickview
|
||||||
|
from calibre.gui2 import error_dialog
|
||||||
|
|
||||||
|
class ShowQuickviewAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Show quickview'
|
||||||
|
action_spec = (_('Show quickview'), 'user_profile.png', None,
|
||||||
|
_('Q'))
|
||||||
|
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
|
current_instance = None
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.triggered.connect(self.show_quickview)
|
||||||
|
|
||||||
|
def show_quickview(self, *args):
|
||||||
|
if self.current_instance:
|
||||||
|
if not self.current_instance.is_closed:
|
||||||
|
return
|
||||||
|
self.current_instance = None
|
||||||
|
if self.gui.current_view() is not self.gui.library_view:
|
||||||
|
error_dialog(self.gui, _('No quickview available'),
|
||||||
|
_('Quickview is not available for books '
|
||||||
|
'on the device.')).exec_()
|
||||||
|
return
|
||||||
|
index = self.gui.library_view.currentIndex()
|
||||||
|
if index.isValid():
|
||||||
|
self.current_instance = \
|
||||||
|
Quickview(self.gui, self.gui.library_view, index)
|
||||||
|
self.current_instance.show()
|
||||||
|
|
@ -5,6 +5,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
from calibre.gui2.dialogs.tweak_epub import TweakEpub
|
from calibre.gui2.dialogs.tweak_epub import TweakEpub
|
||||||
@ -30,8 +32,8 @@ class TweakEpubAction(InterfaceAction):
|
|||||||
# Confirm 'EPUB' in formats
|
# Confirm 'EPUB' in formats
|
||||||
book_id = self.gui.library_view.model().id(row)
|
book_id = self.gui.library_view.model().id(row)
|
||||||
try:
|
try:
|
||||||
path_to_epub = self.gui.library_view.model().db.format_abspath(
|
path_to_epub = self.gui.library_view.model().db.format(
|
||||||
book_id, 'EPUB', index_is_id=True)
|
book_id, 'EPUB', index_is_id=True, as_path=True)
|
||||||
except:
|
except:
|
||||||
path_to_epub = None
|
path_to_epub = None
|
||||||
|
|
||||||
@ -45,6 +47,7 @@ class TweakEpubAction(InterfaceAction):
|
|||||||
if dlg.exec_() == dlg.Accepted:
|
if dlg.exec_() == dlg.Accepted:
|
||||||
self.update_db(book_id, dlg._output)
|
self.update_db(book_id, dlg._output)
|
||||||
dlg.cleanup()
|
dlg.cleanup()
|
||||||
|
os.remove(path_to_epub)
|
||||||
|
|
||||||
def update_db(self, book_id, rebuilt):
|
def update_db(self, book_id, rebuilt):
|
||||||
'''
|
'''
|
||||||
|
@ -445,6 +445,7 @@ class Saver(QObject): # {{{
|
|||||||
self.pd.setModal(True)
|
self.pd.setModal(True)
|
||||||
self.pd.show()
|
self.pd.show()
|
||||||
self.pd.set_min(0)
|
self.pd.set_min(0)
|
||||||
|
self.pd.set_msg(_('Collecting data, please wait...'))
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.callback_called = False
|
self.callback_called = False
|
||||||
|
@ -4,7 +4,7 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re
|
import re, os
|
||||||
|
|
||||||
from PyQt4.QtCore import SIGNAL, Qt, pyqtSignal
|
from PyQt4.QtCore import SIGNAL, Qt, pyqtSignal
|
||||||
from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, \
|
from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, \
|
||||||
@ -134,7 +134,12 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
_('Cannot build regex using the GUI builder without a book.'),
|
_('Cannot build regex using the GUI builder without a book.'),
|
||||||
show=True)
|
show=True)
|
||||||
return False
|
return False
|
||||||
self.open_book(db.format_abspath(book_id, format, index_is_id=True))
|
fpath = db.format(book_id, format, index_is_id=True,
|
||||||
|
as_path=True)
|
||||||
|
try:
|
||||||
|
self.open_book(fpath)
|
||||||
|
finally:
|
||||||
|
os.remove(fpath)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def open_book(self, pathtoebook):
|
def open_book(self, pathtoebook):
|
||||||
|
@ -106,7 +106,6 @@ class Config(ResizableDialog, Ui_Dialog):
|
|||||||
Configuration dialog for single book conversion. If accepted, has the
|
Configuration dialog for single book conversion. If accepted, has the
|
||||||
following important attributes
|
following important attributes
|
||||||
|
|
||||||
input_path - Path to input file
|
|
||||||
output_format - Output format (without a leading .)
|
output_format - Output format (without a leading .)
|
||||||
input_format - Input format (without a leading .)
|
input_format - Input format (without a leading .)
|
||||||
opf_path - Path to OPF file with user specified metadata
|
opf_path - Path to OPF file with user specified metadata
|
||||||
@ -156,13 +155,10 @@ class Config(ResizableDialog, Ui_Dialog):
|
|||||||
oidx = self.groups.currentIndex().row()
|
oidx = self.groups.currentIndex().row()
|
||||||
input_format = self.input_format
|
input_format = self.input_format
|
||||||
output_format = self.output_format
|
output_format = self.output_format
|
||||||
input_path = self.db.format_abspath(self.book_id, input_format,
|
|
||||||
index_is_id=True)
|
|
||||||
self.input_path = input_path
|
|
||||||
output_path = 'dummy.'+output_format
|
output_path = 'dummy.'+output_format
|
||||||
log = Log()
|
log = Log()
|
||||||
log.outputs = []
|
log.outputs = []
|
||||||
self.plumber = Plumber(input_path, output_path, log)
|
self.plumber = Plumber('dummy.'+input_format, output_path, log)
|
||||||
|
|
||||||
def widget_factory(cls):
|
def widget_factory(cls):
|
||||||
return cls(self.stack, self.plumber.get_option_by_name,
|
return cls(self.stack, self.plumber.get_option_by_name,
|
||||||
|
@ -49,16 +49,26 @@ class DeviceJob(BaseJob): # {{{
|
|||||||
self._aborted = False
|
self._aborted = False
|
||||||
|
|
||||||
def start_work(self):
|
def start_work(self):
|
||||||
|
if DEBUG:
|
||||||
|
prints('Job:', self.id, self.description, 'started',
|
||||||
|
safe_encode=True)
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
self.job_manager.changed_queue.put(self)
|
self.job_manager.changed_queue.put(self)
|
||||||
|
|
||||||
def job_done(self):
|
def job_done(self):
|
||||||
self.duration = time.time() - self.start_time
|
self.duration = time.time() - self.start_time
|
||||||
self.percent = 1
|
self.percent = 1
|
||||||
|
if DEBUG:
|
||||||
|
prints('DeviceJob:', self.id, self.description,
|
||||||
|
'done, calling callback', safe_encode=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.callback_on_done(self)
|
self.callback_on_done(self)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
if DEBUG:
|
||||||
|
prints('DeviceJob:', self.id, self.description,
|
||||||
|
'callback returned', safe_encode=True)
|
||||||
self.job_manager.changed_queue.put(self)
|
self.job_manager.changed_queue.put(self)
|
||||||
|
|
||||||
def report_progress(self, percent, msg=''):
|
def report_progress(self, percent, msg=''):
|
||||||
@ -386,8 +396,17 @@ class DeviceManager(Thread): # {{{
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints(traceback.format_exc(), file=sys.__stdout__)
|
prints(traceback.format_exc(), file=sys.__stdout__)
|
||||||
|
|
||||||
|
try:
|
||||||
return self.device.upload_books(files, names, on_card,
|
return self.device.upload_books(files, names, on_card,
|
||||||
metadata=metadata, end_session=False)
|
metadata=metadata, end_session=False)
|
||||||
|
finally:
|
||||||
|
if metadata:
|
||||||
|
for mi in metadata:
|
||||||
|
try:
|
||||||
|
if mi.cover:
|
||||||
|
os.remove(mi.cover)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def upload_books(self, done, files, names, on_card=None, titles=None,
|
def upload_books(self, done, files, names, on_card=None, titles=None,
|
||||||
metadata=None, plugboards=None, add_as_step_to_job=None):
|
metadata=None, plugboards=None, add_as_step_to_job=None):
|
||||||
@ -1062,8 +1081,6 @@ class DeviceMixin(object): # {{{
|
|||||||
'the device?'), autos):
|
'the device?'), autos):
|
||||||
self.iactions['Convert Books'].auto_convert_news(auto, format)
|
self.iactions['Convert Books'].auto_convert_news(auto, format)
|
||||||
files = [f for f in files if f is not None]
|
files = [f for f in files if f is not None]
|
||||||
for f in files:
|
|
||||||
f.deleted_after_upload = del_on_upload
|
|
||||||
if not files:
|
if not files:
|
||||||
self.news_to_be_synced = set([])
|
self.news_to_be_synced = set([])
|
||||||
return
|
return
|
||||||
@ -1305,8 +1322,17 @@ class DeviceMixin(object): # {{{
|
|||||||
self.card_b_view if on_card == 'cardb' else self.memory_view
|
self.card_b_view if on_card == 'cardb' else self.memory_view
|
||||||
view.model().resort(reset=False)
|
view.model().resort(reset=False)
|
||||||
view.model().research()
|
view.model().research()
|
||||||
|
if files:
|
||||||
for f in files:
|
for f in files:
|
||||||
getattr(f, 'close', lambda : True)()
|
# Remove temporary files
|
||||||
|
try:
|
||||||
|
rem = not getattr(
|
||||||
|
self.device_manager.device,
|
||||||
|
'KEEP_TEMP_FILES_AFTER_UPLOAD', False)
|
||||||
|
if rem and 'caltmpfmt.' in f:
|
||||||
|
os.remove(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def book_on_device(self, id, reset=False):
|
def book_on_device(self, id, reset=False):
|
||||||
'''
|
'''
|
||||||
|
@ -24,7 +24,7 @@ from calibre.utils.config import prefs, tweaks
|
|||||||
from calibre.utils.magick.draw import identify_data
|
from calibre.utils.magick.draw import identify_data
|
||||||
from calibre.utils.date import qt_to_dt
|
from calibre.utils.date import qt_to_dt
|
||||||
|
|
||||||
def get_cover_data(path): # {{{
|
def get_cover_data(stream, ext): # {{{
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
old = prefs['read_file_metadata']
|
old = prefs['read_file_metadata']
|
||||||
if not old:
|
if not old:
|
||||||
@ -32,8 +32,8 @@ def get_cover_data(path): # {{{
|
|||||||
cdata = area = None
|
cdata = area = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mi = get_metadata(open(path, 'rb'),
|
with stream:
|
||||||
os.path.splitext(path)[1][1:].lower())
|
mi = get_metadata(stream, ext)
|
||||||
if mi.cover and os.access(mi.cover, os.R_OK):
|
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||||
cdata = open(mi.cover).read()
|
cdata = open(mi.cover).read()
|
||||||
elif mi.cover_data[1] is not None:
|
elif mi.cover_data[1] is not None:
|
||||||
@ -186,9 +186,10 @@ class MyBlockingBusy(QDialog): # {{{
|
|||||||
if fmts:
|
if fmts:
|
||||||
covers = []
|
covers = []
|
||||||
for fmt in fmts.split(','):
|
for fmt in fmts.split(','):
|
||||||
fmt = self.db.format_abspath(id, fmt, index_is_id=True)
|
fmtf = self.db.format(id, fmt, index_is_id=True,
|
||||||
if not fmt: continue
|
as_file=True)
|
||||||
cdata, area = get_cover_data(fmt)
|
if fmtf is None: continue
|
||||||
|
cdata, area = get_cover_data(fmtf, fmt)
|
||||||
if cdata:
|
if cdata:
|
||||||
covers.append((cdata, area))
|
covers.append((cdata, area))
|
||||||
covers.sort(key=lambda x: x[1])
|
covers.sort(key=lambda x: x[1])
|
||||||
@ -361,7 +362,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
fm = self.db.field_metadata
|
fm = self.db.field_metadata
|
||||||
for f in fm:
|
for f in fm:
|
||||||
if (f in ['author_sort'] or
|
if (f in ['author_sort'] or
|
||||||
(fm[f]['datatype'] in ['text', 'series', 'enumeration']
|
(fm[f]['datatype'] in ['text', 'series', 'enumeration', 'comments']
|
||||||
and fm[f].get('search_terms', None)
|
and fm[f].get('search_terms', None)
|
||||||
and f not in ['formats', 'ondevice']) or
|
and f not in ['formats', 'ondevice']) or
|
||||||
(fm[f]['datatype'] in ['int', 'float', 'bool'] and
|
(fm[f]['datatype'] in ['int', 'float', 'bool'] and
|
||||||
|
171
src/calibre/gui2/dialogs/quickview.py
Normal file
171
src/calibre/gui2/dialogs/quickview.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt4.Qt import (Qt, QDialog, QAbstractItemView, QTableWidgetItem,
|
||||||
|
QListWidgetItem, QByteArray, QModelIndex)
|
||||||
|
|
||||||
|
from calibre.gui2.dialogs.quickview_ui import Ui_Quickview
|
||||||
|
from calibre.utils.icu import sort_key
|
||||||
|
from calibre.gui2 import gprefs
|
||||||
|
|
||||||
|
class tableItem(QTableWidgetItem):
|
||||||
|
|
||||||
|
def __init__(self, val):
|
||||||
|
QTableWidgetItem.__init__(self, val)
|
||||||
|
self.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable)
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return sort_key(unicode(self.text())) >= sort_key(unicode(other.text()))
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return sort_key(unicode(self.text())) < sort_key(unicode(other.text()))
|
||||||
|
|
||||||
|
class Quickview(QDialog, Ui_Quickview):
|
||||||
|
|
||||||
|
def __init__(self, gui, view, row):
|
||||||
|
QDialog.__init__(self, gui, flags=Qt.Window)
|
||||||
|
Ui_Quickview.__init__(self)
|
||||||
|
self.setupUi(self)
|
||||||
|
self.isClosed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
geom = gprefs.get('quickview_dialog_geometry', bytearray(''))
|
||||||
|
self.restoreGeometry(QByteArray(geom))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
icon = self.windowIcon()
|
||||||
|
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
|
||||||
|
self.setWindowIcon(icon)
|
||||||
|
|
||||||
|
self.db = view.model().db
|
||||||
|
self.view = view
|
||||||
|
self.gui = gui
|
||||||
|
|
||||||
|
self.items.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
|
self.items.currentTextChanged.connect(self.item_selected)
|
||||||
|
# self.items.setFixedWidth(150)
|
||||||
|
|
||||||
|
self.books_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
self.books_table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
|
self.books_table.setColumnCount(3)
|
||||||
|
t = QTableWidgetItem(_('Title'))
|
||||||
|
self.books_table.setHorizontalHeaderItem(0, t)
|
||||||
|
t = QTableWidgetItem(_('Authors'))
|
||||||
|
self.books_table.setHorizontalHeaderItem(1, t)
|
||||||
|
t = QTableWidgetItem(_('Series'))
|
||||||
|
self.books_table.setHorizontalHeaderItem(2, t)
|
||||||
|
self.books_table_header_height = self.books_table.height()
|
||||||
|
self.books_table.cellDoubleClicked.connect(self.book_doubleclicked)
|
||||||
|
|
||||||
|
self.is_closed = False
|
||||||
|
self.current_book_id = None
|
||||||
|
self.current_key = None
|
||||||
|
self.use_current_key_for_next_refresh = False
|
||||||
|
self.last_search = None
|
||||||
|
|
||||||
|
self.refresh(row)
|
||||||
|
# self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave)
|
||||||
|
self.view.selectionModel().currentChanged[QModelIndex,QModelIndex].connect(self.slave)
|
||||||
|
self.search_button.clicked.connect(self.do_search)
|
||||||
|
|
||||||
|
def do_search(self):
|
||||||
|
if self.last_search is not None:
|
||||||
|
self.use_current_key_for_next_refresh = True
|
||||||
|
self.gui.search.set_search_string(self.last_search)
|
||||||
|
|
||||||
|
def item_selected(self, txt):
|
||||||
|
self.fill_in_books_box(unicode(txt))
|
||||||
|
|
||||||
|
def refresh(self, idx):
|
||||||
|
bv_row = idx.row()
|
||||||
|
key = self.view.model().column_map[idx.column()]
|
||||||
|
|
||||||
|
book_id = self.view.model().id(bv_row)
|
||||||
|
|
||||||
|
if self.use_current_key_for_next_refresh:
|
||||||
|
key = self.current_key
|
||||||
|
self.use_current_key_for_next_refresh = False
|
||||||
|
else:
|
||||||
|
if not self.db.field_metadata[key]['is_category']:
|
||||||
|
if self.current_key is None:
|
||||||
|
return
|
||||||
|
key = self.current_key
|
||||||
|
self.items_label.setText('{0} ({1})'.format(
|
||||||
|
self.db.field_metadata[key]['name'], key))
|
||||||
|
self.items.clear()
|
||||||
|
self.books_table.setRowCount(0)
|
||||||
|
|
||||||
|
mi = self.db.get_metadata(book_id, index_is_id=True, get_user_categories=False)
|
||||||
|
vals = mi.get(key, None)
|
||||||
|
if not vals:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(vals, list):
|
||||||
|
vals = [vals]
|
||||||
|
vals.sort(key=sort_key)
|
||||||
|
|
||||||
|
self.items.blockSignals(True)
|
||||||
|
for v in vals:
|
||||||
|
a = QListWidgetItem(v)
|
||||||
|
self.items.addItem(a)
|
||||||
|
self.items.setCurrentRow(0)
|
||||||
|
self.items.blockSignals(False)
|
||||||
|
|
||||||
|
self.current_book_id = book_id
|
||||||
|
self.current_key = key
|
||||||
|
|
||||||
|
self.fill_in_books_box(vals[0])
|
||||||
|
|
||||||
|
def fill_in_books_box(self, selected_item):
|
||||||
|
if selected_item.startswith('.'):
|
||||||
|
sv = '.' + selected_item
|
||||||
|
else:
|
||||||
|
sv = selected_item
|
||||||
|
sv = sv.replace('"', r'\"')
|
||||||
|
self.last_search = self.current_key+':"=' + sv + '"'
|
||||||
|
books = self.db.search_getting_ids(self.last_search,
|
||||||
|
self.db.data.search_restriction)
|
||||||
|
self.books_table.setRowCount(len(books))
|
||||||
|
self.books_label.setText(_('Books with selected item: {0}').format(len(books)))
|
||||||
|
|
||||||
|
select_row = None
|
||||||
|
self.books_table.setSortingEnabled(False)
|
||||||
|
for row, b in enumerate(books):
|
||||||
|
mi = self.db.get_metadata(b, index_is_id=True, get_user_categories=False)
|
||||||
|
a = tableItem(mi.title)
|
||||||
|
a.setData(Qt.UserRole, b)
|
||||||
|
self.books_table.setItem(row, 0, a)
|
||||||
|
a = tableItem(' & '.join(mi.authors))
|
||||||
|
self.books_table.setItem(row, 1, a)
|
||||||
|
series = mi.format_field('series')[1]
|
||||||
|
if series is None:
|
||||||
|
series = ''
|
||||||
|
a = tableItem(series)
|
||||||
|
self.books_table.setItem(row, 2, a)
|
||||||
|
if b == self.current_book_id:
|
||||||
|
select_row = row
|
||||||
|
|
||||||
|
self.books_table.resizeColumnsToContents()
|
||||||
|
# self.books_table.resizeRowsToContents()
|
||||||
|
|
||||||
|
if select_row is not None:
|
||||||
|
self.books_table.selectRow(select_row)
|
||||||
|
self.books_table.setSortingEnabled(True)
|
||||||
|
|
||||||
|
def book_doubleclicked(self, row, column):
|
||||||
|
self.use_current_key_for_next_refresh = True
|
||||||
|
self.view.select_rows([self.books_table.item(row, 0).data(Qt.UserRole).toInt()[0]])
|
||||||
|
|
||||||
|
def slave(self, current, previous):
|
||||||
|
self.refresh(current)
|
||||||
|
self.view.activateWindow()
|
||||||
|
|
||||||
|
def done(self, r):
|
||||||
|
geom = bytearray(self.saveGeometry())
|
||||||
|
gprefs['quickview_dialog_geometry'] = geom
|
||||||
|
self.is_closed = True
|
||||||
|
QDialog.done(self, r)
|
131
src/calibre/gui2/dialogs/quickview.ui
Normal file
131
src/calibre/gui2/dialogs/quickview.ui
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Quickview</class>
|
||||||
|
<widget class="QDialog" name="Quickview">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>768</width>
|
||||||
|
<height>342</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Quickview</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="items_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Items</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QListWidget" name="items">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding" >
|
||||||
|
<horstretch>1</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLabel" name="books_label">
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QTableWidget" name="books_table">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding" >
|
||||||
|
<horstretch>4</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="columnCount">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rowCount">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<spacer>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="2">
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="search_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>Search</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Search in the library view for the selected item</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Close</set>
|
||||||
|
</property>
|
||||||
|
<property name="centerButtons">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>Quickview</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>297</x>
|
||||||
|
<y>217</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>234</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -174,7 +174,8 @@ class EmailMixin(object): # {{{
|
|||||||
else:
|
else:
|
||||||
_auto_ids = []
|
_auto_ids = []
|
||||||
|
|
||||||
full_metadata = self.library_view.model().metadata_for(ids)
|
full_metadata = self.library_view.model().metadata_for(ids,
|
||||||
|
get_cover=False)
|
||||||
|
|
||||||
bad, remove_ids, jobnames = [], [], []
|
bad, remove_ids, jobnames = [], [], []
|
||||||
texts, subjects, attachments, attachment_names = [], [], [], []
|
texts, subjects, attachments, attachment_names = [], [], [], []
|
||||||
|
@ -5,8 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import shutil, functools, re, os, traceback
|
import functools, re, os, traceback
|
||||||
from contextlib import closing
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage,
|
from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage,
|
||||||
@ -36,14 +35,6 @@ TIME_FMT = '%d %b %Y'
|
|||||||
ALIGNMENT_MAP = {'left': Qt.AlignLeft, 'right': Qt.AlignRight, 'center':
|
ALIGNMENT_MAP = {'left': Qt.AlignLeft, 'right': Qt.AlignRight, 'center':
|
||||||
Qt.AlignHCenter}
|
Qt.AlignHCenter}
|
||||||
|
|
||||||
class FormatPath(unicode):
|
|
||||||
|
|
||||||
def __new__(cls, path, orig_file_path):
|
|
||||||
ans = unicode.__new__(cls, path)
|
|
||||||
ans.orig_file_path = orig_file_path
|
|
||||||
ans.deleted_after_upload = False
|
|
||||||
return ans
|
|
||||||
|
|
||||||
_default_image = None
|
_default_image = None
|
||||||
|
|
||||||
def default_image():
|
def default_image():
|
||||||
@ -391,10 +382,14 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
data = self.current_changed(index, None, False)
|
data = self.current_changed(index, None, False)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def metadata_for(self, ids):
|
def metadata_for(self, ids, get_cover=True):
|
||||||
|
'''
|
||||||
|
WARNING: if get_cover=True temp files are created for mi.cover.
|
||||||
|
Remember to delete them once you are done with them.
|
||||||
|
'''
|
||||||
ans = []
|
ans = []
|
||||||
for id in ids:
|
for id in ids:
|
||||||
mi = self.db.get_metadata(id, index_is_id=True, get_cover=True)
|
mi = self.db.get_metadata(id, index_is_id=True, get_cover=get_cover)
|
||||||
ans.append(mi)
|
ans.append(mi)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
@ -449,18 +444,14 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
format = f
|
format = f
|
||||||
break
|
break
|
||||||
if format is not None:
|
if format is not None:
|
||||||
pt = PersistentTemporaryFile(suffix='.'+format)
|
pt = PersistentTemporaryFile(suffix='caltmpfmt.'+format)
|
||||||
with closing(self.db.format(id, format, index_is_id=True,
|
self.db.copy_format_to(id, format, pt, index_is_id=True)
|
||||||
as_file=True)) as src:
|
|
||||||
shutil.copyfileobj(src, pt)
|
|
||||||
pt.flush()
|
|
||||||
if getattr(src, 'name', None):
|
|
||||||
pt.orig_file_path = os.path.abspath(src.name)
|
|
||||||
pt.seek(0)
|
pt.seek(0)
|
||||||
if set_metadata:
|
if set_metadata:
|
||||||
try:
|
try:
|
||||||
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
|
_set_metadata(pt, self.db.get_metadata(
|
||||||
format)
|
id, get_cover=True, index_is_id=True,
|
||||||
|
cover_as_data=True), format)
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
pt.close()
|
pt.close()
|
||||||
@ -468,9 +459,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
if isbytestring(x):
|
if isbytestring(x):
|
||||||
x = x.decode(filesystem_encoding)
|
x = x.decode(filesystem_encoding)
|
||||||
return x
|
return x
|
||||||
name, op = map(to_uni, map(os.path.abspath, (pt.name,
|
ans.append(to_uni(os.path.abspath(pt.name)))
|
||||||
pt.orig_file_path)))
|
|
||||||
ans.append(FormatPath(name, op))
|
|
||||||
else:
|
else:
|
||||||
need_auto.append(id)
|
need_auto.append(id)
|
||||||
if not exclude_auto:
|
if not exclude_auto:
|
||||||
@ -499,13 +488,11 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
break
|
break
|
||||||
if format is not None:
|
if format is not None:
|
||||||
pt = PersistentTemporaryFile(suffix='.'+format)
|
pt = PersistentTemporaryFile(suffix='.'+format)
|
||||||
with closing(self.db.format(row, format, as_file=True)) as src:
|
self.db.copy_format_to(id, format, pt, index_is_id=True)
|
||||||
shutil.copyfileobj(src, pt)
|
|
||||||
pt.flush()
|
|
||||||
pt.seek(0)
|
pt.seek(0)
|
||||||
if set_metadata:
|
if set_metadata:
|
||||||
_set_metadata(pt, self.db.get_metadata(row, get_cover=True),
|
_set_metadata(pt, self.db.get_metadata(row, get_cover=True,
|
||||||
format)
|
cover_as_data=True), format)
|
||||||
pt.close() if paths else pt.seek(0)
|
pt.close() if paths else pt.seek(0)
|
||||||
ans.append(pt)
|
ans.append(pt)
|
||||||
else:
|
else:
|
||||||
|
@ -584,14 +584,15 @@ class BooksView(QTableView): # {{{
|
|||||||
m = self.model()
|
m = self.model()
|
||||||
db = m.db
|
db = m.db
|
||||||
rows = self.selectionModel().selectedRows()
|
rows = self.selectionModel().selectedRows()
|
||||||
selected = map(m.id, rows)
|
selected = list(map(m.id, rows))
|
||||||
ids = ' '.join(map(str, selected))
|
ids = ' '.join(map(str, selected))
|
||||||
md = QMimeData()
|
md = QMimeData()
|
||||||
md.setData('application/calibre+from_library', ids)
|
md.setData('application/calibre+from_library', ids)
|
||||||
fmt = prefs['output_format']
|
fmt = prefs['output_format']
|
||||||
|
|
||||||
def url_for_id(i):
|
def url_for_id(i):
|
||||||
ans = db.format_abspath(i, fmt, index_is_id=True)
|
ans = db.format(i, fmt, index_is_id=True, as_path=True,
|
||||||
|
preserve_filename=True)
|
||||||
if ans is None:
|
if ans is None:
|
||||||
fmts = db.formats(i, index_is_id=True)
|
fmts = db.formats(i, index_is_id=True)
|
||||||
if fmts:
|
if fmts:
|
||||||
@ -599,14 +600,13 @@ class BooksView(QTableView): # {{{
|
|||||||
else:
|
else:
|
||||||
fmts = []
|
fmts = []
|
||||||
for f in fmts:
|
for f in fmts:
|
||||||
ans = db.format_abspath(i, f, index_is_id=True)
|
ans = db.format(i, f, index_is_id=True, as_path=True,
|
||||||
if ans is not None:
|
preserve_filename=True)
|
||||||
break
|
|
||||||
if ans is None:
|
if ans is None:
|
||||||
ans = db.abspath(i, index_is_id=True)
|
ans = db.abspath(i, index_is_id=True)
|
||||||
return QUrl.fromLocalFile(ans)
|
return QUrl.fromLocalFile(ans)
|
||||||
|
|
||||||
md.setUrls([url_for_id(i) for i in selected])
|
md.setUrls([url_for_id(i) for i in selected[:25]])
|
||||||
drag = QDrag(self)
|
drag = QDrag(self)
|
||||||
col = self.selectionModel().currentIndex().column()
|
col = self.selectionModel().currentIndex().column()
|
||||||
md.column_name = self.column_map[col]
|
md.column_name = self.column_map[col]
|
||||||
|
@ -688,6 +688,7 @@ class FormatsManager(QWidget): # {{{
|
|||||||
else:
|
else:
|
||||||
stream = open(fmt.path, 'r+b')
|
stream = open(fmt.path, 'r+b')
|
||||||
try:
|
try:
|
||||||
|
with stream:
|
||||||
mi = get_metadata(stream, ext)
|
mi = get_metadata(stream, ext)
|
||||||
return mi, ext
|
return mi, ext
|
||||||
except:
|
except:
|
||||||
|
@ -51,19 +51,19 @@ class BeamEBooksDEStore(BasicStoreConfig, StorePlugin):
|
|||||||
if counter <= 0:
|
if counter <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
id = ''.join(data.xpath('./tr/td/div[@class="stil2"]/a/@href')).strip()
|
id = ''.join(data.xpath('./tr/td[1]/a/@href')).strip()
|
||||||
if not id:
|
if not id:
|
||||||
continue
|
continue
|
||||||
id = id[7:]
|
id = id[7:]
|
||||||
cover_url = ''.join(data.xpath('./tr/td[1]/a/img/@src'))
|
cover_url = ''.join(data.xpath('./tr/td[1]/a/img/@src'))
|
||||||
if cover_url:
|
if cover_url:
|
||||||
cover_url = 'http://www.beam-ebooks.de' + cover_url
|
cover_url = 'http://www.beam-ebooks.de' + cover_url
|
||||||
title = ''.join(data.xpath('./tr/td/div[@class="stil2"]/a/b/text()'))
|
temp = ''.join(data.xpath('./tr/td[1]/a/img/@alt'))
|
||||||
author = ' '.join(data.xpath('./tr/td/div[@class="stil2"]/'
|
colon = temp.find(':')
|
||||||
'child::b/text()'
|
if not temp.startswith('eBook') or colon < 0:
|
||||||
'|'
|
continue
|
||||||
'./tr/td/div[@class="stil2"]/'
|
author = temp[5:colon]
|
||||||
'child::strong/text()'))
|
title = temp[colon+1:]
|
||||||
price = ''.join(data.xpath('./tr/td[3]/text()'))
|
price = ''.join(data.xpath('./tr/td[3]/text()'))
|
||||||
pdf = data.xpath(
|
pdf = data.xpath(
|
||||||
'boolean(./tr/td[3]/a/img[contains(@alt, "PDF")]/@alt)')
|
'boolean(./tr/td[3]/a/img[contains(@alt, "PDF")]/@alt)')
|
||||||
|
@ -58,6 +58,8 @@ class LegimiStore(BasicStoreConfig, StorePlugin):
|
|||||||
cover_url = ''.join(data.xpath('.//div[@class="item_cover_container"]/a/img/@src'))
|
cover_url = ''.join(data.xpath('.//div[@class="item_cover_container"]/a/img/@src'))
|
||||||
title = ''.join(data.xpath('.//div[@class="item_entries"]/h2/a/text()'))
|
title = ''.join(data.xpath('.//div[@class="item_entries"]/h2/a/text()'))
|
||||||
author = ''.join(data.xpath('.//div[@class="item_entries"]/span[1]/a/text()'))
|
author = ''.join(data.xpath('.//div[@class="item_entries"]/span[1]/a/text()'))
|
||||||
|
author = re.sub(',','',author)
|
||||||
|
author = re.sub(';',',',author)
|
||||||
price = ''.join(data.xpath('.//div[@class="item_entries"]/span[3]/text()'))
|
price = ''.join(data.xpath('.//div[@class="item_entries"]/span[3]/text()'))
|
||||||
price = re.sub(r'[^0-9,]*','',price) + ' zł'
|
price = re.sub(r'[^0-9,]*','',price) + ' zł'
|
||||||
|
|
||||||
|
90
src/calibre/gui2/store/libri_de_plugin.py
Normal file
90
src/calibre/gui2/store/libri_de_plugin.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__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 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=[['
|
||||||
|
'http://www.libri.de/shop/action/productDetails?artiId={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.libri.de/shop/action/quickSearch?facetNodeId=6'
|
||||||
|
'&mainsearchSubmit=Los!&searchString=' + 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('//div[contains(@class, "item")]'):
|
||||||
|
if counter <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
details = data.xpath('./div[@class="beschreibungContainer"]')
|
||||||
|
if not details:
|
||||||
|
continue
|
||||||
|
details = details[0]
|
||||||
|
id = ''.join(details.xpath('./div[@class="text"]/a/@name')).strip()
|
||||||
|
if not id:
|
||||||
|
continue
|
||||||
|
cover_url = ''.join(details.xpath('./div[@class="bild"]/a/img/@src'))
|
||||||
|
title = ''.join(details.xpath('./div[@class="text"]/span[@class="titel"]/a/text()')).strip()
|
||||||
|
author = ''.join(details.xpath('./div[@class="text"]/span[@class="author"]/text()')).strip()
|
||||||
|
pdf = details.xpath(
|
||||||
|
'boolean(.//span[@class="format" and contains(text(), "pdf")]/text())')
|
||||||
|
epub = details.xpath(
|
||||||
|
'boolean(.//span[@class="format" and contains(text(), "epub")]/text())')
|
||||||
|
mobi = details.xpath(
|
||||||
|
'boolean(.//span[@class="format" and contains(text(), "mobipocket")]/text())')
|
||||||
|
price = (''.join(data.xpath('.//span[@class="preis"]/text()'))).replace('*', '')
|
||||||
|
counter -= 1
|
||||||
|
|
||||||
|
s = SearchResult()
|
||||||
|
s.cover_url = cover_url
|
||||||
|
s.title = title.strip()
|
||||||
|
s.author = author.strip()
|
||||||
|
s.price = price
|
||||||
|
s.drm = SearchResult.DRM_UNKNOWN
|
||||||
|
s.detail_item = id
|
||||||
|
formats = []
|
||||||
|
if epub:
|
||||||
|
formats.append('ePub')
|
||||||
|
if pdf:
|
||||||
|
formats.append('PDF')
|
||||||
|
if mobi:
|
||||||
|
formats.append('MOBI')
|
||||||
|
s.formats = ', '.join(formats)
|
||||||
|
|
||||||
|
yield s
|
@ -57,7 +57,7 @@ class WoblinkStore(BasicStoreConfig, StorePlugin):
|
|||||||
|
|
||||||
cover_url = ''.join(data.xpath('.//td[@class="w10 va-t"]/a[1]/img/@src'))
|
cover_url = ''.join(data.xpath('.//td[@class="w10 va-t"]/a[1]/img/@src'))
|
||||||
title = ''.join(data.xpath('.//h3[@class="title"]/a[1]/text()'))
|
title = ''.join(data.xpath('.//h3[@class="title"]/a[1]/text()'))
|
||||||
author = ''.join(data.xpath('.//p[@class="author"]/a[1]/text()'))
|
author = ', '.join(data.xpath('.//p[@class="author"]/a/text()'))
|
||||||
price = ''.join(data.xpath('.//div[@class="prices"]/p[1]/span/text()'))
|
price = ''.join(data.xpath('.//div[@class="prices"]/p[1]/span/text()'))
|
||||||
price = re.sub('PLN', ' zł', price)
|
price = re.sub('PLN', ' zł', price)
|
||||||
price = re.sub('\.', ',', price)
|
price = re.sub('\.', ',', price)
|
||||||
|
@ -53,7 +53,7 @@ class ZixoStore(BasicStoreConfig, StorePlugin):
|
|||||||
|
|
||||||
cover_url = ''.join(data.xpath('.//a[@class="productThumb"]/img/@src'))
|
cover_url = ''.join(data.xpath('.//a[@class="productThumb"]/img/@src'))
|
||||||
title = ''.join(data.xpath('.//a[@class="title"]/text()'))
|
title = ''.join(data.xpath('.//a[@class="title"]/text()'))
|
||||||
author = ''.join(data.xpath('.//div[@class="productDescription"]/span[1]/a/text()'))
|
author = ','.join(data.xpath('.//div[@class="productDescription"]/span[1]/a/text()'))
|
||||||
price = ''.join(data.xpath('.//div[@class="priceList"]/span/text()'))
|
price = ''.join(data.xpath('.//div[@class="priceList"]/span/text()'))
|
||||||
price = re.sub('\.', ',', price)
|
price = re.sub('\.', ',', price)
|
||||||
|
|
||||||
|
@ -51,12 +51,15 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
|
|||||||
# continue
|
# continue
|
||||||
|
|
||||||
mi = db.get_metadata(book_id, True)
|
mi = db.get_metadata(book_id, True)
|
||||||
in_file = db.format_abspath(book_id, d.input_format, True)
|
in_file = PersistentTemporaryFile('.'+d.input_format)
|
||||||
|
with in_file:
|
||||||
|
db.copy_format_to(book_id, d.input_format, in_file,
|
||||||
|
index_is_id=True)
|
||||||
|
|
||||||
out_file = PersistentTemporaryFile('.' + d.output_format)
|
out_file = PersistentTemporaryFile('.' + d.output_format)
|
||||||
out_file.write(d.output_format)
|
out_file.write(d.output_format)
|
||||||
out_file.close()
|
out_file.close()
|
||||||
temp_files = []
|
temp_files = [in_file]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dtitle = unicode(mi.title)
|
dtitle = unicode(mi.title)
|
||||||
@ -74,7 +77,7 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
|
|||||||
recs.append(('cover', d.cover_file.name,
|
recs.append(('cover', d.cover_file.name,
|
||||||
OptionRecommendation.HIGH))
|
OptionRecommendation.HIGH))
|
||||||
temp_files.append(d.cover_file)
|
temp_files.append(d.cover_file)
|
||||||
args = [in_file, out_file.name, recs]
|
args = [in_file.name, out_file.name, recs]
|
||||||
temp_files.append(out_file)
|
temp_files.append(out_file)
|
||||||
jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files))
|
jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files))
|
||||||
|
|
||||||
@ -142,12 +145,15 @@ class QueueBulk(QProgressDialog):
|
|||||||
try:
|
try:
|
||||||
input_format = get_input_format_for_book(self.db, book_id, None)[0]
|
input_format = get_input_format_for_book(self.db, book_id, None)[0]
|
||||||
mi, opf_file = create_opf_file(self.db, book_id)
|
mi, opf_file = create_opf_file(self.db, book_id)
|
||||||
in_file = self.db.format_abspath(book_id, input_format, True)
|
in_file = PersistentTemporaryFile('.'+input_format)
|
||||||
|
with in_file:
|
||||||
|
self.db.copy_format_to(book_id, input_format, in_file,
|
||||||
|
index_is_id=True)
|
||||||
|
|
||||||
out_file = PersistentTemporaryFile('.' + self.output_format)
|
out_file = PersistentTemporaryFile('.' + self.output_format)
|
||||||
out_file.write(self.output_format)
|
out_file.write(self.output_format)
|
||||||
out_file.close()
|
out_file.close()
|
||||||
temp_files = []
|
temp_files = [in_file]
|
||||||
|
|
||||||
combined_recs = GuiRecommendations()
|
combined_recs = GuiRecommendations()
|
||||||
default_recs = bulk_defaults_for_input_format(input_format)
|
default_recs = bulk_defaults_for_input_format(input_format)
|
||||||
@ -183,7 +189,7 @@ class QueueBulk(QProgressDialog):
|
|||||||
self.setLabelText(_('Queueing ')+dtitle)
|
self.setLabelText(_('Queueing ')+dtitle)
|
||||||
desc = _('Convert book %d of %d (%s)') % (self.i, len(self.book_ids), dtitle)
|
desc = _('Convert book %d of %d (%s)') % (self.i, len(self.book_ids), dtitle)
|
||||||
|
|
||||||
args = [in_file, out_file.name, lrecs]
|
args = [in_file.name, out_file.name, lrecs]
|
||||||
temp_files.append(out_file)
|
temp_files.append(out_file)
|
||||||
self.jobs.append(('gui_convert_override', args, desc, self.output_format.upper(), book_id, temp_files))
|
self.jobs.append(('gui_convert_override', args, desc, self.output_format.upper(), book_id, temp_files))
|
||||||
|
|
||||||
|
@ -61,22 +61,4 @@ def generate_test_db(library_path, # {{{
|
|||||||
print 'Time per record:', t/float(num_of_records)
|
print 'Time per record:', t/float(num_of_records)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def cover_load_timing(path=None):
|
|
||||||
from PyQt4.Qt import QApplication, QImage
|
|
||||||
import os, time
|
|
||||||
app = QApplication([])
|
|
||||||
app
|
|
||||||
d = db(path)
|
|
||||||
paths = [d.cover(i, index_is_id=True, as_path=True) for i in
|
|
||||||
d.data.iterallids()]
|
|
||||||
paths = [p for p in paths if (p and os.path.exists(p) and os.path.isfile(p))]
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
for p in paths:
|
|
||||||
with open(p, 'rb') as f:
|
|
||||||
img = QImage()
|
|
||||||
img.loadFromData(f.read())
|
|
||||||
|
|
||||||
print 'Average load time:', (time.time() - start)/len(paths), 'seconds'
|
|
||||||
|
|
||||||
|
@ -5137,6 +5137,7 @@ Author '{0}':
|
|||||||
OptionRecommendation.HIGH))
|
OptionRecommendation.HIGH))
|
||||||
|
|
||||||
# If cover exists, use it
|
# If cover exists, use it
|
||||||
|
cpath = None
|
||||||
try:
|
try:
|
||||||
search_text = 'title:"%s" author:%s' % (
|
search_text = 'title:"%s" author:%s' % (
|
||||||
opts.catalog_title.replace('"', '\\"'), 'calibre')
|
opts.catalog_title.replace('"', '\\"'), 'calibre')
|
||||||
@ -5157,5 +5158,10 @@ Author '{0}':
|
|||||||
plumber.merge_ui_recommendations(recommendations)
|
plumber.merge_ui_recommendations(recommendations)
|
||||||
plumber.run()
|
plumber.run()
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(cpath)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# returns to gui2.actions.catalog:catalog_generated()
|
# returns to gui2.actions.catalog:catalog_generated()
|
||||||
return catalog.error
|
return catalog.error
|
||||||
|
@ -12,8 +12,6 @@ import threading, random
|
|||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
from PyQt4.QtGui import QImage
|
|
||||||
|
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
from calibre.ebooks.metadata import (title_sort, author_to_author_sort,
|
from calibre.ebooks.metadata import (title_sort, author_to_author_sort,
|
||||||
string_to_authors, authors_to_string)
|
string_to_authors, authors_to_string)
|
||||||
@ -27,7 +25,7 @@ from calibre.library.sqlite import connect, IntegrityError
|
|||||||
from calibre.library.prefs import DBPrefs
|
from calibre.library.prefs import DBPrefs
|
||||||
from calibre.ebooks.metadata.book.base import Metadata
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
from calibre.constants import preferred_encoding, iswindows, filesystem_encoding
|
from calibre.constants import preferred_encoding, iswindows, filesystem_encoding
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile, base_dir
|
||||||
from calibre.customize.ui import run_plugins_on_import
|
from calibre.customize.ui import run_plugins_on_import
|
||||||
from calibre import isbytestring
|
from calibre import isbytestring
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
@ -39,8 +37,10 @@ from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
|||||||
from calibre.utils.magick.draw import save_cover_data_to
|
from calibre.utils.magick.draw import save_cover_data_to
|
||||||
from calibre.utils.recycle_bin import delete_file, delete_tree
|
from calibre.utils.recycle_bin import delete_file, delete_tree
|
||||||
from calibre.utils.formatter_functions import load_user_template_functions
|
from calibre.utils.formatter_functions import load_user_template_functions
|
||||||
|
from calibre.db.errors import NoSuchFormat
|
||||||
|
|
||||||
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||||
|
SPOOL_SIZE = 30*1024*1024
|
||||||
|
|
||||||
class Tag(object):
|
class Tag(object):
|
||||||
|
|
||||||
@ -201,12 +201,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
dbprefs = DBPrefs(self)
|
dbprefs = DBPrefs(self)
|
||||||
for key in default_prefs:
|
for key in default_prefs:
|
||||||
# be sure that prefs not to be copied are listed below
|
# be sure that prefs not to be copied are listed below
|
||||||
if key in ['news_to_be_synced']:
|
if key in frozenset(['news_to_be_synced']):
|
||||||
continue
|
continue
|
||||||
try:
|
|
||||||
dbprefs[key] = default_prefs[key]
|
dbprefs[key] = default_prefs[key]
|
||||||
except:
|
if 'field_metadata' in default_prefs:
|
||||||
pass # ignore options that don't exist anymore
|
|
||||||
fmvals = [f for f in default_prefs['field_metadata'].values() if f['is_custom']]
|
fmvals = [f for f in default_prefs['field_metadata'].values() if f['is_custom']]
|
||||||
for f in fmvals:
|
for f in fmvals:
|
||||||
self.create_custom_column(f['label'], f['name'], f['datatype'],
|
self.create_custom_column(f['label'], f['name'], f['datatype'],
|
||||||
@ -603,14 +601,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
with lopen(os.path.join(tpath, 'cover.jpg'), 'wb') as f:
|
with lopen(os.path.join(tpath, 'cover.jpg'), 'wb') as f:
|
||||||
f.write(cdata)
|
f.write(cdata)
|
||||||
for format in formats:
|
for format in formats:
|
||||||
# Get data as string (can't use file as source and target files may be the same)
|
with tempfile.SpooledTemporaryFile(max_size=SPOOL_SIZE) as stream:
|
||||||
f = self.format(id, format, index_is_id=True, as_file=True)
|
try:
|
||||||
if f is None:
|
self.copy_format_to(id, format, stream, index_is_id=True)
|
||||||
continue
|
|
||||||
with tempfile.SpooledTemporaryFile(max_size=30*(1024**2)) as stream:
|
|
||||||
with f:
|
|
||||||
shutil.copyfileobj(f, stream)
|
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
|
except NoSuchFormat:
|
||||||
|
continue
|
||||||
self.add_format(id, format, stream, index_is_id=True,
|
self.add_format(id, format, stream, index_is_id=True,
|
||||||
path=tpath, notify=False)
|
path=tpath, notify=False)
|
||||||
self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id))
|
self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id))
|
||||||
@ -667,28 +663,49 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
'''
|
'''
|
||||||
Return the cover image as a bytestring (in JPEG format) or None.
|
Return the cover image as a bytestring (in JPEG format) or None.
|
||||||
|
|
||||||
`as_file` : If True return the image as an open file object
|
WARNING: Using as_path will copy the cover to a temp file and return
|
||||||
`as_image`: If True return the image as a QImage object
|
the path to the temp file. You should delete the temp file when you are
|
||||||
|
done with it.
|
||||||
|
|
||||||
|
:param as_file: If True return the image as an open file object (a SpooledTemporaryFile)
|
||||||
|
:param as_image: If True return the image as a QImage object
|
||||||
'''
|
'''
|
||||||
id = index if index_is_id else self.id(index)
|
id = index if index_is_id else self.id(index)
|
||||||
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
||||||
if os.access(path, os.R_OK):
|
if os.access(path, os.R_OK):
|
||||||
if as_path:
|
|
||||||
return path
|
|
||||||
try:
|
try:
|
||||||
f = lopen(path, 'rb')
|
f = lopen(path, 'rb')
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
f = lopen(path, 'rb')
|
f = lopen(path, 'rb')
|
||||||
|
with f:
|
||||||
|
if as_path:
|
||||||
|
pt = PersistentTemporaryFile('_dbcover.jpg')
|
||||||
|
with pt:
|
||||||
|
shutil.copyfileobj(f, pt)
|
||||||
|
return pt.name
|
||||||
|
if as_file:
|
||||||
|
ret = tempfile.SpooledTemporaryFile(SPOOL_SIZE)
|
||||||
|
shutil.copyfileobj(f, ret)
|
||||||
|
ret.seek(0)
|
||||||
|
else:
|
||||||
|
ret = f.read()
|
||||||
if as_image:
|
if as_image:
|
||||||
img = QImage()
|
from PyQt4.Qt import QImage
|
||||||
img.loadFromData(f.read())
|
i = QImage()
|
||||||
f.close()
|
i.loadFromData(ret)
|
||||||
return img
|
ret = i
|
||||||
ans = f if as_file else f.read()
|
return ret
|
||||||
if ans is not f:
|
|
||||||
f.close()
|
def cover_last_modified(self, index, index_is_id=False):
|
||||||
return ans
|
id = index if index_is_id else self.id(index)
|
||||||
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
||||||
|
try:
|
||||||
|
return utcfromtimestamp(os.stat(path).st_mtime)
|
||||||
|
except:
|
||||||
|
# Cover doesn't exist
|
||||||
|
pass
|
||||||
|
return self.last_modified()
|
||||||
|
|
||||||
### The field-style interface. These use field keys.
|
### The field-style interface. These use field keys.
|
||||||
|
|
||||||
@ -861,7 +878,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return (path, mi, sequence)
|
return (path, mi, sequence)
|
||||||
|
|
||||||
def get_metadata(self, idx, index_is_id=False, get_cover=False,
|
def get_metadata(self, idx, index_is_id=False, get_cover=False,
|
||||||
get_user_categories=True):
|
get_user_categories=True, cover_as_data=False):
|
||||||
'''
|
'''
|
||||||
Convenience method to return metadata as a :class:`Metadata` object.
|
Convenience method to return metadata as a :class:`Metadata` object.
|
||||||
Note that the list of formats is not verified.
|
Note that the list of formats is not verified.
|
||||||
@ -936,6 +953,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
mi.user_categories = user_cat_vals
|
mi.user_categories = user_cat_vals
|
||||||
|
|
||||||
if get_cover:
|
if get_cover:
|
||||||
|
if cover_as_data:
|
||||||
|
cdata = self.cover(id, index_is_id=True)
|
||||||
|
if cdata:
|
||||||
|
mi.cover_data = ('jpeg', cdata)
|
||||||
|
else:
|
||||||
mi.cover = self.cover(id, index_is_id=True, as_path=True)
|
mi.cover = self.cover(id, index_is_id=True, as_path=True)
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
@ -1101,7 +1123,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return utcfromtimestamp(os.stat(path).st_mtime)
|
return utcfromtimestamp(os.stat(path).st_mtime)
|
||||||
|
|
||||||
def format_abspath(self, index, format, index_is_id=False):
|
def format_abspath(self, index, format, index_is_id=False):
|
||||||
'Return absolute path to the ebook file of format `format`'
|
'''
|
||||||
|
Return absolute path to the ebook file of format `format`
|
||||||
|
|
||||||
|
WARNING: This method will return a dummy path for a network backend DB,
|
||||||
|
so do not rely on it, use format(..., as_path=True) instead.
|
||||||
|
|
||||||
|
Currently used only in calibredb list, the viewer and the catalogs (via
|
||||||
|
get_data_as_dict()).
|
||||||
|
|
||||||
|
Apart from the viewer, I don't believe any of the others do any file
|
||||||
|
I/O with the results of this call.
|
||||||
|
'''
|
||||||
id = index if index_is_id else self.id(index)
|
id = index if index_is_id else self.id(index)
|
||||||
try:
|
try:
|
||||||
name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False)
|
name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False)
|
||||||
@ -1121,25 +1154,63 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
shutil.copyfile(candidates[0], fmt_path)
|
shutil.copyfile(candidates[0], fmt_path)
|
||||||
return fmt_path
|
return fmt_path
|
||||||
|
|
||||||
def format(self, index, format, index_is_id=False, as_file=False, mode='r+b'):
|
def copy_format_to(self, index, fmt, dest, index_is_id=False):
|
||||||
|
'''
|
||||||
|
Copy the format ``fmt`` to the file like object ``dest``. If the
|
||||||
|
specified format does not exist, raises :class:`NoSuchFormat` error.
|
||||||
|
'''
|
||||||
|
path = self.format_abspath(index, fmt, index_is_id=index_is_id)
|
||||||
|
if path is None:
|
||||||
|
id_ = index if index_is_id else self.id(index)
|
||||||
|
raise NoSuchFormat('Record %d has no %s file'%(id_, fmt))
|
||||||
|
with lopen(path, 'rb') as f:
|
||||||
|
shutil.copyfileobj(f, dest)
|
||||||
|
if hasattr(dest, 'flush'):
|
||||||
|
dest.flush()
|
||||||
|
|
||||||
|
def format(self, index, format, index_is_id=False, as_file=False,
|
||||||
|
mode='r+b', as_path=False, preserve_filename=False):
|
||||||
'''
|
'''
|
||||||
Return the ebook format as a bytestring or `None` if the format doesn't exist,
|
Return the ebook format as a bytestring or `None` if the format doesn't exist,
|
||||||
or we don't have permission to write to the ebook file.
|
or we don't have permission to write to the ebook file.
|
||||||
|
|
||||||
`as_file`: If True the ebook format is returned as a file object opened in `mode`
|
:param as_file: If True the ebook format is returned as a file object. Note
|
||||||
|
that the file object is a SpooledTemporaryFile, so if what you want to
|
||||||
|
do is copy the format to another file, use :method:`copy_format_to`
|
||||||
|
instead for performance.
|
||||||
|
:param as_path: Copies the format file to a temp file and returns the
|
||||||
|
path to the temp file
|
||||||
|
:param preserve_filename: If True and returning a path the filename is
|
||||||
|
the same as that used in the library. Note that using
|
||||||
|
this means that repeated calls yield the same
|
||||||
|
temp file (which is re-created each time)
|
||||||
|
:param mode: This is ignored (present for legacy compatibility)
|
||||||
'''
|
'''
|
||||||
path = self.format_abspath(index, format, index_is_id=index_is_id)
|
path = self.format_abspath(index, format, index_is_id=index_is_id)
|
||||||
if path is not None:
|
if path is not None:
|
||||||
f = lopen(path, mode)
|
with lopen(path, mode) as f:
|
||||||
|
if as_path:
|
||||||
|
if preserve_filename:
|
||||||
|
bd = base_dir()
|
||||||
|
d = os.path.join(bd, 'format_abspath')
|
||||||
try:
|
try:
|
||||||
ret = f if as_file else f.read()
|
os.makedirs(d)
|
||||||
except IOError:
|
except:
|
||||||
f.seek(0)
|
pass
|
||||||
out = cStringIO.StringIO()
|
fname = os.path.basename(path)
|
||||||
shutil.copyfileobj(f, out)
|
ret = os.path.join(d, fname)
|
||||||
ret = out.getvalue()
|
with lopen(ret, 'wb') as f2:
|
||||||
if not as_file:
|
shutil.copyfileobj(f, f2)
|
||||||
f.close()
|
else:
|
||||||
|
with PersistentTemporaryFile('.'+format.lower()) as pt:
|
||||||
|
shutil.copyfileobj(f, pt)
|
||||||
|
ret = pt.name
|
||||||
|
elif as_file:
|
||||||
|
ret = tempfile.SpooledTemporaryFile(max_size=SPOOL_SIZE)
|
||||||
|
shutil.copyfileobj(f, ret)
|
||||||
|
ret.seek(0)
|
||||||
|
else:
|
||||||
|
ret = f.read()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def add_format_with_hooks(self, index, format, fpath, index_is_id=False,
|
def add_format_with_hooks(self, index, format, fpath, index_is_id=False,
|
||||||
@ -1931,13 +2002,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
def authors_with_sort_strings(self, id, index_is_id=False):
|
def authors_with_sort_strings(self, id, index_is_id=False):
|
||||||
id = id if index_is_id else self.id(id)
|
id = id if index_is_id else self.id(id)
|
||||||
aut_strings = self.conn.get('''
|
aut_strings = self.conn.get('''
|
||||||
SELECT authors.name, authors.sort
|
SELECT authors.id, authors.name, authors.sort
|
||||||
FROM authors, books_authors_link as bl
|
FROM authors, books_authors_link as bl
|
||||||
WHERE bl.book=? and authors.id=bl.author
|
WHERE bl.book=? and authors.id=bl.author
|
||||||
ORDER BY bl.id''', (id,))
|
ORDER BY bl.id''', (id,))
|
||||||
result = []
|
result = []
|
||||||
for (author, sort,) in aut_strings:
|
for (id_, author, sort,) in aut_strings:
|
||||||
result.append((author.replace('|', ','), sort))
|
result.append((id_, author.replace('|', ','), sort))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Given a book, return the author_sort string for authors of the book
|
# Given a book, return the author_sort string for authors of the book
|
||||||
@ -1945,6 +2016,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
auts = self.authors_sort_strings(id, index_is_id)
|
auts = self.authors_sort_strings(id, index_is_id)
|
||||||
return ' & '.join(auts).replace('|', ',')
|
return ' & '.join(auts).replace('|', ',')
|
||||||
|
|
||||||
|
# Given an author, return a list of books with that author
|
||||||
|
def books_for_author(self, id_, index_is_id=False):
|
||||||
|
id_ = id_ if index_is_id else self.id(id_)
|
||||||
|
books = self.conn.get('''
|
||||||
|
SELECT bl.book
|
||||||
|
FROM books_authors_link as bl
|
||||||
|
WHERE bl.author=?''', (id_,))
|
||||||
|
return [b[0] for b in books]
|
||||||
|
|
||||||
# Given a list of authors, return the author_sort string for the authors,
|
# Given a list of authors, return the author_sort string for the authors,
|
||||||
# preferring the author sort associated with the author over the computed
|
# preferring the author sort associated with the author over the computed
|
||||||
# string
|
# string
|
||||||
@ -1968,7 +2048,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
|
|
||||||
aum = self.authors_with_sort_strings(id_, index_is_id=True)
|
aum = self.authors_with_sort_strings(id_, index_is_id=True)
|
||||||
self.data.set(id_, self.FIELD_MAP['au_map'],
|
self.data.set(id_, self.FIELD_MAP['au_map'],
|
||||||
':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (au, aus) in aum]),
|
':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (_, au, aus) in aum]),
|
||||||
row_is_id=True)
|
row_is_id=True)
|
||||||
|
|
||||||
def _set_authors(self, id, authors, allow_case_change=False):
|
def _set_authors(self, id, authors, allow_case_change=False):
|
||||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, traceback, cStringIO, re, shutil
|
import os, traceback, cStringIO, re
|
||||||
|
|
||||||
from calibre.constants import DEBUG
|
from calibre.constants import DEBUG
|
||||||
from calibre.utils.config import Config, StringConfig, tweaks
|
from calibre.utils.config import Config, StringConfig, tweaks
|
||||||
@ -238,7 +238,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
|||||||
|
|
||||||
def save_book_to_disk(id_, db, root, opts, length):
|
def save_book_to_disk(id_, db, root, opts, length):
|
||||||
mi = db.get_metadata(id_, index_is_id=True)
|
mi = db.get_metadata(id_, index_is_id=True)
|
||||||
cover = db.cover(id_, index_is_id=True, as_path=True)
|
cover = db.cover(id_, index_is_id=True)
|
||||||
plugboards = db.prefs.get('plugboards', {})
|
plugboards = db.prefs.get('plugboards', {})
|
||||||
|
|
||||||
available_formats = db.formats(id_, index_is_id=True)
|
available_formats = db.formats(id_, index_is_id=True)
|
||||||
@ -252,12 +252,20 @@ def save_book_to_disk(id_, db, root, opts, length):
|
|||||||
if fmts:
|
if fmts:
|
||||||
fmts = fmts.split(',')
|
fmts = fmts.split(',')
|
||||||
for fmt in fmts:
|
for fmt in fmts:
|
||||||
fpath = db.format_abspath(id_, fmt, index_is_id=True)
|
fpath = db.format(id_, fmt, index_is_id=True, as_path=True)
|
||||||
if fpath is not None:
|
if fpath is not None:
|
||||||
formats[fmt.lower()] = fpath
|
formats[fmt.lower()] = fpath
|
||||||
|
|
||||||
|
try:
|
||||||
return do_save_book_to_disk(id_, mi, cover, plugboards,
|
return do_save_book_to_disk(id_, mi, cover, plugboards,
|
||||||
formats, root, opts, length)
|
formats, root, opts, length)
|
||||||
|
finally:
|
||||||
|
for temp in formats.itervalues():
|
||||||
|
try:
|
||||||
|
os.remove(temp)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def do_save_book_to_disk(id_, mi, cover, plugboards,
|
def do_save_book_to_disk(id_, mi, cover, plugboards,
|
||||||
@ -289,10 +297,9 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
ocover = mi.cover
|
ocover = mi.cover
|
||||||
if opts.save_cover and cover and os.access(cover, os.R_OK):
|
if opts.save_cover and cover:
|
||||||
with open(base_path+'.jpg', 'wb') as f:
|
with open(base_path+'.jpg', 'wb') as f:
|
||||||
with open(cover, 'rb') as s:
|
f.write(cover)
|
||||||
shutil.copyfileobj(s, f)
|
|
||||||
mi.cover = base_name+'.jpg'
|
mi.cover = base_name+'.jpg'
|
||||||
else:
|
else:
|
||||||
mi.cover = None
|
mi.cover = None
|
||||||
@ -395,8 +402,13 @@ def save_serialized_to_disk(ids, data, plugboards, root, opts, callback):
|
|||||||
pass
|
pass
|
||||||
tb = ''
|
tb = ''
|
||||||
try:
|
try:
|
||||||
failed, id, title = do_save_book_to_disk(x, mi, cover, plugboards,
|
with open(cover, 'rb') as f:
|
||||||
format_map, root, opts, length)
|
cover = f.read()
|
||||||
|
except:
|
||||||
|
cover = None
|
||||||
|
try:
|
||||||
|
failed, id, title = do_save_book_to_disk(x, mi, cover,
|
||||||
|
plugboards, format_map, root, opts, length)
|
||||||
tb = _('Requested formats not available')
|
tb = _('Requested formats not available')
|
||||||
except:
|
except:
|
||||||
failed, id, title = True, x, mi.title
|
failed, id, title = True, x, mi.title
|
||||||
|
@ -10,12 +10,13 @@ import re, os, posixpath
|
|||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
from calibre import fit_image, guess_type
|
from calibre import fit_image, guess_type
|
||||||
from calibre.utils.date import fromtimestamp
|
from calibre.utils.date import fromtimestamp, utcnow
|
||||||
from calibre.library.caches import SortKeyGenerator
|
from calibre.library.caches import SortKeyGenerator
|
||||||
from calibre.library.save_to_disk import find_plugboard
|
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, \
|
from calibre.utils.magick.draw import (save_cover_data_to, Image,
|
||||||
thumbnail as generate_thumbnail
|
thumbnail as generate_thumbnail)
|
||||||
|
from calibre.utils.filenames import ascii_filename
|
||||||
|
|
||||||
plugboard_content_server_value = 'content_server'
|
plugboard_content_server_value = 'content_server'
|
||||||
plugboard_content_server_formats = ['epub']
|
plugboard_content_server_formats = ['epub']
|
||||||
@ -46,7 +47,7 @@ class ContentServer(object):
|
|||||||
# Utility methods {{{
|
# Utility methods {{{
|
||||||
def last_modified(self, updated):
|
def last_modified(self, updated):
|
||||||
'''
|
'''
|
||||||
Generates a local independent, english timestamp from a datetime
|
Generates a locale independent, english timestamp from a datetime
|
||||||
object
|
object
|
||||||
'''
|
'''
|
||||||
lm = updated.strftime('day, %d month %Y %H:%M:%S GMT')
|
lm = updated.strftime('day, %d month %Y %H:%M:%S GMT')
|
||||||
@ -151,14 +152,12 @@ class ContentServer(object):
|
|||||||
try:
|
try:
|
||||||
cherrypy.response.headers['Content-Type'] = 'image/jpeg'
|
cherrypy.response.headers['Content-Type'] = 'image/jpeg'
|
||||||
cherrypy.response.timeout = 3600
|
cherrypy.response.timeout = 3600
|
||||||
cover = self.db.cover(id, index_is_id=True, as_file=True)
|
cover = self.db.cover(id, index_is_id=True)
|
||||||
if cover is None:
|
if cover is None:
|
||||||
cover = self.default_cover
|
cover = self.default_cover
|
||||||
updated = self.build_time
|
updated = self.build_time
|
||||||
else:
|
else:
|
||||||
with cover as f:
|
updated = self.db.cover_last_modified(id, index_is_id=True)
|
||||||
updated = fromtimestamp(os.fstat(f.fileno()).st_mtime)
|
|
||||||
cover = f.read()
|
|
||||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||||
|
|
||||||
if thumbnail:
|
if thumbnail:
|
||||||
@ -187,9 +186,9 @@ class ContentServer(object):
|
|||||||
mode='rb')
|
mode='rb')
|
||||||
if fmt is None:
|
if fmt is None:
|
||||||
raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format))
|
raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format))
|
||||||
|
mi = self.db.get_metadata(id, index_is_id=True)
|
||||||
if format == 'EPUB':
|
if format == 'EPUB':
|
||||||
# Get the original metadata
|
# Get the original metadata
|
||||||
mi = self.db.get_metadata(id, index_is_id=True)
|
|
||||||
|
|
||||||
# Get any EPUB plugboards for the content server
|
# Get any EPUB plugboards for the content server
|
||||||
plugboards = self.db.prefs.get('plugboards', {})
|
plugboards = self.db.prefs.get('plugboards', {})
|
||||||
@ -203,24 +202,22 @@ class ContentServer(object):
|
|||||||
newmi = mi
|
newmi = mi
|
||||||
|
|
||||||
# Write the updated file
|
# Write the updated file
|
||||||
from tempfile import TemporaryFile
|
|
||||||
from calibre.ebooks.metadata.meta import set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata
|
||||||
raw = fmt.read()
|
|
||||||
fmt = TemporaryFile()
|
|
||||||
fmt.write(raw)
|
|
||||||
fmt.seek(0)
|
|
||||||
set_metadata(fmt, newmi, 'epub')
|
set_metadata(fmt, newmi, 'epub')
|
||||||
fmt.seek(0)
|
fmt.seek(0)
|
||||||
|
|
||||||
mt = guess_type('dummy.'+format.lower())[0]
|
mt = guess_type('dummy.'+format.lower())[0]
|
||||||
if mt is None:
|
if mt is None:
|
||||||
mt = 'application/octet-stream'
|
mt = 'application/octet-stream'
|
||||||
|
au = authors_to_string(mi.authors if mi.authors else [_('Unknown')])
|
||||||
|
title = mi.title if mi.title else _('Unknown')
|
||||||
|
fname = u'%s - %s_%s.%s'%(title[:30], au[:30], id, format.lower())
|
||||||
|
fname = ascii_filename(fname).replace('"', '_')
|
||||||
cherrypy.response.headers['Content-Type'] = mt
|
cherrypy.response.headers['Content-Type'] = mt
|
||||||
|
cherrypy.response.headers['Content-Disposition'] = \
|
||||||
|
b'attachment; filename="%s"'%fname
|
||||||
cherrypy.response.timeout = 3600
|
cherrypy.response.timeout = 3600
|
||||||
path = getattr(fmt, 'name', None)
|
cherrypy.response.headers['Last-Modified'] = self.last_modified(utcnow())
|
||||||
if path and os.path.exists(path):
|
|
||||||
updated = fromtimestamp(os.stat(path).st_mtime)
|
|
||||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
|
||||||
return fmt
|
return fmt
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user