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,
|
||||
lccn TEXT DEFAULT "" COLLATE NOCASE,
|
||||
path TEXT NOT NULL DEFAULT "",
|
||||
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");
|
||||
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");
|
||||
CREATE TABLE books_authors_link ( id INTEGER PRIMARY KEY,
|
||||
book INTEGER NOT NULL,
|
||||
author INTEGER NOT NULL,
|
||||
|
@ -578,6 +578,7 @@ def url_slash_cleaner(url):
|
||||
def get_download_filename(url, cookie_file=None):
|
||||
'''
|
||||
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 urllib2 import unquote as urllib2_unquote
|
||||
@ -591,8 +592,10 @@ def get_download_filename(url, cookie_file=None):
|
||||
cj.load(cookie_file)
|
||||
br.set_cookiejar(cj)
|
||||
|
||||
last_part_name = ''
|
||||
try:
|
||||
with closing(br.open(url)) as r:
|
||||
last_part_name = r.geturl().split('/')[-1]
|
||||
disposition = r.info().get('Content-disposition', '')
|
||||
for p in disposition.split(';'):
|
||||
if 'filename' in p:
|
||||
@ -612,7 +615,7 @@ def get_download_filename(url, cookie_file=None):
|
||||
traceback.print_exc()
|
||||
|
||||
if not filename:
|
||||
filename = r.geturl().split('/')[-1]
|
||||
filename = last_part_name
|
||||
|
||||
return filename
|
||||
|
||||
|
@ -762,99 +762,132 @@ plugins += input_profiles + output_profiles
|
||||
class ActionAdd(InterfaceActionBase):
|
||||
name = 'Add Books'
|
||||
actual_plugin = 'calibre.gui2.actions.add:AddAction'
|
||||
description = _('Add books to calibre or the connected device')
|
||||
|
||||
class ActionFetchAnnotations(InterfaceActionBase):
|
||||
name = 'Fetch Annotations'
|
||||
actual_plugin = 'calibre.gui2.actions.annotate:FetchAnnotationsAction'
|
||||
description = _('Fetch annotations from a connected Kindle (experimental)')
|
||||
|
||||
class ActionGenerateCatalog(InterfaceActionBase):
|
||||
name = 'Generate Catalog'
|
||||
actual_plugin = 'calibre.gui2.actions.catalog:GenerateCatalogAction'
|
||||
description = _('Generate a catalog of the books in your calibre library')
|
||||
|
||||
class ActionConvert(InterfaceActionBase):
|
||||
name = 'Convert Books'
|
||||
actual_plugin = 'calibre.gui2.actions.convert:ConvertAction'
|
||||
description = _('Convert books to various ebook formats')
|
||||
|
||||
class ActionDelete(InterfaceActionBase):
|
||||
name = 'Remove Books'
|
||||
actual_plugin = 'calibre.gui2.actions.delete:DeleteAction'
|
||||
description = _('Delete books from your calibre library or connected device')
|
||||
|
||||
class ActionEditMetadata(InterfaceActionBase):
|
||||
name = 'Edit Metadata'
|
||||
actual_plugin = 'calibre.gui2.actions.edit_metadata:EditMetadataAction'
|
||||
description = _('Edit the metadata of books in your calibre library')
|
||||
|
||||
class ActionView(InterfaceActionBase):
|
||||
name = 'View'
|
||||
actual_plugin = 'calibre.gui2.actions.view:ViewAction'
|
||||
description = _('Read books in your calibre library')
|
||||
|
||||
class ActionFetchNews(InterfaceActionBase):
|
||||
name = 'Fetch News'
|
||||
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):
|
||||
name = 'Save To Disk'
|
||||
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
|
||||
description = _('Export books from your calibre library to the hard disk')
|
||||
|
||||
class ActionShowBookDetails(InterfaceActionBase):
|
||||
name = 'Show Book Details'
|
||||
actual_plugin = 'calibre.gui2.actions.show_book_details:ShowBookDetailsAction'
|
||||
description = _('Show book details in a separate popup')
|
||||
|
||||
class ActionRestart(InterfaceActionBase):
|
||||
name = 'Restart'
|
||||
actual_plugin = 'calibre.gui2.actions.restart:RestartAction'
|
||||
description = _('Restart calibre')
|
||||
|
||||
class ActionOpenFolder(InterfaceActionBase):
|
||||
name = 'Open Folder'
|
||||
actual_plugin = 'calibre.gui2.actions.open:OpenFolderAction'
|
||||
description = _('Open the folder that contains the book files in your'
|
||||
' calibre library')
|
||||
|
||||
class ActionSendToDevice(InterfaceActionBase):
|
||||
name = 'Send To Device'
|
||||
actual_plugin = 'calibre.gui2.actions.device:SendToDeviceAction'
|
||||
description = _('Send books to the connected device')
|
||||
|
||||
class ActionConnectShare(InterfaceActionBase):
|
||||
name = 'Connect Share'
|
||||
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):
|
||||
name = 'Help'
|
||||
actual_plugin = 'calibre.gui2.actions.help:HelpAction'
|
||||
description = _('Browse the calibre User Manual')
|
||||
|
||||
class ActionPreferences(InterfaceActionBase):
|
||||
name = 'Preferences'
|
||||
actual_plugin = 'calibre.gui2.actions.preferences:PreferencesAction'
|
||||
description = _('Customize calibre')
|
||||
|
||||
class ActionSimilarBooks(InterfaceActionBase):
|
||||
name = 'Similar Books'
|
||||
actual_plugin = 'calibre.gui2.actions.similar_books:SimilarBooksAction'
|
||||
description = _('Easily find books similar to the currently selected one')
|
||||
|
||||
class ActionChooseLibrary(InterfaceActionBase):
|
||||
name = 'Choose Library'
|
||||
actual_plugin = 'calibre.gui2.actions.choose_library:ChooseLibraryAction'
|
||||
description = _('Switch between different calibre libraries and perform'
|
||||
' maintenance on them')
|
||||
|
||||
class ActionAddToLibrary(InterfaceActionBase):
|
||||
name = 'Add To Library'
|
||||
actual_plugin = 'calibre.gui2.actions.add_to_library:AddToLibraryAction'
|
||||
description = _('Copy books from the devce to your calibre library')
|
||||
|
||||
class ActionEditCollections(InterfaceActionBase):
|
||||
name = 'Edit Collections'
|
||||
actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction'
|
||||
description = _('Edit the collections in which books are placed on your device')
|
||||
|
||||
class ActionCopyToLibrary(InterfaceActionBase):
|
||||
name = 'Copy To Library'
|
||||
actual_plugin = 'calibre.gui2.actions.copy_to_library:CopyToLibraryAction'
|
||||
description = _('Copy a book from one calibre library to another')
|
||||
|
||||
class ActionTweakEpub(InterfaceActionBase):
|
||||
name = 'Tweak ePub'
|
||||
actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction'
|
||||
description = _('Make small twekas to epub files in your calibre library')
|
||||
|
||||
class ActionNextMatch(InterfaceActionBase):
|
||||
name = 'Next Match'
|
||||
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):
|
||||
name = 'Store'
|
||||
author = 'John Schember'
|
||||
actual_plugin = 'calibre.gui2.actions.store:StoreAction'
|
||||
description = _('Search for books from different book sellers')
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Customize the behavior of the store search.'
|
||||
@ -870,13 +903,13 @@ class ActionStore(InterfaceActionBase):
|
||||
class ActionPluginUpdater(InterfaceActionBase):
|
||||
name = 'Plugin Updater'
|
||||
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'
|
||||
|
||||
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
||||
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
|
||||
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||
ActionFetchNews, ActionSaveToDisk, ActionQuickview,
|
||||
ActionShowBookDetails,ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore,
|
||||
@ -1307,6 +1340,16 @@ class StoreLegimiStore(StoreBase):
|
||||
headquarters = 'PL'
|
||||
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):
|
||||
name = 'ManyBooks'
|
||||
description = u'Public domain and creative commons works from many sources.'
|
||||
@ -1457,6 +1500,7 @@ plugins += [
|
||||
StoreGutenbergStore,
|
||||
StoreKoboStore,
|
||||
StoreLegimiStore,
|
||||
StoreLibreDEStore,
|
||||
StoreManyBooksStore,
|
||||
StoreMobileReadStore,
|
||||
StoreNextoStore,
|
||||
|
@ -259,6 +259,10 @@ class OutputFormatPlugin(Plugin):
|
||||
#: (option_name, recommended_value, recommendation_level)
|
||||
recommendations = set([])
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return _('Convert ebooks to the %s format'%self.file_type)
|
||||
|
||||
def __init__(self, *args):
|
||||
Plugin.__init__(self, *args)
|
||||
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
|
||||
FORMATS = ['epub', 'pdf']
|
||||
USER_CAN_ADD_NEW_FORMATS = False
|
||||
KEEP_TEMP_FILES_AFTER_UPLOAD = True
|
||||
|
||||
# Hide the standard customization widgets
|
||||
SUPPORTS_SUB_DIRS = False
|
||||
|
@ -327,12 +327,7 @@ class DevicePlugin(Plugin):
|
||||
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".
|
||||
|
||||
:param files: A list of paths and/or file-like objects. If they are paths and
|
||||
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 files: A list of paths
|
||||
:param names: A list of file names that the books should have
|
||||
once uploaded to the device. len(names) == len(files)
|
||||
: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):
|
||||
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
|
||||
try:
|
||||
lpath = path.partition(self.normalize_path(prefix))[2]
|
||||
@ -111,12 +111,23 @@ class KOBO(USBMS):
|
||||
|
||||
playlist_map = {}
|
||||
|
||||
if lpath not in playlist_map:
|
||||
playlist_map[lpath] = []
|
||||
|
||||
if readstatus == 1:
|
||||
playlist_map[lpath]= "Im_Reading"
|
||||
playlist_map[lpath].append('Im_Reading')
|
||||
elif readstatus == 2:
|
||||
playlist_map[lpath]= "Read"
|
||||
playlist_map[lpath].append('Read')
|
||||
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)
|
||||
# print "Normalized FileName: " + path
|
||||
@ -126,7 +137,13 @@ class KOBO(USBMS):
|
||||
bl_cache[lpath] = None
|
||||
if ImageID is not None:
|
||||
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
|
||||
if not os.path.exists(imagename):
|
||||
debug_print("Strange - The image name does not exist - title: ", title)
|
||||
if imagename is not None:
|
||||
bl[idx].thumbnail = ImageWrapper(imagename)
|
||||
if (ContentType != '6' and MimeType != 'Shortcover'):
|
||||
@ -138,7 +155,7 @@ class KOBO(USBMS):
|
||||
debug_print(" Strange: The file: ", prefix, lpath, " does mot exist!")
|
||||
if lpath in playlist_map and \
|
||||
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:
|
||||
if ContentType == '6' and MimeType == 'Shortcover':
|
||||
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
|
||||
@ -157,7 +174,7 @@ class KOBO(USBMS):
|
||||
raise
|
||||
|
||||
# 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):
|
||||
changed = True
|
||||
@ -186,24 +203,30 @@ class KOBO(USBMS):
|
||||
result = cursor.fetchone()
|
||||
self.dbversion = result[0]
|
||||
|
||||
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
|
||||
'ImageID, ReadStatus from content where BookID is Null'
|
||||
if self.dbversion >= 14:
|
||||
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
|
||||
'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)
|
||||
|
||||
changed = False
|
||||
for i, row in enumerate(cursor):
|
||||
# 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)
|
||||
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
|
||||
# debug_print("mime:", mime)
|
||||
|
||||
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
|
||||
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:
|
||||
need_sync = True
|
||||
@ -267,8 +290,12 @@ class KOBO(USBMS):
|
||||
cursor.execute('delete from content_keys where volumeid = ?', t)
|
||||
|
||||
# Delete the chapters associated with the book next
|
||||
t = (ContentID,ContentID,)
|
||||
cursor.execute('delete from content where BookID = ? or ContentID = ?', t)
|
||||
t = (ContentID,)
|
||||
# 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()
|
||||
|
||||
@ -286,7 +313,7 @@ class KOBO(USBMS):
|
||||
path_prefix = '.kobo/images/'
|
||||
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:
|
||||
fpath = path + ending
|
||||
@ -450,7 +477,10 @@ class KOBO(USBMS):
|
||||
path = self._main_prefix + path + '.kobo'
|
||||
# print "Path: " + path
|
||||
elif (ContentType == "6" or ContentType == "10") and MimeType == 'application/x-kobo-epub+zip':
|
||||
path = self._main_prefix + '.kobo/kepub/' + path
|
||||
if path.startswith("file:///mnt/onboard/"):
|
||||
path = self._main_prefix + path.replace("file:///mnt/onboard/", '')
|
||||
else:
|
||||
path = self._main_prefix + '.kobo/kepub/' + path
|
||||
# print "Internal: " + path
|
||||
else:
|
||||
# if path.startswith("file:///mnt/onboard/"):
|
||||
|
@ -26,6 +26,10 @@ class Epubcheck(ePubFixer):
|
||||
'significant changes to your epub, complain to the epubcheck '
|
||||
'project.')
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.long_description
|
||||
|
||||
@property
|
||||
def fix_name(self):
|
||||
return 'epubcheck'
|
||||
|
@ -22,6 +22,10 @@ class Unmanifested(ePubFixer):
|
||||
'the manifest or delete them as specified by the '
|
||||
'delete unmanifested option.')
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.long_description
|
||||
|
||||
@property
|
||||
def fix_name(self):
|
||||
return 'unmanifested'
|
||||
|
@ -15,7 +15,7 @@ from calibre.utils.ipc.server import Server
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory
|
||||
from calibre import prints, isbytestring
|
||||
from calibre.constants import filesystem_encoding
|
||||
|
||||
from calibre.db.errors import NoSuchFormat
|
||||
|
||||
def debug(*args):
|
||||
prints(*args)
|
||||
@ -201,27 +201,35 @@ class SaveWorker(Thread):
|
||||
self.spare_server = spare_server
|
||||
self.start()
|
||||
|
||||
def collect_data(self, ids):
|
||||
def collect_data(self, ids, tdir):
|
||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||
data = {}
|
||||
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)
|
||||
if isbytestring(opf):
|
||||
opf = opf.decode('utf-8')
|
||||
cpath = None
|
||||
if mi.cover:
|
||||
cpath = mi.cover
|
||||
if mi.cover_data and mi.cover_data[1]:
|
||||
cpath = os.path.join(tdir, 'cover_%s.jpg'%i)
|
||||
with lopen(cpath, 'wb') as f:
|
||||
f.write(mi.cover_data[1])
|
||||
if isbytestring(cpath):
|
||||
cpath = cpath.decode(filesystem_encoding)
|
||||
formats = {}
|
||||
if mi.formats:
|
||||
for fmt in mi.formats:
|
||||
fpath = self.db.format_abspath(i, fmt, index_is_id=True)
|
||||
if fpath is not None:
|
||||
if isbytestring(fpath):
|
||||
fpath = fpath.decode(filesystem_encoding)
|
||||
formats[fmt.lower()] = fpath
|
||||
fpath = os.path.join(tdir, 'fmt_%s.%s'%(i, fmt.lower()))
|
||||
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):
|
||||
fpath = fpath.decode(filesystem_encoding)
|
||||
formats[fmt.lower()] = fpath
|
||||
data[i] = [opf, cpath, formats, mi.last_modified.isoformat()]
|
||||
return data
|
||||
|
||||
@ -244,7 +252,7 @@ class SaveWorker(Thread):
|
||||
|
||||
for i, task in enumerate(tasks):
|
||||
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)
|
||||
with open(dpath, 'wb') as f:
|
||||
f.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
||||
|
@ -53,13 +53,18 @@ class Worker(Thread): # {{{
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
newdb = LibraryDatabase2(self.loc)
|
||||
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)
|
||||
fmts = self.db.formats(x, index_is_id=True)
|
||||
if not fmts: fmts = []
|
||||
else: fmts = fmts.split(',')
|
||||
paths = [self.db.format_abspath(x, fmt, index_is_id=True) for fmt in
|
||||
fmts]
|
||||
paths = []
|
||||
for fmt in fmts:
|
||||
p = self.db.format(x, fmt, index_is_id=True,
|
||||
as_path=True)
|
||||
if p:
|
||||
paths.append(p)
|
||||
added = False
|
||||
if prefs['add_formats_to_existing']:
|
||||
identical_book_list = newdb.find_identical_books(mi)
|
||||
@ -75,6 +80,11 @@ class Worker(Thread): # {{{
|
||||
if co is not None:
|
||||
newdb.set_conversion_options(x, 'PIPE', co)
|
||||
self.processed.add(x)
|
||||
for path in paths:
|
||||
try:
|
||||
os.remove(path)
|
||||
except:
|
||||
pass
|
||||
# }}}
|
||||
|
||||
class CopyToLibraryAction(InterfaceAction):
|
||||
|
@ -17,6 +17,7 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.db.errors import NoSuchFormat
|
||||
|
||||
class EditMetadataAction(InterfaceAction):
|
||||
|
||||
@ -265,7 +266,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
+'</p>', 'merge_too_many_books', self.gui):
|
||||
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)
|
||||
if safe_merge:
|
||||
if not confirm('<p>'+_(
|
||||
@ -277,7 +278,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
'Please confirm you want to proceed.')%title
|
||||
+'</p>', 'merge_books_safe', self.gui):
|
||||
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)
|
||||
elif merge_only_formats:
|
||||
if not confirm('<p>'+_(
|
||||
@ -293,7 +294,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
'Are you <b>sure</b> you want to proceed?')%title
|
||||
+'</p>', 'merge_only_formats', self.gui):
|
||||
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)
|
||||
else:
|
||||
if not confirm('<p>'+_(
|
||||
@ -308,7 +309,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
'Are you <b>sure</b> you want to proceed?')%title
|
||||
+'</p>', 'merge_books', self.gui):
|
||||
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.delete_books_after_merge(src_ids)
|
||||
# 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,
|
||||
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):
|
||||
src_books = []
|
||||
src_ids = []
|
||||
m = self.gui.library_view.model()
|
||||
for i, row in enumerate(rows):
|
||||
@ -339,22 +354,19 @@ class EditMetadataAction(InterfaceAction):
|
||||
dest_id = id_
|
||||
else:
|
||||
src_ids.append(id_)
|
||||
dbfmts = m.db.formats(id_, index_is_id=True)
|
||||
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]
|
||||
return [dest_id, src_ids]
|
||||
|
||||
def delete_books_after_merge(self, ids_to_delete):
|
||||
self.gui.library_view.model().delete_books_by_id(ids_to_delete)
|
||||
|
||||
def merge_metadata(self, dest_id, src_ids):
|
||||
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
|
||||
dest_cover = db.cover(dest_id, index_is_id=True)
|
||||
had_orig_cover = bool(dest_cover)
|
||||
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 not dest_mi.comments:
|
||||
dest_mi.comments = src_mi.comments
|
||||
@ -372,8 +384,10 @@ class EditMetadataAction(InterfaceAction):
|
||||
dest_mi.tags = src_mi.tags
|
||||
else:
|
||||
dest_mi.tags.extend(src_mi.tags)
|
||||
if src_mi.cover and not dest_mi.cover:
|
||||
dest_mi.cover = src_mi.cover
|
||||
if not dest_cover:
|
||||
src_cover = db.cover(src_id, index_is_id=True)
|
||||
if src_cover:
|
||||
dest_cover = src_cover
|
||||
if not dest_mi.publisher:
|
||||
dest_mi.publisher = src_mi.publisher
|
||||
if not dest_mi.rating:
|
||||
@ -382,6 +396,8 @@ class EditMetadataAction(InterfaceAction):
|
||||
dest_mi.series = src_mi.series
|
||||
dest_mi.series_index = src_mi.series_index
|
||||
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
|
||||
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>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.gui2.dialogs.tweak_epub import TweakEpub
|
||||
@ -30,8 +32,8 @@ class TweakEpubAction(InterfaceAction):
|
||||
# Confirm 'EPUB' in formats
|
||||
book_id = self.gui.library_view.model().id(row)
|
||||
try:
|
||||
path_to_epub = self.gui.library_view.model().db.format_abspath(
|
||||
book_id, 'EPUB', index_is_id=True)
|
||||
path_to_epub = self.gui.library_view.model().db.format(
|
||||
book_id, 'EPUB', index_is_id=True, as_path=True)
|
||||
except:
|
||||
path_to_epub = None
|
||||
|
||||
@ -45,6 +47,7 @@ class TweakEpubAction(InterfaceAction):
|
||||
if dlg.exec_() == dlg.Accepted:
|
||||
self.update_db(book_id, dlg._output)
|
||||
dlg.cleanup()
|
||||
os.remove(path_to_epub)
|
||||
|
||||
def update_db(self, book_id, rebuilt):
|
||||
'''
|
||||
|
@ -445,6 +445,7 @@ class Saver(QObject): # {{{
|
||||
self.pd.setModal(True)
|
||||
self.pd.show()
|
||||
self.pd.set_min(0)
|
||||
self.pd.set_msg(_('Collecting data, please wait...'))
|
||||
self._parent = parent
|
||||
self.callback = callback
|
||||
self.callback_called = False
|
||||
|
@ -4,7 +4,7 @@ __license__ = 'GPL 3'
|
||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re
|
||||
import re, os
|
||||
|
||||
from PyQt4.QtCore import SIGNAL, Qt, pyqtSignal
|
||||
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.'),
|
||||
show=True)
|
||||
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
|
||||
|
||||
def open_book(self, pathtoebook):
|
||||
|
@ -106,7 +106,6 @@ class Config(ResizableDialog, Ui_Dialog):
|
||||
Configuration dialog for single book conversion. If accepted, has the
|
||||
following important attributes
|
||||
|
||||
input_path - Path to input file
|
||||
output_format - Output format (without a leading .)
|
||||
input_format - Input format (without a leading .)
|
||||
opf_path - Path to OPF file with user specified metadata
|
||||
@ -156,13 +155,10 @@ class Config(ResizableDialog, Ui_Dialog):
|
||||
oidx = self.groups.currentIndex().row()
|
||||
input_format = self.input_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
|
||||
log = Log()
|
||||
log.outputs = []
|
||||
self.plumber = Plumber(input_path, output_path, log)
|
||||
self.plumber = Plumber('dummy.'+input_format, output_path, log)
|
||||
|
||||
def widget_factory(cls):
|
||||
return cls(self.stack, self.plumber.get_option_by_name,
|
||||
|
@ -49,16 +49,26 @@ class DeviceJob(BaseJob): # {{{
|
||||
self._aborted = False
|
||||
|
||||
def start_work(self):
|
||||
if DEBUG:
|
||||
prints('Job:', self.id, self.description, 'started',
|
||||
safe_encode=True)
|
||||
self.start_time = time.time()
|
||||
self.job_manager.changed_queue.put(self)
|
||||
|
||||
def job_done(self):
|
||||
self.duration = time.time() - self.start_time
|
||||
self.percent = 1
|
||||
if DEBUG:
|
||||
prints('DeviceJob:', self.id, self.description,
|
||||
'done, calling callback', safe_encode=True)
|
||||
|
||||
try:
|
||||
self.callback_on_done(self)
|
||||
except:
|
||||
pass
|
||||
if DEBUG:
|
||||
prints('DeviceJob:', self.id, self.description,
|
||||
'callback returned', safe_encode=True)
|
||||
self.job_manager.changed_queue.put(self)
|
||||
|
||||
def report_progress(self, percent, msg=''):
|
||||
@ -386,8 +396,17 @@ class DeviceManager(Thread): # {{{
|
||||
if DEBUG:
|
||||
prints(traceback.format_exc(), file=sys.__stdout__)
|
||||
|
||||
return self.device.upload_books(files, names, on_card,
|
||||
metadata=metadata, end_session=False)
|
||||
try:
|
||||
return self.device.upload_books(files, names, on_card,
|
||||
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,
|
||||
metadata=None, plugboards=None, add_as_step_to_job=None):
|
||||
@ -1062,8 +1081,6 @@ class DeviceMixin(object): # {{{
|
||||
'the device?'), autos):
|
||||
self.iactions['Convert Books'].auto_convert_news(auto, format)
|
||||
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:
|
||||
self.news_to_be_synced = set([])
|
||||
return
|
||||
@ -1305,8 +1322,17 @@ class DeviceMixin(object): # {{{
|
||||
self.card_b_view if on_card == 'cardb' else self.memory_view
|
||||
view.model().resort(reset=False)
|
||||
view.model().research()
|
||||
for f in files:
|
||||
getattr(f, 'close', lambda : True)()
|
||||
if files:
|
||||
for f in files:
|
||||
# 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):
|
||||
'''
|
||||
|
@ -24,7 +24,7 @@ from calibre.utils.config import prefs, tweaks
|
||||
from calibre.utils.magick.draw import identify_data
|
||||
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
|
||||
old = prefs['read_file_metadata']
|
||||
if not old:
|
||||
@ -32,8 +32,8 @@ def get_cover_data(path): # {{{
|
||||
cdata = area = None
|
||||
|
||||
try:
|
||||
mi = get_metadata(open(path, 'rb'),
|
||||
os.path.splitext(path)[1][1:].lower())
|
||||
with stream:
|
||||
mi = get_metadata(stream, ext)
|
||||
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||
cdata = open(mi.cover).read()
|
||||
elif mi.cover_data[1] is not None:
|
||||
@ -186,9 +186,10 @@ class MyBlockingBusy(QDialog): # {{{
|
||||
if fmts:
|
||||
covers = []
|
||||
for fmt in fmts.split(','):
|
||||
fmt = self.db.format_abspath(id, fmt, index_is_id=True)
|
||||
if not fmt: continue
|
||||
cdata, area = get_cover_data(fmt)
|
||||
fmtf = self.db.format(id, fmt, index_is_id=True,
|
||||
as_file=True)
|
||||
if fmtf is None: continue
|
||||
cdata, area = get_cover_data(fmtf, fmt)
|
||||
if cdata:
|
||||
covers.append((cdata, area))
|
||||
covers.sort(key=lambda x: x[1])
|
||||
@ -361,7 +362,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
||||
fm = self.db.field_metadata
|
||||
for f in fm:
|
||||
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 f not in ['formats', 'ondevice']) or
|
||||
(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:
|
||||
_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 = [], [], []
|
||||
texts, subjects, attachments, attachment_names = [], [], [], []
|
||||
|
@ -5,8 +5,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import shutil, functools, re, os, traceback
|
||||
from contextlib import closing
|
||||
import functools, re, os, traceback
|
||||
from collections import defaultdict
|
||||
|
||||
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':
|
||||
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
|
||||
|
||||
def default_image():
|
||||
@ -391,10 +382,14 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
data = self.current_changed(index, None, False)
|
||||
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 = []
|
||||
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)
|
||||
return ans
|
||||
|
||||
@ -449,18 +444,14 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
format = f
|
||||
break
|
||||
if format is not None:
|
||||
pt = PersistentTemporaryFile(suffix='.'+format)
|
||||
with closing(self.db.format(id, format, 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 = PersistentTemporaryFile(suffix='caltmpfmt.'+format)
|
||||
self.db.copy_format_to(id, format, pt, index_is_id=True)
|
||||
pt.seek(0)
|
||||
if set_metadata:
|
||||
try:
|
||||
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
|
||||
format)
|
||||
_set_metadata(pt, self.db.get_metadata(
|
||||
id, get_cover=True, index_is_id=True,
|
||||
cover_as_data=True), format)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
pt.close()
|
||||
@ -468,9 +459,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
if isbytestring(x):
|
||||
x = x.decode(filesystem_encoding)
|
||||
return x
|
||||
name, op = map(to_uni, map(os.path.abspath, (pt.name,
|
||||
pt.orig_file_path)))
|
||||
ans.append(FormatPath(name, op))
|
||||
ans.append(to_uni(os.path.abspath(pt.name)))
|
||||
else:
|
||||
need_auto.append(id)
|
||||
if not exclude_auto:
|
||||
@ -499,13 +488,11 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
break
|
||||
if format is not None:
|
||||
pt = PersistentTemporaryFile(suffix='.'+format)
|
||||
with closing(self.db.format(row, format, as_file=True)) as src:
|
||||
shutil.copyfileobj(src, pt)
|
||||
pt.flush()
|
||||
self.db.copy_format_to(id, format, pt, index_is_id=True)
|
||||
pt.seek(0)
|
||||
if set_metadata:
|
||||
_set_metadata(pt, self.db.get_metadata(row, get_cover=True),
|
||||
format)
|
||||
_set_metadata(pt, self.db.get_metadata(row, get_cover=True,
|
||||
cover_as_data=True), format)
|
||||
pt.close() if paths else pt.seek(0)
|
||||
ans.append(pt)
|
||||
else:
|
||||
|
@ -584,14 +584,15 @@ class BooksView(QTableView): # {{{
|
||||
m = self.model()
|
||||
db = m.db
|
||||
rows = self.selectionModel().selectedRows()
|
||||
selected = map(m.id, rows)
|
||||
selected = list(map(m.id, rows))
|
||||
ids = ' '.join(map(str, selected))
|
||||
md = QMimeData()
|
||||
md.setData('application/calibre+from_library', ids)
|
||||
fmt = prefs['output_format']
|
||||
|
||||
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:
|
||||
fmts = db.formats(i, index_is_id=True)
|
||||
if fmts:
|
||||
@ -599,14 +600,13 @@ class BooksView(QTableView): # {{{
|
||||
else:
|
||||
fmts = []
|
||||
for f in fmts:
|
||||
ans = db.format_abspath(i, f, index_is_id=True)
|
||||
if ans is not None:
|
||||
break
|
||||
ans = db.format(i, f, index_is_id=True, as_path=True,
|
||||
preserve_filename=True)
|
||||
if ans is None:
|
||||
ans = db.abspath(i, index_is_id=True)
|
||||
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)
|
||||
col = self.selectionModel().currentIndex().column()
|
||||
md.column_name = self.column_map[col]
|
||||
|
@ -688,7 +688,8 @@ class FormatsManager(QWidget): # {{{
|
||||
else:
|
||||
stream = open(fmt.path, 'r+b')
|
||||
try:
|
||||
mi = get_metadata(stream, ext)
|
||||
with stream:
|
||||
mi = get_metadata(stream, ext)
|
||||
return mi, ext
|
||||
except:
|
||||
error_dialog(self, _('Could not read metadata'),
|
||||
|
@ -51,19 +51,19 @@ class BeamEBooksDEStore(BasicStoreConfig, StorePlugin):
|
||||
if counter <= 0:
|
||||
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:
|
||||
continue
|
||||
id = id[7:]
|
||||
cover_url = ''.join(data.xpath('./tr/td[1]/a/img/@src'))
|
||||
if cover_url:
|
||||
cover_url = 'http://www.beam-ebooks.de' + cover_url
|
||||
title = ''.join(data.xpath('./tr/td/div[@class="stil2"]/a/b/text()'))
|
||||
author = ' '.join(data.xpath('./tr/td/div[@class="stil2"]/'
|
||||
'child::b/text()'
|
||||
'|'
|
||||
'./tr/td/div[@class="stil2"]/'
|
||||
'child::strong/text()'))
|
||||
temp = ''.join(data.xpath('./tr/td[1]/a/img/@alt'))
|
||||
colon = temp.find(':')
|
||||
if not temp.startswith('eBook') or colon < 0:
|
||||
continue
|
||||
author = temp[5:colon]
|
||||
title = temp[colon+1:]
|
||||
price = ''.join(data.xpath('./tr/td[3]/text()'))
|
||||
pdf = data.xpath(
|
||||
'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'))
|
||||
title = ''.join(data.xpath('.//div[@class="item_entries"]/h2/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 = 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'))
|
||||
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 = re.sub('PLN', ' zł', price)
|
||||
price = re.sub('\.', ',', price)
|
||||
|
@ -53,7 +53,7 @@ class ZixoStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
cover_url = ''.join(data.xpath('.//a[@class="productThumb"]/img/@src'))
|
||||
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 = re.sub('\.', ',', price)
|
||||
|
||||
|
@ -51,12 +51,15 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
|
||||
# continue
|
||||
|
||||
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.write(d.output_format)
|
||||
out_file.close()
|
||||
temp_files = []
|
||||
temp_files = [in_file]
|
||||
|
||||
try:
|
||||
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,
|
||||
OptionRecommendation.HIGH))
|
||||
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)
|
||||
jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files))
|
||||
|
||||
@ -142,12 +145,15 @@ class QueueBulk(QProgressDialog):
|
||||
try:
|
||||
input_format = get_input_format_for_book(self.db, book_id, None)[0]
|
||||
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.write(self.output_format)
|
||||
out_file.close()
|
||||
temp_files = []
|
||||
temp_files = [in_file]
|
||||
|
||||
combined_recs = GuiRecommendations()
|
||||
default_recs = bulk_defaults_for_input_format(input_format)
|
||||
@ -183,7 +189,7 @@ class QueueBulk(QProgressDialog):
|
||||
self.setLabelText(_('Queueing ')+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)
|
||||
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)
|
||||
# }}}
|
||||
|
||||
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))
|
||||
|
||||
# If cover exists, use it
|
||||
cpath = None
|
||||
try:
|
||||
search_text = 'title:"%s" author:%s' % (
|
||||
opts.catalog_title.replace('"', '\\"'), 'calibre')
|
||||
@ -5157,5 +5158,10 @@ Author '{0}':
|
||||
plumber.merge_ui_recommendations(recommendations)
|
||||
plumber.run()
|
||||
|
||||
try:
|
||||
os.remove(cpath)
|
||||
except:
|
||||
pass
|
||||
|
||||
# returns to gui2.actions.catalog:catalog_generated()
|
||||
return catalog.error
|
||||
|
@ -12,8 +12,6 @@ import threading, random
|
||||
from itertools import repeat
|
||||
from math import ceil
|
||||
|
||||
from PyQt4.QtGui import QImage
|
||||
|
||||
from calibre import prints
|
||||
from calibre.ebooks.metadata import (title_sort, author_to_author_sort,
|
||||
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.ebooks.metadata.book.base import Metadata
|
||||
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 import isbytestring
|
||||
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.recycle_bin import delete_file, delete_tree
|
||||
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
|
||||
SPOOL_SIZE = 30*1024*1024
|
||||
|
||||
class Tag(object):
|
||||
|
||||
@ -201,16 +201,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
dbprefs = DBPrefs(self)
|
||||
for key in default_prefs:
|
||||
# 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
|
||||
try:
|
||||
dbprefs[key] = default_prefs[key]
|
||||
except:
|
||||
pass # ignore options that don't exist anymore
|
||||
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'])
|
||||
dbprefs[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'])
|
||||
self.initialize_dynamic()
|
||||
|
||||
def get_property(self, idx, index_is_id=False, loc=-1):
|
||||
@ -603,14 +601,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
with lopen(os.path.join(tpath, 'cover.jpg'), 'wb') as f:
|
||||
f.write(cdata)
|
||||
for format in formats:
|
||||
# Get data as string (can't use file as source and target files may be the same)
|
||||
f = self.format(id, format, index_is_id=True, as_file=True)
|
||||
if f is None:
|
||||
continue
|
||||
with tempfile.SpooledTemporaryFile(max_size=30*(1024**2)) as stream:
|
||||
with f:
|
||||
shutil.copyfileobj(f, stream)
|
||||
stream.seek(0)
|
||||
with tempfile.SpooledTemporaryFile(max_size=SPOOL_SIZE) as stream:
|
||||
try:
|
||||
self.copy_format_to(id, format, stream, index_is_id=True)
|
||||
stream.seek(0)
|
||||
except NoSuchFormat:
|
||||
continue
|
||||
self.add_format(id, format, stream, index_is_id=True,
|
||||
path=tpath, notify=False)
|
||||
self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id))
|
||||
@ -663,32 +659,53 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
continue
|
||||
|
||||
def cover(self, index, index_is_id=False, as_file=False, as_image=False,
|
||||
as_path=False):
|
||||
as_path=False):
|
||||
'''
|
||||
Return the cover image as a bytestring (in JPEG format) or None.
|
||||
|
||||
`as_file` : If True return the image as an open file object
|
||||
`as_image`: If True return the image as a QImage object
|
||||
WARNING: Using as_path will copy the cover to a temp file and return
|
||||
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')
|
||||
if os.access(path, os.R_OK):
|
||||
if as_path:
|
||||
return path
|
||||
try:
|
||||
f = lopen(path, 'rb')
|
||||
except (IOError, OSError):
|
||||
time.sleep(0.2)
|
||||
f = lopen(path, 'rb')
|
||||
if as_image:
|
||||
img = QImage()
|
||||
img.loadFromData(f.read())
|
||||
f.close()
|
||||
return img
|
||||
ans = f if as_file else f.read()
|
||||
if ans is not f:
|
||||
f.close()
|
||||
return ans
|
||||
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:
|
||||
from PyQt4.Qt import QImage
|
||||
i = QImage()
|
||||
i.loadFromData(ret)
|
||||
ret = i
|
||||
return ret
|
||||
|
||||
def cover_last_modified(self, index, index_is_id=False):
|
||||
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.
|
||||
|
||||
@ -861,7 +878,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
return (path, mi, sequence)
|
||||
|
||||
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.
|
||||
Note that the list of formats is not verified.
|
||||
@ -936,7 +953,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
mi.user_categories = user_cat_vals
|
||||
|
||||
if get_cover:
|
||||
mi.cover = self.cover(id, index_is_id=True, as_path=True)
|
||||
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)
|
||||
return mi
|
||||
|
||||
def has_book(self, mi):
|
||||
@ -1101,7 +1123,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
return utcfromtimestamp(os.stat(path).st_mtime)
|
||||
|
||||
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)
|
||||
try:
|
||||
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)
|
||||
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,
|
||||
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)
|
||||
if path is not None:
|
||||
f = lopen(path, mode)
|
||||
try:
|
||||
ret = f if as_file else f.read()
|
||||
except IOError:
|
||||
f.seek(0)
|
||||
out = cStringIO.StringIO()
|
||||
shutil.copyfileobj(f, out)
|
||||
ret = out.getvalue()
|
||||
if not as_file:
|
||||
f.close()
|
||||
with lopen(path, mode) as f:
|
||||
if as_path:
|
||||
if preserve_filename:
|
||||
bd = base_dir()
|
||||
d = os.path.join(bd, 'format_abspath')
|
||||
try:
|
||||
os.makedirs(d)
|
||||
except:
|
||||
pass
|
||||
fname = os.path.basename(path)
|
||||
ret = os.path.join(d, fname)
|
||||
with lopen(ret, 'wb') as f2:
|
||||
shutil.copyfileobj(f, f2)
|
||||
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
|
||||
|
||||
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):
|
||||
id = id if index_is_id else self.id(id)
|
||||
aut_strings = self.conn.get('''
|
||||
SELECT authors.name, authors.sort
|
||||
SELECT authors.id, authors.name, authors.sort
|
||||
FROM authors, books_authors_link as bl
|
||||
WHERE bl.book=? and authors.id=bl.author
|
||||
ORDER BY bl.id''', (id,))
|
||||
result = []
|
||||
for (author, sort,) in aut_strings:
|
||||
result.append((author.replace('|', ','), sort))
|
||||
for (id_, author, sort,) in aut_strings:
|
||||
result.append((id_, author.replace('|', ','), sort))
|
||||
return result
|
||||
|
||||
# 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)
|
||||
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,
|
||||
# preferring the author sort associated with the author over the computed
|
||||
# string
|
||||
@ -1968,7 +2048,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
|
||||
aum = self.authors_with_sort_strings(id_, index_is_id=True)
|
||||
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)
|
||||
|
||||
def _set_authors(self, id, authors, allow_case_change=False):
|
||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, traceback, cStringIO, re, shutil
|
||||
import os, traceback, cStringIO, re
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
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):
|
||||
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', {})
|
||||
|
||||
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:
|
||||
fmts = fmts.split(',')
|
||||
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:
|
||||
formats[fmt.lower()] = fpath
|
||||
|
||||
return do_save_book_to_disk(id_, mi, cover, plugboards,
|
||||
try:
|
||||
return do_save_book_to_disk(id_, mi, cover, plugboards,
|
||||
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,
|
||||
@ -289,10 +297,9 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
|
||||
raise
|
||||
|
||||
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(cover, 'rb') as s:
|
||||
shutil.copyfileobj(s, f)
|
||||
f.write(cover)
|
||||
mi.cover = base_name+'.jpg'
|
||||
else:
|
||||
mi.cover = None
|
||||
@ -395,8 +402,13 @@ def save_serialized_to_disk(ids, data, plugboards, root, opts, callback):
|
||||
pass
|
||||
tb = ''
|
||||
try:
|
||||
failed, id, title = do_save_book_to_disk(x, mi, cover, plugboards,
|
||||
format_map, root, opts, length)
|
||||
with open(cover, 'rb') as f:
|
||||
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')
|
||||
except:
|
||||
failed, id, title = True, x, mi.title
|
||||
|
@ -10,12 +10,13 @@ import re, os, posixpath
|
||||
import cherrypy
|
||||
|
||||
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.save_to_disk import find_plugboard
|
||||
|
||||
from calibre.utils.magick.draw import save_cover_data_to, Image, \
|
||||
thumbnail as generate_thumbnail
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre.utils.magick.draw import (save_cover_data_to, Image,
|
||||
thumbnail as generate_thumbnail)
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
|
||||
plugboard_content_server_value = 'content_server'
|
||||
plugboard_content_server_formats = ['epub']
|
||||
@ -46,7 +47,7 @@ class ContentServer(object):
|
||||
# Utility methods {{{
|
||||
def last_modified(self, updated):
|
||||
'''
|
||||
Generates a local independent, english timestamp from a datetime
|
||||
Generates a locale independent, english timestamp from a datetime
|
||||
object
|
||||
'''
|
||||
lm = updated.strftime('day, %d month %Y %H:%M:%S GMT')
|
||||
@ -151,14 +152,12 @@ class ContentServer(object):
|
||||
try:
|
||||
cherrypy.response.headers['Content-Type'] = 'image/jpeg'
|
||||
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:
|
||||
cover = self.default_cover
|
||||
updated = self.build_time
|
||||
else:
|
||||
with cover as f:
|
||||
updated = fromtimestamp(os.fstat(f.fileno()).st_mtime)
|
||||
cover = f.read()
|
||||
updated = self.db.cover_last_modified(id, index_is_id=True)
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||
|
||||
if thumbnail:
|
||||
@ -187,9 +186,9 @@ class ContentServer(object):
|
||||
mode='rb')
|
||||
if fmt is None:
|
||||
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':
|
||||
# Get the original metadata
|
||||
mi = self.db.get_metadata(id, index_is_id=True)
|
||||
|
||||
# Get any EPUB plugboards for the content server
|
||||
plugboards = self.db.prefs.get('plugboards', {})
|
||||
@ -203,24 +202,22 @@ class ContentServer(object):
|
||||
newmi = mi
|
||||
|
||||
# Write the updated file
|
||||
from tempfile import TemporaryFile
|
||||
from calibre.ebooks.metadata.meta import set_metadata
|
||||
raw = fmt.read()
|
||||
fmt = TemporaryFile()
|
||||
fmt.write(raw)
|
||||
fmt.seek(0)
|
||||
set_metadata(fmt, newmi, 'epub')
|
||||
fmt.seek(0)
|
||||
|
||||
mt = guess_type('dummy.'+format.lower())[0]
|
||||
if mt is None:
|
||||
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-Disposition'] = \
|
||||
b'attachment; filename="%s"'%fname
|
||||
cherrypy.response.timeout = 3600
|
||||
path = getattr(fmt, 'name', None)
|
||||
if path and os.path.exists(path):
|
||||
updated = fromtimestamp(os.stat(path).st_mtime)
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(utcnow())
|
||||
return fmt
|
||||
# }}}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user