Merge from trunk

This commit is contained in:
Charles Haley 2011-06-22 08:13:55 +01:00
commit eacb8160ed
28 changed files with 480 additions and 198 deletions

View 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;}
'''

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -802,6 +802,7 @@ class ActionFetchNews(InterfaceActionBase):
class ActionQuickview(InterfaceActionBase): class ActionQuickview(InterfaceActionBase):
name = 'Show Quickview' name = 'Show Quickview'
actual_plugin = 'calibre.gui2.actions.show_quickview:ShowQuickviewAction' actual_plugin = 'calibre.gui2.actions.show_quickview:ShowQuickviewAction'
description = _('Show a list of related books quickly')
class ActionSaveToDisk(InterfaceActionBase): class ActionSaveToDisk(InterfaceActionBase):
name = 'Save To Disk' name = 'Save To Disk'

View File

@ -222,7 +222,9 @@ class DB(object, SchemaUpgrade):
if self.user_version == 0: if self.user_version == 0:
self.initialize_database() self.initialize_database()
with self.conn:
SchemaUpgrade.__init__(self) SchemaUpgrade.__init__(self)
# Guarantee that the library_id is set # Guarantee that the library_id is set
self.library_id self.library_id

13
src/calibre/db/errors.py Normal file
View 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

View File

@ -107,6 +107,7 @@ class DriverBase(DeviceConfig, DevicePlugin):
# Needed for config_widget to work # Needed for config_widget to work
FORMATS = ['epub', 'pdf'] FORMATS = ['epub', 'pdf']
USER_CAN_ADD_NEW_FORMATS = False USER_CAN_ADD_NEW_FORMATS = False
KEEP_TEMP_FILES_AFTER_UPLOAD = True
# Hide the standard customization widgets # Hide the standard customization widgets
SUPPORTS_SUB_DIRS = False SUPPORTS_SUB_DIRS = False

View File

@ -327,12 +327,7 @@ class DevicePlugin(Plugin):
free space on the device. The text of the FreeSpaceError must contain the free space on the device. The text of the FreeSpaceError must contain the
word "card" if ``on_card`` is not None otherwise it must contain the word "memory". word "card" if ``on_card`` is not None otherwise it must contain the word "memory".
:param files: A list of paths and/or file-like objects. If they are paths and :param files: A list of paths
the paths point to temporary files, they may have an additional
attribute, original_file_path pointing to the originals. They may have
another optional attribute, deleted_after_upload which if True means
that the file pointed to by original_file_path will be deleted after
being uploaded to the device.
:param names: A list of file names that the books should have :param names: A list of file names that the books should have
once uploaded to the device. len(names) == len(files) once uploaded to the device. len(names) == len(files)
:param metadata: If not None, it is a list of :class:`Metadata` objects. :param metadata: If not None, it is a list of :class:`Metadata` objects.

View File

@ -100,7 +100,7 @@ class KOBO(USBMS):
for idx,b in enumerate(bl): for idx,b in enumerate(bl):
bl_cache[b.lpath] = idx bl_cache[b.lpath] = idx
def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType, expired): def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType, expired, favouritesindex):
changed = False changed = False
try: try:
lpath = path.partition(self.normalize_path(prefix))[2] lpath = path.partition(self.normalize_path(prefix))[2]
@ -111,17 +111,23 @@ class KOBO(USBMS):
playlist_map = {} playlist_map = {}
if lpath not in playlist_map:
playlist_map[lpath] = []
if readstatus == 1: if readstatus == 1:
playlist_map[lpath]= "Im_Reading" playlist_map[lpath].append('Im_Reading')
elif readstatus == 2: elif readstatus == 2:
playlist_map[lpath]= "Read" playlist_map[lpath].append('Read')
elif readstatus == 3: elif readstatus == 3:
playlist_map[lpath]= "Closed" playlist_map[lpath].append('Closed')
# Related to a bug in the Kobo firmware that leaves an expired row for deleted books # 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 # this shows an expired Collection so the user can decide to delete the book
if expired == 3: if expired == 3:
playlist_map[lpath] = "Expired" playlist_map[lpath].append('Expired')
# Favourites are supported on the touch but the data field is there on most earlier models
if favouritesindex == 1:
playlist_map[lpath].append('Favourite')
path = self.normalize_path(path) path = self.normalize_path(path)
# print "Normalized FileName: " + path # print "Normalized FileName: " + path
@ -149,7 +155,7 @@ class KOBO(USBMS):
debug_print(" Strange: The file: ", prefix, lpath, " does mot exist!") debug_print(" Strange: The file: ", prefix, lpath, " does mot exist!")
if lpath in playlist_map and \ if lpath in playlist_map and \
playlist_map[lpath] not in bl[idx].device_collections: playlist_map[lpath] not in bl[idx].device_collections:
bl[idx].device_collections.append(playlist_map[lpath]) bl[idx].device_collections = playlist_map.get(lpath,[])
else: else:
if ContentType == '6' and MimeType == 'Shortcover': if ContentType == '6' and MimeType == 'Shortcover':
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576) book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
@ -168,7 +174,7 @@ class KOBO(USBMS):
raise raise
# print 'Update booklist' # print 'Update booklist'
book.device_collections = [playlist_map[lpath]] if lpath in playlist_map else [] book.device_collections = playlist_map.get(lpath,[])# if lpath in playlist_map else []
if bl.add_book(book, replace_metadata=False): if bl.add_book(book, replace_metadata=False):
changed = True changed = True
@ -197,8 +203,12 @@ class KOBO(USBMS):
result = cursor.fetchone() result = cursor.fetchone()
self.dbversion = result[0] self.dbversion = result[0]
if self.dbversion >= 14:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \ query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, ___ExpirationStatus from content where BookID is Null' 'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex from content where BookID is Null'
else:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, ___ExpirationStatus, "-1" as FavouritesIndex from content where BookID is Null'
cursor.execute (query) cursor.execute (query)
@ -213,10 +223,10 @@ class KOBO(USBMS):
# debug_print("mime:", mime) # debug_print("mime:", mime)
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"): if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8]) changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9])
# print "shortbook: " + path # print "shortbook: " + path
elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"): elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8]) changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9])
if changed: if changed:
need_sync = True need_sync = True

View File

@ -15,7 +15,7 @@ from calibre.utils.ipc.server import Server
from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory
from calibre import prints, isbytestring from calibre import prints, isbytestring
from calibre.constants import filesystem_encoding from calibre.constants import filesystem_encoding
from calibre.db.errors import NoSuchFormat
def debug(*args): def debug(*args):
prints(*args) prints(*args)
@ -201,24 +201,32 @@ class SaveWorker(Thread):
self.spare_server = spare_server self.spare_server = spare_server
self.start() self.start()
def collect_data(self, ids): def collect_data(self, ids, tdir):
from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ebooks.metadata.opf2 import metadata_to_opf
data = {} data = {}
for i in set(ids): for i in set(ids):
mi = self.db.get_metadata(i, index_is_id=True, get_cover=True) mi = self.db.get_metadata(i, index_is_id=True, get_cover=True,
cover_as_data=True)
opf = metadata_to_opf(mi) opf = metadata_to_opf(mi)
if isbytestring(opf): if isbytestring(opf):
opf = opf.decode('utf-8') opf = opf.decode('utf-8')
cpath = None cpath = None
if mi.cover: if mi.cover_data and mi.cover_data[1]:
cpath = mi.cover cpath = os.path.join(tdir, 'cover_%s.jpg'%i)
with lopen(cpath, 'wb') as f:
f.write(mi.cover_data[1])
if isbytestring(cpath): if isbytestring(cpath):
cpath = cpath.decode(filesystem_encoding) cpath = cpath.decode(filesystem_encoding)
formats = {} formats = {}
if mi.formats: if mi.formats:
for fmt in mi.formats: for fmt in mi.formats:
fpath = self.db.format_abspath(i, fmt, index_is_id=True) fpath = os.path.join(tdir, 'fmt_%s.%s'%(i, fmt.lower()))
if fpath is not None: with lopen(fpath, 'wb') as f:
try:
self.db.copy_format_to(i, fmt, f, index_is_id=True)
except NoSuchFormat:
continue
else:
if isbytestring(fpath): if isbytestring(fpath):
fpath = fpath.decode(filesystem_encoding) fpath = fpath.decode(filesystem_encoding)
formats[fmt.lower()] = fpath formats[fmt.lower()] = fpath
@ -244,7 +252,7 @@ class SaveWorker(Thread):
for i, task in enumerate(tasks): for i, task in enumerate(tasks):
tids = [x[-1] for x in task] tids = [x[-1] for x in task]
data = self.collect_data(tids) data = self.collect_data(tids, tdir)
dpath = os.path.join(tdir, '%d.json'%i) dpath = os.path.join(tdir, '%d.json'%i)
with open(dpath, 'wb') as f: with open(dpath, 'wb') as f:
f.write(json.dumps(data, ensure_ascii=False).encode('utf-8')) f.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))

View File

@ -53,13 +53,18 @@ class Worker(Thread): # {{{
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
newdb = LibraryDatabase2(self.loc) newdb = LibraryDatabase2(self.loc)
for i, x in enumerate(self.ids): for i, x in enumerate(self.ids):
mi = self.db.get_metadata(x, index_is_id=True, get_cover=True) mi = self.db.get_metadata(x, index_is_id=True, get_cover=True,
cover_as_data=True)
self.progress(i, mi.title) self.progress(i, mi.title)
fmts = self.db.formats(x, index_is_id=True) fmts = self.db.formats(x, index_is_id=True)
if not fmts: fmts = [] if not fmts: fmts = []
else: fmts = fmts.split(',') else: fmts = fmts.split(',')
paths = [self.db.format_abspath(x, fmt, index_is_id=True) for fmt in paths = []
fmts] for fmt in fmts:
p = self.db.format(x, fmt, index_is_id=True,
as_path=True)
if p:
paths.append(p)
added = False added = False
if prefs['add_formats_to_existing']: if prefs['add_formats_to_existing']:
identical_book_list = newdb.find_identical_books(mi) identical_book_list = newdb.find_identical_books(mi)
@ -75,6 +80,11 @@ class Worker(Thread): # {{{
if co is not None: if co is not None:
newdb.set_conversion_options(x, 'PIPE', co) newdb.set_conversion_options(x, 'PIPE', co)
self.processed.add(x) self.processed.add(x)
for path in paths:
try:
os.remove(path)
except:
pass
# }}} # }}}
class CopyToLibraryAction(InterfaceAction): class CopyToLibraryAction(InterfaceAction):

View File

@ -17,6 +17,7 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.db.errors import NoSuchFormat
class EditMetadataAction(InterfaceAction): class EditMetadataAction(InterfaceAction):
@ -265,7 +266,7 @@ class EditMetadataAction(InterfaceAction):
+'</p>', 'merge_too_many_books', self.gui): +'</p>', 'merge_too_many_books', self.gui):
return return
dest_id, src_books, src_ids = self.books_to_merge(rows) dest_id, src_ids = self.books_to_merge(rows)
title = self.gui.library_view.model().db.title(dest_id, index_is_id=True) title = self.gui.library_view.model().db.title(dest_id, index_is_id=True)
if safe_merge: if safe_merge:
if not confirm('<p>'+_( if not confirm('<p>'+_(
@ -277,7 +278,7 @@ class EditMetadataAction(InterfaceAction):
'Please confirm you want to proceed.')%title 'Please confirm you want to proceed.')%title
+'</p>', 'merge_books_safe', self.gui): +'</p>', 'merge_books_safe', self.gui):
return return
self.add_formats(dest_id, src_books) self.add_formats(dest_id, self.formats_for_books(rows))
self.merge_metadata(dest_id, src_ids) self.merge_metadata(dest_id, src_ids)
elif merge_only_formats: elif merge_only_formats:
if not confirm('<p>'+_( if not confirm('<p>'+_(
@ -293,7 +294,7 @@ class EditMetadataAction(InterfaceAction):
'Are you <b>sure</b> you want to proceed?')%title 'Are you <b>sure</b> you want to proceed?')%title
+'</p>', 'merge_only_formats', self.gui): +'</p>', 'merge_only_formats', self.gui):
return return
self.add_formats(dest_id, src_books) self.add_formats(dest_id, self.formats_for_books(rows))
self.delete_books_after_merge(src_ids) self.delete_books_after_merge(src_ids)
else: else:
if not confirm('<p>'+_( if not confirm('<p>'+_(
@ -308,7 +309,7 @@ class EditMetadataAction(InterfaceAction):
'Are you <b>sure</b> you want to proceed?')%title 'Are you <b>sure</b> you want to proceed?')%title
+'</p>', 'merge_books', self.gui): +'</p>', 'merge_books', self.gui):
return return
self.add_formats(dest_id, src_books) self.add_formats(dest_id, self.formats_for_books(rows))
self.merge_metadata(dest_id, src_ids) self.merge_metadata(dest_id, src_ids)
self.delete_books_after_merge(src_ids) self.delete_books_after_merge(src_ids)
# leave the selection highlight on first selected book # leave the selection highlight on first selected book
@ -329,8 +330,22 @@ class EditMetadataAction(InterfaceAction):
self.gui.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True, self.gui.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True,
notify=False, replace=replace) notify=False, replace=replace)
def formats_for_books(self, rows):
m = self.gui.library_view.model()
ans = []
for id_ in map(m.id, rows):
dbfmts = m.db.formats(id_, index_is_id=True)
if dbfmts:
for fmt in dbfmts.split(','):
try:
path = m.db.format(id_, fmt, index_is_id=True,
as_path=True)
ans.append(path)
except NoSuchFormat:
continue
return ans
def books_to_merge(self, rows): def books_to_merge(self, rows):
src_books = []
src_ids = [] src_ids = []
m = self.gui.library_view.model() m = self.gui.library_view.model()
for i, row in enumerate(rows): for i, row in enumerate(rows):
@ -339,22 +354,19 @@ class EditMetadataAction(InterfaceAction):
dest_id = id_ dest_id = id_
else: else:
src_ids.append(id_) src_ids.append(id_)
dbfmts = m.db.formats(id_, index_is_id=True) return [dest_id, src_ids]
if dbfmts:
for fmt in dbfmts.split(','):
src_books.append(m.db.format_abspath(id_, fmt,
index_is_id=True))
return [dest_id, src_books, src_ids]
def delete_books_after_merge(self, ids_to_delete): def delete_books_after_merge(self, ids_to_delete):
self.gui.library_view.model().delete_books_by_id(ids_to_delete) self.gui.library_view.model().delete_books_by_id(ids_to_delete)
def merge_metadata(self, dest_id, src_ids): def merge_metadata(self, dest_id, src_ids):
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
dest_mi = db.get_metadata(dest_id, index_is_id=True, get_cover=True) dest_mi = db.get_metadata(dest_id, index_is_id=True)
orig_dest_comments = dest_mi.comments orig_dest_comments = dest_mi.comments
dest_cover = db.cover(dest_id, index_is_id=True)
had_orig_cover = bool(dest_cover)
for src_id in src_ids: for src_id in src_ids:
src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True) src_mi = db.get_metadata(src_id, index_is_id=True)
if src_mi.comments and orig_dest_comments != src_mi.comments: if src_mi.comments and orig_dest_comments != src_mi.comments:
if not dest_mi.comments: if not dest_mi.comments:
dest_mi.comments = src_mi.comments dest_mi.comments = src_mi.comments
@ -372,8 +384,10 @@ class EditMetadataAction(InterfaceAction):
dest_mi.tags = src_mi.tags dest_mi.tags = src_mi.tags
else: else:
dest_mi.tags.extend(src_mi.tags) dest_mi.tags.extend(src_mi.tags)
if src_mi.cover and not dest_mi.cover: if not dest_cover:
dest_mi.cover = src_mi.cover src_cover = db.cover(src_id, index_is_id=True)
if src_cover:
dest_cover = src_cover
if not dest_mi.publisher: if not dest_mi.publisher:
dest_mi.publisher = src_mi.publisher dest_mi.publisher = src_mi.publisher
if not dest_mi.rating: if not dest_mi.rating:
@ -382,6 +396,8 @@ class EditMetadataAction(InterfaceAction):
dest_mi.series = src_mi.series dest_mi.series = src_mi.series
dest_mi.series_index = src_mi.series_index dest_mi.series_index = src_mi.series_index
db.set_metadata(dest_id, dest_mi, ignore_errors=False) db.set_metadata(dest_id, dest_mi, ignore_errors=False)
if not had_orig_cover and dest_cover:
db.set_cover(dest_id, dest_cover)
for key in db.field_metadata: #loop thru all defined fields for key in db.field_metadata: #loop thru all defined fields
if db.field_metadata[key]['is_custom']: if db.field_metadata[key]['is_custom']:

View File

@ -5,6 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.gui2.dialogs.tweak_epub import TweakEpub from calibre.gui2.dialogs.tweak_epub import TweakEpub
@ -30,8 +32,8 @@ class TweakEpubAction(InterfaceAction):
# Confirm 'EPUB' in formats # Confirm 'EPUB' in formats
book_id = self.gui.library_view.model().id(row) book_id = self.gui.library_view.model().id(row)
try: try:
path_to_epub = self.gui.library_view.model().db.format_abspath( path_to_epub = self.gui.library_view.model().db.format(
book_id, 'EPUB', index_is_id=True) book_id, 'EPUB', index_is_id=True, as_path=True)
except: except:
path_to_epub = None path_to_epub = None
@ -45,6 +47,7 @@ class TweakEpubAction(InterfaceAction):
if dlg.exec_() == dlg.Accepted: if dlg.exec_() == dlg.Accepted:
self.update_db(book_id, dlg._output) self.update_db(book_id, dlg._output)
dlg.cleanup() dlg.cleanup()
os.remove(path_to_epub)
def update_db(self, book_id, rebuilt): def update_db(self, book_id, rebuilt):
''' '''

View File

@ -445,6 +445,7 @@ class Saver(QObject): # {{{
self.pd.setModal(True) self.pd.setModal(True)
self.pd.show() self.pd.show()
self.pd.set_min(0) self.pd.set_min(0)
self.pd.set_msg(_('Collecting data, please wait...'))
self._parent = parent self._parent = parent
self.callback = callback self.callback = callback
self.callback_called = False self.callback_called = False

View File

@ -4,7 +4,7 @@ __license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>' __copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import re import re, os
from PyQt4.QtCore import SIGNAL, Qt, pyqtSignal from PyQt4.QtCore import SIGNAL, Qt, pyqtSignal
from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, \ from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, \
@ -134,7 +134,12 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
_('Cannot build regex using the GUI builder without a book.'), _('Cannot build regex using the GUI builder without a book.'),
show=True) show=True)
return False return False
self.open_book(db.format_abspath(book_id, format, index_is_id=True)) fpath = db.format(book_id, format, index_is_id=True,
as_path=True)
try:
self.open_book(fpath)
finally:
os.remove(fpath)
return True return True
def open_book(self, pathtoebook): def open_book(self, pathtoebook):

View File

@ -106,7 +106,6 @@ class Config(ResizableDialog, Ui_Dialog):
Configuration dialog for single book conversion. If accepted, has the Configuration dialog for single book conversion. If accepted, has the
following important attributes following important attributes
input_path - Path to input file
output_format - Output format (without a leading .) output_format - Output format (without a leading .)
input_format - Input format (without a leading .) input_format - Input format (without a leading .)
opf_path - Path to OPF file with user specified metadata opf_path - Path to OPF file with user specified metadata
@ -156,13 +155,10 @@ class Config(ResizableDialog, Ui_Dialog):
oidx = self.groups.currentIndex().row() oidx = self.groups.currentIndex().row()
input_format = self.input_format input_format = self.input_format
output_format = self.output_format output_format = self.output_format
input_path = self.db.format_abspath(self.book_id, input_format,
index_is_id=True)
self.input_path = input_path
output_path = 'dummy.'+output_format output_path = 'dummy.'+output_format
log = Log() log = Log()
log.outputs = [] log.outputs = []
self.plumber = Plumber(input_path, output_path, log) self.plumber = Plumber('dummy.'+input_format, output_path, log)
def widget_factory(cls): def widget_factory(cls):
return cls(self.stack, self.plumber.get_option_by_name, return cls(self.stack, self.plumber.get_option_by_name,

View File

@ -396,8 +396,17 @@ class DeviceManager(Thread): # {{{
if DEBUG: if DEBUG:
prints(traceback.format_exc(), file=sys.__stdout__) prints(traceback.format_exc(), file=sys.__stdout__)
try:
return self.device.upload_books(files, names, on_card, return self.device.upload_books(files, names, on_card,
metadata=metadata, end_session=False) metadata=metadata, end_session=False)
finally:
if metadata:
for mi in metadata:
try:
if mi.cover:
os.remove(mi.cover)
except:
pass
def upload_books(self, done, files, names, on_card=None, titles=None, def upload_books(self, done, files, names, on_card=None, titles=None,
metadata=None, plugboards=None, add_as_step_to_job=None): metadata=None, plugboards=None, add_as_step_to_job=None):
@ -1072,8 +1081,6 @@ class DeviceMixin(object): # {{{
'the device?'), autos): 'the device?'), autos):
self.iactions['Convert Books'].auto_convert_news(auto, format) self.iactions['Convert Books'].auto_convert_news(auto, format)
files = [f for f in files if f is not None] files = [f for f in files if f is not None]
for f in files:
f.deleted_after_upload = del_on_upload
if not files: if not files:
self.news_to_be_synced = set([]) self.news_to_be_synced = set([])
return return
@ -1315,8 +1322,17 @@ class DeviceMixin(object): # {{{
self.card_b_view if on_card == 'cardb' else self.memory_view self.card_b_view if on_card == 'cardb' else self.memory_view
view.model().resort(reset=False) view.model().resort(reset=False)
view.model().research() view.model().research()
if files:
for f in files: for f in files:
getattr(f, 'close', lambda : True)() # Remove temporary files
try:
rem = not getattr(
self.device_manager.device,
'KEEP_TEMP_FILES_AFTER_UPLOAD', False)
if rem and 'caltmpfmt.' in f:
os.remove(f)
except:
pass
def book_on_device(self, id, reset=False): def book_on_device(self, id, reset=False):
''' '''

View File

@ -24,7 +24,7 @@ from calibre.utils.config import prefs, tweaks
from calibre.utils.magick.draw import identify_data from calibre.utils.magick.draw import identify_data
from calibre.utils.date import qt_to_dt from calibre.utils.date import qt_to_dt
def get_cover_data(path): # {{{ def get_cover_data(stream, ext): # {{{
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
old = prefs['read_file_metadata'] old = prefs['read_file_metadata']
if not old: if not old:
@ -32,8 +32,8 @@ def get_cover_data(path): # {{{
cdata = area = None cdata = area = None
try: try:
mi = get_metadata(open(path, 'rb'), with stream:
os.path.splitext(path)[1][1:].lower()) mi = get_metadata(stream, ext)
if mi.cover and os.access(mi.cover, os.R_OK): if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover).read() cdata = open(mi.cover).read()
elif mi.cover_data[1] is not None: elif mi.cover_data[1] is not None:
@ -186,9 +186,10 @@ class MyBlockingBusy(QDialog): # {{{
if fmts: if fmts:
covers = [] covers = []
for fmt in fmts.split(','): for fmt in fmts.split(','):
fmt = self.db.format_abspath(id, fmt, index_is_id=True) fmtf = self.db.format(id, fmt, index_is_id=True,
if not fmt: continue as_file=True)
cdata, area = get_cover_data(fmt) if fmtf is None: continue
cdata, area = get_cover_data(fmtf, fmt)
if cdata: if cdata:
covers.append((cdata, area)) covers.append((cdata, area))
covers.sort(key=lambda x: x[1]) covers.sort(key=lambda x: x[1])

View File

@ -174,7 +174,8 @@ class EmailMixin(object): # {{{
else: else:
_auto_ids = [] _auto_ids = []
full_metadata = self.library_view.model().metadata_for(ids) full_metadata = self.library_view.model().metadata_for(ids,
get_cover=False)
bad, remove_ids, jobnames = [], [], [] bad, remove_ids, jobnames = [], [], []
texts, subjects, attachments, attachment_names = [], [], [], [] texts, subjects, attachments, attachment_names = [], [], [], []

View File

@ -5,8 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import shutil, functools, re, os, traceback import functools, re, os, traceback
from contextlib import closing
from collections import defaultdict from collections import defaultdict
from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage,
@ -36,14 +35,6 @@ TIME_FMT = '%d %b %Y'
ALIGNMENT_MAP = {'left': Qt.AlignLeft, 'right': Qt.AlignRight, 'center': ALIGNMENT_MAP = {'left': Qt.AlignLeft, 'right': Qt.AlignRight, 'center':
Qt.AlignHCenter} Qt.AlignHCenter}
class FormatPath(unicode):
def __new__(cls, path, orig_file_path):
ans = unicode.__new__(cls, path)
ans.orig_file_path = orig_file_path
ans.deleted_after_upload = False
return ans
_default_image = None _default_image = None
def default_image(): def default_image():
@ -391,10 +382,14 @@ class BooksModel(QAbstractTableModel): # {{{
data = self.current_changed(index, None, False) data = self.current_changed(index, None, False)
return data return data
def metadata_for(self, ids): def metadata_for(self, ids, get_cover=True):
'''
WARNING: if get_cover=True temp files are created for mi.cover.
Remember to delete them once you are done with them.
'''
ans = [] ans = []
for id in ids: for id in ids:
mi = self.db.get_metadata(id, index_is_id=True, get_cover=True) mi = self.db.get_metadata(id, index_is_id=True, get_cover=get_cover)
ans.append(mi) ans.append(mi)
return ans return ans
@ -449,18 +444,14 @@ class BooksModel(QAbstractTableModel): # {{{
format = f format = f
break break
if format is not None: if format is not None:
pt = PersistentTemporaryFile(suffix='.'+format) pt = PersistentTemporaryFile(suffix='caltmpfmt.'+format)
with closing(self.db.format(id, format, index_is_id=True, self.db.copy_format_to(id, format, pt, index_is_id=True)
as_file=True)) as src:
shutil.copyfileobj(src, pt)
pt.flush()
if getattr(src, 'name', None):
pt.orig_file_path = os.path.abspath(src.name)
pt.seek(0) pt.seek(0)
if set_metadata: if set_metadata:
try: try:
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True), _set_metadata(pt, self.db.get_metadata(
format) id, get_cover=True, index_is_id=True,
cover_as_data=True), format)
except: except:
traceback.print_exc() traceback.print_exc()
pt.close() pt.close()
@ -468,9 +459,7 @@ class BooksModel(QAbstractTableModel): # {{{
if isbytestring(x): if isbytestring(x):
x = x.decode(filesystem_encoding) x = x.decode(filesystem_encoding)
return x return x
name, op = map(to_uni, map(os.path.abspath, (pt.name, ans.append(to_uni(os.path.abspath(pt.name)))
pt.orig_file_path)))
ans.append(FormatPath(name, op))
else: else:
need_auto.append(id) need_auto.append(id)
if not exclude_auto: if not exclude_auto:
@ -499,13 +488,11 @@ class BooksModel(QAbstractTableModel): # {{{
break break
if format is not None: if format is not None:
pt = PersistentTemporaryFile(suffix='.'+format) pt = PersistentTemporaryFile(suffix='.'+format)
with closing(self.db.format(row, format, as_file=True)) as src: self.db.copy_format_to(id, format, pt, index_is_id=True)
shutil.copyfileobj(src, pt)
pt.flush()
pt.seek(0) pt.seek(0)
if set_metadata: if set_metadata:
_set_metadata(pt, self.db.get_metadata(row, get_cover=True), _set_metadata(pt, self.db.get_metadata(row, get_cover=True,
format) cover_as_data=True), format)
pt.close() if paths else pt.seek(0) pt.close() if paths else pt.seek(0)
ans.append(pt) ans.append(pt)
else: else:

View File

@ -584,14 +584,15 @@ class BooksView(QTableView): # {{{
m = self.model() m = self.model()
db = m.db db = m.db
rows = self.selectionModel().selectedRows() rows = self.selectionModel().selectedRows()
selected = map(m.id, rows) selected = list(map(m.id, rows))
ids = ' '.join(map(str, selected)) ids = ' '.join(map(str, selected))
md = QMimeData() md = QMimeData()
md.setData('application/calibre+from_library', ids) md.setData('application/calibre+from_library', ids)
fmt = prefs['output_format'] fmt = prefs['output_format']
def url_for_id(i): def url_for_id(i):
ans = db.format_abspath(i, fmt, index_is_id=True) ans = db.format(i, fmt, index_is_id=True, as_path=True,
preserve_filename=True)
if ans is None: if ans is None:
fmts = db.formats(i, index_is_id=True) fmts = db.formats(i, index_is_id=True)
if fmts: if fmts:
@ -599,14 +600,13 @@ class BooksView(QTableView): # {{{
else: else:
fmts = [] fmts = []
for f in fmts: for f in fmts:
ans = db.format_abspath(i, f, index_is_id=True) ans = db.format(i, f, index_is_id=True, as_path=True,
if ans is not None: preserve_filename=True)
break
if ans is None: if ans is None:
ans = db.abspath(i, index_is_id=True) ans = db.abspath(i, index_is_id=True)
return QUrl.fromLocalFile(ans) return QUrl.fromLocalFile(ans)
md.setUrls([url_for_id(i) for i in selected]) md.setUrls([url_for_id(i) for i in selected[:25]])
drag = QDrag(self) drag = QDrag(self)
col = self.selectionModel().currentIndex().column() col = self.selectionModel().currentIndex().column()
md.column_name = self.column_map[col] md.column_name = self.column_map[col]

View File

@ -688,6 +688,7 @@ class FormatsManager(QWidget): # {{{
else: else:
stream = open(fmt.path, 'r+b') stream = open(fmt.path, 'r+b')
try: try:
with stream:
mi = get_metadata(stream, ext) mi = get_metadata(stream, ext)
return mi, ext return mi, ext
except: except:

View File

@ -51,12 +51,15 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
# continue # continue
mi = db.get_metadata(book_id, True) mi = db.get_metadata(book_id, True)
in_file = db.format_abspath(book_id, d.input_format, True) in_file = PersistentTemporaryFile('.'+d.input_format)
with in_file:
db.copy_format_to(book_id, d.input_format, in_file,
index_is_id=True)
out_file = PersistentTemporaryFile('.' + d.output_format) out_file = PersistentTemporaryFile('.' + d.output_format)
out_file.write(d.output_format) out_file.write(d.output_format)
out_file.close() out_file.close()
temp_files = [] temp_files = [in_file]
try: try:
dtitle = unicode(mi.title) dtitle = unicode(mi.title)
@ -74,7 +77,7 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
recs.append(('cover', d.cover_file.name, recs.append(('cover', d.cover_file.name,
OptionRecommendation.HIGH)) OptionRecommendation.HIGH))
temp_files.append(d.cover_file) temp_files.append(d.cover_file)
args = [in_file, out_file.name, recs] args = [in_file.name, out_file.name, recs]
temp_files.append(out_file) temp_files.append(out_file)
jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files)) jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files))
@ -142,12 +145,15 @@ class QueueBulk(QProgressDialog):
try: try:
input_format = get_input_format_for_book(self.db, book_id, None)[0] input_format = get_input_format_for_book(self.db, book_id, None)[0]
mi, opf_file = create_opf_file(self.db, book_id) mi, opf_file = create_opf_file(self.db, book_id)
in_file = self.db.format_abspath(book_id, input_format, True) in_file = PersistentTemporaryFile('.'+input_format)
with in_file:
self.db.copy_format_to(book_id, input_format, in_file,
index_is_id=True)
out_file = PersistentTemporaryFile('.' + self.output_format) out_file = PersistentTemporaryFile('.' + self.output_format)
out_file.write(self.output_format) out_file.write(self.output_format)
out_file.close() out_file.close()
temp_files = [] temp_files = [in_file]
combined_recs = GuiRecommendations() combined_recs = GuiRecommendations()
default_recs = bulk_defaults_for_input_format(input_format) default_recs = bulk_defaults_for_input_format(input_format)
@ -183,7 +189,7 @@ class QueueBulk(QProgressDialog):
self.setLabelText(_('Queueing ')+dtitle) self.setLabelText(_('Queueing ')+dtitle)
desc = _('Convert book %d of %d (%s)') % (self.i, len(self.book_ids), dtitle) desc = _('Convert book %d of %d (%s)') % (self.i, len(self.book_ids), dtitle)
args = [in_file, out_file.name, lrecs] args = [in_file.name, out_file.name, lrecs]
temp_files.append(out_file) temp_files.append(out_file)
self.jobs.append(('gui_convert_override', args, desc, self.output_format.upper(), book_id, temp_files)) self.jobs.append(('gui_convert_override', args, desc, self.output_format.upper(), book_id, temp_files))

View File

@ -61,22 +61,4 @@ def generate_test_db(library_path, # {{{
print 'Time per record:', t/float(num_of_records) print 'Time per record:', t/float(num_of_records)
# }}} # }}}
def cover_load_timing(path=None):
from PyQt4.Qt import QApplication, QImage
import os, time
app = QApplication([])
app
d = db(path)
paths = [d.cover(i, index_is_id=True, as_path=True) for i in
d.data.iterallids()]
paths = [p for p in paths if (p and os.path.exists(p) and os.path.isfile(p))]
start = time.time()
for p in paths:
with open(p, 'rb') as f:
img = QImage()
img.loadFromData(f.read())
print 'Average load time:', (time.time() - start)/len(paths), 'seconds'

View File

@ -5137,6 +5137,7 @@ Author '{0}':
OptionRecommendation.HIGH)) OptionRecommendation.HIGH))
# If cover exists, use it # If cover exists, use it
cpath = None
try: try:
search_text = 'title:"%s" author:%s' % ( search_text = 'title:"%s" author:%s' % (
opts.catalog_title.replace('"', '\\"'), 'calibre') opts.catalog_title.replace('"', '\\"'), 'calibre')
@ -5157,5 +5158,10 @@ Author '{0}':
plumber.merge_ui_recommendations(recommendations) plumber.merge_ui_recommendations(recommendations)
plumber.run() plumber.run()
try:
os.remove(cpath)
except:
pass
# returns to gui2.actions.catalog:catalog_generated() # returns to gui2.actions.catalog:catalog_generated()
return catalog.error return catalog.error

View File

@ -12,8 +12,6 @@ import threading, random
from itertools import repeat from itertools import repeat
from math import ceil from math import ceil
from PyQt4.QtGui import QImage
from calibre import prints from calibre import prints
from calibre.ebooks.metadata import (title_sort, author_to_author_sort, from calibre.ebooks.metadata import (title_sort, author_to_author_sort,
string_to_authors, authors_to_string) string_to_authors, authors_to_string)
@ -27,7 +25,7 @@ from calibre.library.sqlite import connect, IntegrityError
from calibre.library.prefs import DBPrefs from calibre.library.prefs import DBPrefs
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.constants import preferred_encoding, iswindows, filesystem_encoding from calibre.constants import preferred_encoding, iswindows, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile, base_dir
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
from calibre import isbytestring from calibre import isbytestring
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
@ -39,8 +37,10 @@ from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.utils.magick.draw import save_cover_data_to from calibre.utils.magick.draw import save_cover_data_to
from calibre.utils.recycle_bin import delete_file, delete_tree from calibre.utils.recycle_bin import delete_file, delete_tree
from calibre.utils.formatter_functions import load_user_template_functions from calibre.utils.formatter_functions import load_user_template_functions
from calibre.db.errors import NoSuchFormat
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
SPOOL_SIZE = 30*1024*1024
class Tag(object): class Tag(object):
@ -601,14 +601,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
with lopen(os.path.join(tpath, 'cover.jpg'), 'wb') as f: with lopen(os.path.join(tpath, 'cover.jpg'), 'wb') as f:
f.write(cdata) f.write(cdata)
for format in formats: for format in formats:
# Get data as string (can't use file as source and target files may be the same) with tempfile.SpooledTemporaryFile(max_size=SPOOL_SIZE) as stream:
f = self.format(id, format, index_is_id=True, as_file=True) try:
if f is None: self.copy_format_to(id, format, stream, index_is_id=True)
continue
with tempfile.SpooledTemporaryFile(max_size=30*(1024**2)) as stream:
with f:
shutil.copyfileobj(f, stream)
stream.seek(0) stream.seek(0)
except NoSuchFormat:
continue
self.add_format(id, format, stream, index_is_id=True, self.add_format(id, format, stream, index_is_id=True,
path=tpath, notify=False) path=tpath, notify=False)
self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id)) self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id))
@ -665,28 +663,49 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
''' '''
Return the cover image as a bytestring (in JPEG format) or None. Return the cover image as a bytestring (in JPEG format) or None.
`as_file` : If True return the image as an open file object WARNING: Using as_path will copy the cover to a temp file and return
`as_image`: If True return the image as a QImage object the path to the temp file. You should delete the temp file when you are
done with it.
:param as_file: If True return the image as an open file object (a SpooledTemporaryFile)
:param as_image: If True return the image as a QImage object
''' '''
id = index if index_is_id else self.id(index) id = index if index_is_id else self.id(index)
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg') path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
if os.access(path, os.R_OK): if os.access(path, os.R_OK):
if as_path:
return path
try: try:
f = lopen(path, 'rb') f = lopen(path, 'rb')
except (IOError, OSError): except (IOError, OSError):
time.sleep(0.2) time.sleep(0.2)
f = lopen(path, 'rb') f = lopen(path, 'rb')
with f:
if as_path:
pt = PersistentTemporaryFile('_dbcover.jpg')
with pt:
shutil.copyfileobj(f, pt)
return pt.name
if as_file:
ret = tempfile.SpooledTemporaryFile(SPOOL_SIZE)
shutil.copyfileobj(f, ret)
ret.seek(0)
else:
ret = f.read()
if as_image: if as_image:
img = QImage() from PyQt4.Qt import QImage
img.loadFromData(f.read()) i = QImage()
f.close() i.loadFromData(ret)
return img ret = i
ans = f if as_file else f.read() return ret
if ans is not f:
f.close() def cover_last_modified(self, index, index_is_id=False):
return ans id = index if index_is_id else self.id(index)
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
try:
return utcfromtimestamp(os.stat(path).st_mtime)
except:
# Cover doesn't exist
pass
return self.last_modified()
### The field-style interface. These use field keys. ### The field-style interface. These use field keys.
@ -859,7 +878,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return (path, mi, sequence) return (path, mi, sequence)
def get_metadata(self, idx, index_is_id=False, get_cover=False, def get_metadata(self, idx, index_is_id=False, get_cover=False,
get_user_categories=True): get_user_categories=True, cover_as_data=False):
''' '''
Convenience method to return metadata as a :class:`Metadata` object. Convenience method to return metadata as a :class:`Metadata` object.
Note that the list of formats is not verified. Note that the list of formats is not verified.
@ -934,6 +953,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.user_categories = user_cat_vals mi.user_categories = user_cat_vals
if get_cover: if get_cover:
if cover_as_data:
cdata = self.cover(id, index_is_id=True)
if cdata:
mi.cover_data = ('jpeg', cdata)
else:
mi.cover = self.cover(id, index_is_id=True, as_path=True) mi.cover = self.cover(id, index_is_id=True, as_path=True)
return mi return mi
@ -1099,7 +1123,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return utcfromtimestamp(os.stat(path).st_mtime) return utcfromtimestamp(os.stat(path).st_mtime)
def format_abspath(self, index, format, index_is_id=False): def format_abspath(self, index, format, index_is_id=False):
'Return absolute path to the ebook file of format `format`' '''
Return absolute path to the ebook file of format `format`
WARNING: This method will return a dummy path for a network backend DB,
so do not rely on it, use format(..., as_path=True) instead.
Currently used only in calibredb list, the viewer and the catalogs (via
get_data_as_dict()).
Apart from the viewer, I don't believe any of the others do any file
I/O with the results of this call.
'''
id = index if index_is_id else self.id(index) id = index if index_is_id else self.id(index)
try: try:
name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False) name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False)
@ -1119,25 +1154,63 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
shutil.copyfile(candidates[0], fmt_path) shutil.copyfile(candidates[0], fmt_path)
return fmt_path return fmt_path
def format(self, index, format, index_is_id=False, as_file=False, mode='r+b'): def copy_format_to(self, index, fmt, dest, index_is_id=False):
'''
Copy the format ``fmt`` to the file like object ``dest``. If the
specified format does not exist, raises :class:`NoSuchFormat` error.
'''
path = self.format_abspath(index, fmt, index_is_id=index_is_id)
if path is None:
id_ = index if index_is_id else self.id(index)
raise NoSuchFormat('Record %d has no %s file'%(id_, fmt))
with lopen(path, 'rb') as f:
shutil.copyfileobj(f, dest)
if hasattr(dest, 'flush'):
dest.flush()
def format(self, index, format, index_is_id=False, as_file=False,
mode='r+b', as_path=False, preserve_filename=False):
''' '''
Return the ebook format as a bytestring or `None` if the format doesn't exist, Return the ebook format as a bytestring or `None` if the format doesn't exist,
or we don't have permission to write to the ebook file. or we don't have permission to write to the ebook file.
`as_file`: If True the ebook format is returned as a file object opened in `mode` :param as_file: If True the ebook format is returned as a file object. Note
that the file object is a SpooledTemporaryFile, so if what you want to
do is copy the format to another file, use :method:`copy_format_to`
instead for performance.
:param as_path: Copies the format file to a temp file and returns the
path to the temp file
:param preserve_filename: If True and returning a path the filename is
the same as that used in the library. Note that using
this means that repeated calls yield the same
temp file (which is re-created each time)
:param mode: This is ignored (present for legacy compatibility)
''' '''
path = self.format_abspath(index, format, index_is_id=index_is_id) path = self.format_abspath(index, format, index_is_id=index_is_id)
if path is not None: if path is not None:
f = lopen(path, mode) with lopen(path, mode) as f:
if as_path:
if preserve_filename:
bd = base_dir()
d = os.path.join(bd, 'format_abspath')
try: try:
ret = f if as_file else f.read() os.makedirs(d)
except IOError: except:
f.seek(0) pass
out = cStringIO.StringIO() fname = os.path.basename(path)
shutil.copyfileobj(f, out) ret = os.path.join(d, fname)
ret = out.getvalue() with lopen(ret, 'wb') as f2:
if not as_file: shutil.copyfileobj(f, f2)
f.close() else:
with PersistentTemporaryFile('.'+format.lower()) as pt:
shutil.copyfileobj(f, pt)
ret = pt.name
elif as_file:
ret = tempfile.SpooledTemporaryFile(max_size=SPOOL_SIZE)
shutil.copyfileobj(f, ret)
ret.seek(0)
else:
ret = f.read()
return ret return ret
def add_format_with_hooks(self, index, format, fpath, index_is_id=False, def add_format_with_hooks(self, index, format, fpath, index_is_id=False,

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, traceback, cStringIO, re, shutil import os, traceback, cStringIO, re
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre.utils.config import Config, StringConfig, tweaks from calibre.utils.config import Config, StringConfig, tweaks
@ -238,7 +238,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
def save_book_to_disk(id_, db, root, opts, length): def save_book_to_disk(id_, db, root, opts, length):
mi = db.get_metadata(id_, index_is_id=True) mi = db.get_metadata(id_, index_is_id=True)
cover = db.cover(id_, index_is_id=True, as_path=True) cover = db.cover(id_, index_is_id=True)
plugboards = db.prefs.get('plugboards', {}) plugboards = db.prefs.get('plugboards', {})
available_formats = db.formats(id_, index_is_id=True) available_formats = db.formats(id_, index_is_id=True)
@ -252,12 +252,20 @@ def save_book_to_disk(id_, db, root, opts, length):
if fmts: if fmts:
fmts = fmts.split(',') fmts = fmts.split(',')
for fmt in fmts: for fmt in fmts:
fpath = db.format_abspath(id_, fmt, index_is_id=True) fpath = db.format(id_, fmt, index_is_id=True, as_path=True)
if fpath is not None: if fpath is not None:
formats[fmt.lower()] = fpath formats[fmt.lower()] = fpath
try:
return do_save_book_to_disk(id_, mi, cover, plugboards, return do_save_book_to_disk(id_, mi, cover, plugboards,
formats, root, opts, length) formats, root, opts, length)
finally:
for temp in formats.itervalues():
try:
os.remove(temp)
except:
pass
def do_save_book_to_disk(id_, mi, cover, plugboards, def do_save_book_to_disk(id_, mi, cover, plugboards,
@ -289,10 +297,9 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
raise raise
ocover = mi.cover ocover = mi.cover
if opts.save_cover and cover and os.access(cover, os.R_OK): if opts.save_cover and cover:
with open(base_path+'.jpg', 'wb') as f: with open(base_path+'.jpg', 'wb') as f:
with open(cover, 'rb') as s: f.write(cover)
shutil.copyfileobj(s, f)
mi.cover = base_name+'.jpg' mi.cover = base_name+'.jpg'
else: else:
mi.cover = None mi.cover = None
@ -395,8 +402,13 @@ def save_serialized_to_disk(ids, data, plugboards, root, opts, callback):
pass pass
tb = '' tb = ''
try: try:
failed, id, title = do_save_book_to_disk(x, mi, cover, plugboards, with open(cover, 'rb') as f:
format_map, root, opts, length) cover = f.read()
except:
cover = None
try:
failed, id, title = do_save_book_to_disk(x, mi, cover,
plugboards, format_map, root, opts, length)
tb = _('Requested formats not available') tb = _('Requested formats not available')
except: except:
failed, id, title = True, x, mi.title failed, id, title = True, x, mi.title

View File

@ -10,12 +10,13 @@ import re, os, posixpath
import cherrypy import cherrypy
from calibre import fit_image, guess_type from calibre import fit_image, guess_type
from calibre.utils.date import fromtimestamp from calibre.utils.date import fromtimestamp, utcnow
from calibre.library.caches import SortKeyGenerator from calibre.library.caches import SortKeyGenerator
from calibre.library.save_to_disk import find_plugboard from calibre.library.save_to_disk import find_plugboard
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.magick.draw import save_cover_data_to, Image, \ from calibre.utils.magick.draw import (save_cover_data_to, Image,
thumbnail as generate_thumbnail thumbnail as generate_thumbnail)
from calibre.utils.filenames import ascii_filename
plugboard_content_server_value = 'content_server' plugboard_content_server_value = 'content_server'
plugboard_content_server_formats = ['epub'] plugboard_content_server_formats = ['epub']
@ -46,7 +47,7 @@ class ContentServer(object):
# Utility methods {{{ # Utility methods {{{
def last_modified(self, updated): def last_modified(self, updated):
''' '''
Generates a local independent, english timestamp from a datetime Generates a locale independent, english timestamp from a datetime
object object
''' '''
lm = updated.strftime('day, %d month %Y %H:%M:%S GMT') lm = updated.strftime('day, %d month %Y %H:%M:%S GMT')
@ -151,14 +152,12 @@ class ContentServer(object):
try: try:
cherrypy.response.headers['Content-Type'] = 'image/jpeg' cherrypy.response.headers['Content-Type'] = 'image/jpeg'
cherrypy.response.timeout = 3600 cherrypy.response.timeout = 3600
cover = self.db.cover(id, index_is_id=True, as_file=True) cover = self.db.cover(id, index_is_id=True)
if cover is None: if cover is None:
cover = self.default_cover cover = self.default_cover
updated = self.build_time updated = self.build_time
else: else:
with cover as f: updated = self.db.cover_last_modified(id, index_is_id=True)
updated = fromtimestamp(os.fstat(f.fileno()).st_mtime)
cover = f.read()
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
if thumbnail: if thumbnail:
@ -187,9 +186,9 @@ class ContentServer(object):
mode='rb') mode='rb')
if fmt is None: if fmt is None:
raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format)) raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format))
mi = self.db.get_metadata(id, index_is_id=True)
if format == 'EPUB': if format == 'EPUB':
# Get the original metadata # Get the original metadata
mi = self.db.get_metadata(id, index_is_id=True)
# Get any EPUB plugboards for the content server # Get any EPUB plugboards for the content server
plugboards = self.db.prefs.get('plugboards', {}) plugboards = self.db.prefs.get('plugboards', {})
@ -203,24 +202,22 @@ class ContentServer(object):
newmi = mi newmi = mi
# Write the updated file # Write the updated file
from tempfile import TemporaryFile
from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.meta import set_metadata
raw = fmt.read()
fmt = TemporaryFile()
fmt.write(raw)
fmt.seek(0)
set_metadata(fmt, newmi, 'epub') set_metadata(fmt, newmi, 'epub')
fmt.seek(0) fmt.seek(0)
mt = guess_type('dummy.'+format.lower())[0] mt = guess_type('dummy.'+format.lower())[0]
if mt is None: if mt is None:
mt = 'application/octet-stream' mt = 'application/octet-stream'
au = authors_to_string(mi.authors if mi.authors else [_('Unknown')])
title = mi.title if mi.title else _('Unknown')
fname = u'%s - %s_%s.%s'%(title[:30], au[:30], id, format.lower())
fname = ascii_filename(fname).replace('"', '_')
cherrypy.response.headers['Content-Type'] = mt cherrypy.response.headers['Content-Type'] = mt
cherrypy.response.headers['Content-Disposition'] = \
b'attachment; filename="%s"'%fname
cherrypy.response.timeout = 3600 cherrypy.response.timeout = 3600
path = getattr(fmt, 'name', None) cherrypy.response.headers['Last-Modified'] = self.last_modified(utcnow())
if path and os.path.exists(path):
updated = fromtimestamp(os.stat(path).st_mtime)
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
return fmt return fmt
# }}} # }}}