Sync to trunk.

This commit is contained in:
John Schember 2011-06-23 18:47:37 -04:00
commit 89f2ffd62a
48 changed files with 2486 additions and 1129 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

@ -13,8 +13,10 @@ CREATE TABLE books ( id INTEGER PRIMARY KEY AUTOINCREMENT,
isbn TEXT DEFAULT "" COLLATE NOCASE, isbn TEXT DEFAULT "" COLLATE NOCASE,
lccn TEXT DEFAULT "" COLLATE NOCASE, lccn TEXT DEFAULT "" COLLATE NOCASE,
path TEXT NOT NULL DEFAULT "", path TEXT NOT NULL DEFAULT "",
flags INTEGER NOT NULL DEFAULT 1 flags INTEGER NOT NULL DEFAULT 1,
, uuid TEXT, has_cover BOOL DEFAULT 0, last_modified TIMESTAMP NOT NULL DEFAULT "2000-01-01 00:00:00+00:00"); uuid TEXT,
has_cover BOOL DEFAULT 0,
last_modified TIMESTAMP NOT NULL DEFAULT "2000-01-01 00:00:00+00:00");
CREATE TABLE books_authors_link ( id INTEGER PRIMARY KEY, CREATE TABLE books_authors_link ( id INTEGER PRIMARY KEY,
book INTEGER NOT NULL, book INTEGER NOT NULL,
author INTEGER NOT NULL, author INTEGER NOT NULL,

View File

@ -578,6 +578,7 @@ def url_slash_cleaner(url):
def get_download_filename(url, cookie_file=None): def get_download_filename(url, cookie_file=None):
''' '''
Get a local filename for a URL using the content disposition header Get a local filename for a URL using the content disposition header
Returns empty string if no content disposition header present
''' '''
from contextlib import closing from contextlib import closing
from urllib2 import unquote as urllib2_unquote from urllib2 import unquote as urllib2_unquote

View File

@ -799,6 +799,11 @@ class ActionFetchNews(InterfaceActionBase):
actual_plugin = 'calibre.gui2.actions.fetch_news:FetchNewsAction' actual_plugin = 'calibre.gui2.actions.fetch_news:FetchNewsAction'
description = _('Download news from the internet in ebook form') description = _('Download news from the internet in ebook form')
class ActionQuickview(InterfaceActionBase):
name = 'Show Quickview'
actual_plugin = 'calibre.gui2.actions.show_quickview:ShowQuickviewAction'
description = _('Show a list of related books quickly')
class ActionSaveToDisk(InterfaceActionBase): class ActionSaveToDisk(InterfaceActionBase):
name = 'Save To Disk' name = 'Save To Disk'
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction' actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
@ -870,7 +875,7 @@ class ActionCopyToLibrary(InterfaceActionBase):
class ActionTweakEpub(InterfaceActionBase): class ActionTweakEpub(InterfaceActionBase):
name = 'Tweak ePub' name = 'Tweak ePub'
actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction' actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction'
description = _('Make small twekas to epub files in your calibre library') description = _('Make small tweaks to epub files in your calibre library')
class ActionNextMatch(InterfaceActionBase): class ActionNextMatch(InterfaceActionBase):
name = 'Next Match' name = 'Next Match'
@ -903,8 +908,8 @@ class ActionPluginUpdater(InterfaceActionBase):
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, ActionFetchNews, ActionSaveToDisk, ActionQuickview,
ActionRestart, ActionOpenFolder, ActionConnectShare, ActionShowBookDetails,ActionRestart, ActionOpenFolder, ActionConnectShare,
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary, ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore, ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore,

View File

@ -261,7 +261,7 @@ class OutputFormatPlugin(Plugin):
@property @property
def description(self): def description(self):
return _('Convert ebooks to the %s format'%self.file_type) return _('Convert ebooks to the %s format')%self.file_type
def __init__(self, *args): def __init__(self, *args):
Plugin.__init__(self, *args) Plugin.__init__(self, *args)

View File

@ -23,6 +23,8 @@ from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.utils.icu import strcmp from calibre.utils.icu import strcmp
from calibre.utils.config import to_json, from_json, prefs, tweaks from calibre.utils.config import to_json, from_json, prefs, tweaks
from calibre.utils.date import utcfromtimestamp from calibre.utils.date import utcfromtimestamp
from calibre.db.tables import (OneToOneTable, ManyToOneTable, ManyToManyTable,
SizeTable, FormatsTable, AuthorsTable, IdentifiersTable)
# }}} # }}}
''' '''
@ -167,7 +169,7 @@ class Connection(apsw.Connection): # {{{
return self.cursor().execute(sql) return self.cursor().execute(sql)
# }}} # }}}
class DB(SchemaUpgrade): class DB(object, SchemaUpgrade):
PATH_LIMIT = 40 if iswindows else 100 PATH_LIMIT = 40 if iswindows else 100
WINDOWS_LIBRARY_PATH_LIMIT = 75 WINDOWS_LIBRARY_PATH_LIMIT = 75
@ -220,7 +222,9 @@ class DB(SchemaUpgrade):
if self.user_version == 0: if self.user_version == 0:
self.initialize_database() self.initialize_database()
SchemaUpgrade.__init__(self) with self.conn:
SchemaUpgrade.__init__(self)
# Guarantee that the library_id is set # Guarantee that the library_id is set
self.library_id self.library_id
@ -400,5 +404,40 @@ class DB(SchemaUpgrade):
''' Return last modified time as a UTC datetime object ''' ''' Return last modified time as a UTC datetime object '''
return utcfromtimestamp(os.stat(self.dbpath).st_mtime) 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
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

143
src/calibre/db/tables.py Normal file
View 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]))

View File

@ -5,7 +5,7 @@ __copyright__ = '2010, Gregory Riker'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import cStringIO, ctypes, datetime, os, re, shutil, subprocess, sys, tempfile, time import cStringIO, ctypes, datetime, os, re, sys, tempfile, time
from calibre.constants import __appname__, __version__, DEBUG from calibre.constants import __appname__, __version__, DEBUG
from calibre import fit_image, confirm_config_name from calibre import fit_image, confirm_config_name
from calibre.constants import isosx, iswindows from calibre.constants import isosx, iswindows
@ -13,8 +13,7 @@ from calibre.devices.errors import OpenFeedback, UserFeedback
from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.deviceconfig import DeviceConfig
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.ebooks.metadata import authors_to_string, MetaInformation, \ from calibre.ebooks.metadata import authors_to_string, MetaInformation, title_sort
title_sort
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.epub import set_metadata from calibre.ebooks.metadata.epub import set_metadata
from calibre.library.server.utils import strftime from calibre.library.server.utils import strftime
@ -107,6 +106,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
@ -164,8 +164,12 @@ class ITUNES(DriverBase):
settings() settings()
set_progress_reporter() set_progress_reporter()
upload_books() upload_books()
_get_fpath() _remove_existing_copy()
_update_epub_metadata() _remove_from_device()
_remove_from_iTunes()
_add_new_copy()
_add_library_book()
_update_iTunes_metadata()
add_books_to_metadata() add_books_to_metadata()
use_plugboard_ext() use_plugboard_ext()
set_plugboard() set_plugboard()
@ -182,7 +186,7 @@ class ITUNES(DriverBase):
supported_platforms = ['osx','windows'] supported_platforms = ['osx','windows']
author = 'GRiker' author = 'GRiker'
#: The version of this plugin as a 3-tuple (major, minor, revision) #: The version of this plugin as a 3-tuple (major, minor, revision)
version = (1,0,0) version = (1,1,0)
DISPLAY_DISABLE_DIALOG = "display_disable_apple_driver_dialog" DISPLAY_DISABLE_DIALOG = "display_disable_apple_driver_dialog"
@ -277,7 +281,6 @@ class ITUNES(DriverBase):
description_prefix = "added by calibre" description_prefix = "added by calibre"
ejected = False ejected = False
iTunes= None iTunes= None
iTunes_media = None
library_orphans = None library_orphans = None
log = Log() log = Log()
manual_sync_mode = False manual_sync_mode = False
@ -413,11 +416,11 @@ class ITUNES(DriverBase):
this_book.datetime = parse_date(str(book.date_added())).timetuple() this_book.datetime = parse_date(str(book.date_added())).timetuple()
except: except:
this_book.datetime = time.gmtime() this_book.datetime = time.gmtime()
this_book.db_id = None
this_book.device_collections = [] this_book.device_collections = []
this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
this_book.size = book.size() this_book.size = book.size()
this_book.uuid = book.composer() this_book.uuid = book.composer()
this_book.cid = None
# Hack to discover if we're running in GUI environment # Hack to discover if we're running in GUI environment
if self.report_progress is not None: if self.report_progress is not None:
this_book.thumbnail = self._generate_thumbnail(this_book.path, book) this_book.thumbnail = self._generate_thumbnail(this_book.path, book)
@ -452,10 +455,10 @@ class ITUNES(DriverBase):
this_book.datetime = parse_date(str(book.DateAdded)).timetuple() this_book.datetime = parse_date(str(book.DateAdded)).timetuple()
except: except:
this_book.datetime = time.gmtime() this_book.datetime = time.gmtime()
this_book.db_id = None
this_book.device_collections = [] this_book.device_collections = []
this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
this_book.size = book.Size this_book.size = book.Size
this_book.cid = None
# Hack to discover if we're running in GUI environment # Hack to discover if we're running in GUI environment
if self.report_progress is not None: if self.report_progress is not None:
this_book.thumbnail = self._generate_thumbnail(this_book.path, book) this_book.thumbnail = self._generate_thumbnail(this_book.path, book)
@ -491,7 +494,7 @@ class ITUNES(DriverBase):
def can_handle(self, device_info, debug=False): def can_handle(self, device_info, debug=False):
''' '''
Unix version of :method:`can_handle_windows` OSX version of :method:`can_handle_windows`
:param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product, :param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product,
serial number) serial number)
@ -1021,17 +1024,14 @@ class ITUNES(DriverBase):
if DEBUG: if DEBUG:
self.log.info("ITUNES.upload_books()") self.log.info("ITUNES.upload_books()")
self._dump_files(files, header='upload_books()',indent=2)
self._dump_update_list(header='upload_books()',indent=2)
if isosx: if isosx:
for (i,file) in enumerate(files): for (i,fpath) in enumerate(files):
format = file.rpartition('.')[2].lower() format = fpath.rpartition('.')[2].lower()
path = self.path_template % (metadata[i].title, path = self.path_template % (metadata[i].title,
authors_to_string(metadata[i].authors), authors_to_string(metadata[i].authors),
format) format)
self._remove_existing_copy(path, metadata[i]) self._remove_existing_copy(path, metadata[i])
fpath = self._get_fpath(file, metadata[i], format, update_md=True)
db_added, lb_added = self._add_new_copy(fpath, metadata[i]) db_added, lb_added = self._add_new_copy(fpath, metadata[i])
thumb = self._cover_to_thumb(path, metadata[i], db_added, lb_added, format) thumb = self._cover_to_thumb(path, metadata[i], db_added, lb_added, format)
this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb, format) this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb, format)
@ -1062,13 +1062,12 @@ class ITUNES(DriverBase):
pythoncom.CoInitialize() pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application") self.iTunes = win32com.client.Dispatch("iTunes.Application")
for (i,file) in enumerate(files): for (i,fpath) in enumerate(files):
format = file.rpartition('.')[2].lower() format = fpath.rpartition('.')[2].lower()
path = self.path_template % (metadata[i].title, path = self.path_template % (metadata[i].title,
authors_to_string(metadata[i].authors), authors_to_string(metadata[i].authors),
format) format)
self._remove_existing_copy(path, metadata[i]) self._remove_existing_copy(path, metadata[i])
fpath = self._get_fpath(file, metadata[i],format, update_md=True)
db_added, lb_added = self._add_new_copy(fpath, metadata[i]) db_added, lb_added = self._add_new_copy(fpath, metadata[i])
if self.manual_sync_mode and not db_added: if self.manual_sync_mode and not db_added:
@ -1275,24 +1274,59 @@ class ITUNES(DriverBase):
def _add_new_copy(self, fpath, metadata): def _add_new_copy(self, fpath, metadata):
''' '''
fp = cached_book['lib_book'].location().path
fp = cached_book['lib_book'].Location
''' '''
if DEBUG: if DEBUG:
self.log.info(" ITUNES._add_new_copy()") self.log.info(" ITUNES._add_new_copy()")
def _save_last_known_iTunes_storage(lb_added):
if isosx:
fp = lb_added.location().path
index = fp.rfind('/Books') + len('/Books')
last_known_iTunes_storage = fp[:index]
elif iswindows:
fp = lb_added.Location
index = fp.rfind('\Books') + len('\Books')
last_known_iTunes_storage = fp[:index]
dynamic['last_known_iTunes_storage'] = last_known_iTunes_storage
self.log.warning(" last_known_iTunes_storage: %s" % last_known_iTunes_storage)
db_added = None db_added = None
lb_added = None lb_added = None
if self.manual_sync_mode: if self.manual_sync_mode:
'''
This is the unsupported direct-connect mode.
In an attempt to avoid resetting the iTunes library Media folder, don't try to
add the book to iTunes if the last_known_iTunes_storage path is inaccessible.
This means that the path has to be set at least once, probably by using
'Connect to iTunes' and doing a transfer.
'''
self.log.warning(" unsupported direct connect mode")
db_added = self._add_device_book(fpath, metadata) db_added = self._add_device_book(fpath, metadata)
if not getattr(fpath, 'deleted_after_upload', False): last_known_iTunes_storage = dynamic.get('last_known_iTunes_storage', None)
lb_added = self._add_library_book(fpath, metadata) if last_known_iTunes_storage is not None:
if lb_added: if os.path.exists(last_known_iTunes_storage):
if DEBUG: if DEBUG:
self.log.info(" file added to Library|Books for iTunes<->iBooks tracking") self.log.warning(" iTunes storage online, adding to library")
lb_added = self._add_library_book(fpath, metadata)
else:
if DEBUG:
self.log.warning(" iTunes storage not online, can't add to library")
if lb_added:
_save_last_known_iTunes_storage(lb_added)
if not lb_added and DEBUG:
self.log.warn(" failed to add '%s' to iTunes, iTunes Media folder inaccessible" % metadata.title)
else: else:
lb_added = self._add_library_book(fpath, metadata) lb_added = self._add_library_book(fpath, metadata)
if DEBUG: if lb_added:
self.log.info(" file added to Library|Books for pending sync") _save_last_known_iTunes_storage(lb_added)
else:
raise UserFeedback("iTunes Media folder inaccessible",
details="Failed to add '%s' to iTunes" % metadata.title,
level=UserFeedback.WARN)
return db_added, lb_added return db_added, lb_added
@ -1307,8 +1341,10 @@ class ITUNES(DriverBase):
if metadata.cover: if metadata.cover:
if format == 'epub': if format == 'epub':
# Pre-shrink cover '''
# self.MAX_COVER_WIDTH, self.MAX_COVER_HEIGHT Pre-shrink cover
self.MAX_COVER_WIDTH, self.MAX_COVER_HEIGHT
'''
try: try:
img = PILImage.open(metadata.cover) img = PILImage.open(metadata.cover)
width = img.size[0] width = img.size[0]
@ -1316,8 +1352,8 @@ class ITUNES(DriverBase):
scaled, nwidth, nheight = fit_image(width, height, self.MAX_COVER_WIDTH, self.MAX_COVER_HEIGHT) scaled, nwidth, nheight = fit_image(width, height, self.MAX_COVER_WIDTH, self.MAX_COVER_HEIGHT)
if scaled: if scaled:
if DEBUG: if DEBUG:
self.log.info(" '%s' scaled from %sx%s to %sx%s" % self.log.info(" cover scaled from %sx%s to %sx%s" %
(metadata.cover,width,height,nwidth,nheight)) (width,height,nwidth,nheight))
img = img.resize((nwidth, nheight), PILImage.ANTIALIAS) img = img.resize((nwidth, nheight), PILImage.ANTIALIAS)
cd = cStringIO.StringIO() cd = cStringIO.StringIO()
img.convert('RGB').save(cd, 'JPEG') img.convert('RGB').save(cd, 'JPEG')
@ -1336,9 +1372,11 @@ class ITUNES(DriverBase):
return thumb return thumb
if isosx: if isosx:
# The following commands generate an error, but the artwork does in fact '''
# get sent to the device. Seems like a bug in Apple's automation interface? The following commands generate an error, but the artwork does in fact
# Could also be a problem with the integrity of the cover data? get sent to the device. Seems like a bug in Apple's automation interface?
Could also be a problem with the integrity of the cover data?
'''
if lb_added: if lb_added:
try: try:
lb_added.artworks[1].data_.set(cover_data) lb_added.artworks[1].data_.set(cover_data)
@ -1361,9 +1399,8 @@ class ITUNES(DriverBase):
#ipython(user_ns=locals()) #ipython(user_ns=locals())
pass pass
elif iswindows: elif iswindows:
# Write the data to a real file for Windows iTunes ''' Write the data to a real file for Windows iTunes '''
tc = os.path.join(tempfile.gettempdir(), "cover.jpg") tc = os.path.join(tempfile.gettempdir(), "cover.jpg")
with open(tc,'wb') as tmp_cover: with open(tc,'wb') as tmp_cover:
tmp_cover.write(cover_data) tmp_cover.write(cover_data)
@ -1422,7 +1459,8 @@ class ITUNES(DriverBase):
this_book = Book(metadata.title, authors_to_string(metadata.authors)) this_book = Book(metadata.title, authors_to_string(metadata.authors))
this_book.datetime = time.gmtime() this_book.datetime = time.gmtime()
this_book.db_id = None #this_book.cid = metadata.id
this_book.cid = None
this_book.device_collections = [] this_book.device_collections = []
this_book.format = format this_book.format = format
this_book.library_id = lb_added # ??? GR this_book.library_id = lb_added # ??? GR
@ -1430,7 +1468,6 @@ class ITUNES(DriverBase):
this_book.thumbnail = thumb this_book.thumbnail = thumb
this_book.iTunes_id = lb_added # ??? GR this_book.iTunes_id = lb_added # ??? GR
this_book.uuid = metadata.uuid this_book.uuid = metadata.uuid
if isosx: if isosx:
if lb_added: if lb_added:
this_book.size = self._get_device_book_size(fpath, lb_added.size()) this_book.size = self._get_device_book_size(fpath, lb_added.size())
@ -1461,24 +1498,6 @@ class ITUNES(DriverBase):
return this_book return this_book
def _delete_iTunesMetadata_plist(self,fpath):
'''
Delete the plist file from the file to force recache
'''
zf = ZipFile(fpath,'a')
fnames = zf.namelist()
pl_name = 'iTunesMetadata.plist'
try:
plist = [x for x in fnames if pl_name in x][0]
except:
plist = None
if plist:
if DEBUG:
self.log.info(" _delete_iTunesMetadata_plist():")
self.log.info(" deleting '%s'\n from '%s'" % (pl_name,fpath))
zf.delete(pl_name)
zf.close()
def _discover_manual_sync_mode(self, wait=0): def _discover_manual_sync_mode(self, wait=0):
''' '''
Assumes pythoncom for windows Assumes pythoncom for windows
@ -1663,18 +1682,6 @@ class ITUNES(DriverBase):
zf.close() zf.close()
return (title, author, timestamp) return (title, author, timestamp)
def _dump_files(self, files, header=None,indent=0):
if header:
msg = '\n%sfiles passed to %s:' % (' '*indent,header)
self.log.info(msg)
self.log.info( "%s%s" % (' '*indent,'-' * len(msg)))
for file in files:
if getattr(file, 'orig_file_path', None) is not None:
self.log.info(" %s%s" % (' '*indent,file.orig_file_path))
elif getattr(file, 'name', None) is not None:
self.log.info(" %s%s" % (' '*indent,file.name))
self.log.info()
def _dump_hex(self, src, length=16): def _dump_hex(self, src, length=16):
''' '''
''' '''
@ -1698,7 +1705,7 @@ class ITUNES(DriverBase):
self.log.info() self.log.info()
def _dump_update_list(self,header=None,indent=0): def _dump_update_list(self,header=None,indent=0):
if header: if header and self.update_list:
msg = '\n%sself.update_list %s' % (' '*indent,header) msg = '\n%sself.update_list %s' % (' '*indent,header)
self.log.info(msg) self.log.info(msg)
self.log.info( "%s%s" % (' '*indent,'-' * len(msg))) self.log.info( "%s%s" % (' '*indent,'-' * len(msg)))
@ -1717,7 +1724,6 @@ class ITUNES(DriverBase):
(' '*indent, (' '*indent,
ub['title'], ub['title'],
ub['author'])) ub['author']))
self.log.info()
def _find_device_book(self, search): def _find_device_book(self, search):
''' '''
@ -2116,35 +2122,6 @@ class ITUNES(DriverBase):
self.log.error(" no iPad|Books playlist found") self.log.error(" no iPad|Books playlist found")
return pl return pl
def _get_fpath(self,file, metadata, format, update_md=False):
'''
If the database copy will be deleted after upload, we have to
use file (the PersistentTemporaryFile), which will be around until
calibre exits.
If we're using the database copy, delete the plist
'''
if DEBUG:
self.log.info(" ITUNES._get_fpath()")
fpath = file
if not getattr(fpath, 'deleted_after_upload', False):
if getattr(file, 'orig_file_path', None) is not None:
# Database copy
fpath = file.orig_file_path
self._delete_iTunesMetadata_plist(fpath)
elif getattr(file, 'name', None) is not None:
# PTF
fpath = file.name
else:
# Recipe - PTF
if DEBUG:
self.log.info(" file will be deleted after upload")
if format == 'epub' and update_md:
self._update_epub_metadata(fpath, metadata)
return fpath
def _get_library_books(self): def _get_library_books(self):
''' '''
Populate a dict of paths from iTunes Library|Books Populate a dict of paths from iTunes Library|Books
@ -2348,6 +2325,7 @@ class ITUNES(DriverBase):
self.iTunes = appscript.app('iTunes') self.iTunes = appscript.app('iTunes')
self.initial_status = 'already running' self.initial_status = 'already running'
'''
# Read the current storage path for iTunes media # Read the current storage path for iTunes media
cmd = "defaults read com.apple.itunes NSNavLastRootDirectory" cmd = "defaults read com.apple.itunes NSNavLastRootDirectory"
proc = subprocess.Popen( cmd, shell=True, cwd=os.curdir, stdout=subprocess.PIPE) proc = subprocess.Popen( cmd, shell=True, cwd=os.curdir, stdout=subprocess.PIPE)
@ -2358,12 +2336,13 @@ class ITUNES(DriverBase):
else: else:
self.log.error(" could not confirm valid iTunes.media_dir from %s" % 'com.apple.itunes') self.log.error(" could not confirm valid iTunes.media_dir from %s" % 'com.apple.itunes')
self.log.error(" media_dir: %s" % media_dir) self.log.error(" media_dir: %s" % media_dir)
'''
if DEBUG: if DEBUG:
self.log.info(" %s %s" % (__appname__, __version__)) self.log.info(" %s %s" % (__appname__, __version__))
self.log.info(" [OSX %s - %s (%s), driver version %d.%d.%d]" % self.log.info(" [OSX %s - %s (%s), driver version %d.%d.%d]" %
(self.iTunes.name(), self.iTunes.version(), self.initial_status, (self.iTunes.name(), self.iTunes.version(), self.initial_status,
self.version[0],self.version[1],self.version[2])) self.version[0],self.version[1],self.version[2]))
self.log.info(" iTunes_media: %s" % self.iTunes_media)
self.log.info(" calibre_library_path: %s" % self.calibre_library_path) self.log.info(" calibre_library_path: %s" % self.calibre_library_path)
if iswindows: if iswindows:
@ -2403,6 +2382,7 @@ class ITUNES(DriverBase):
' iTunes automation interface non-responsive, ' + ' iTunes automation interface non-responsive, ' +
'recommend reinstalling iTunes') 'recommend reinstalling iTunes')
'''
# Read the current storage path for iTunes media from the XML file # Read the current storage path for iTunes media from the XML file
media_dir = '' media_dir = ''
string = None string = None
@ -2421,13 +2401,13 @@ class ITUNES(DriverBase):
self.log.error(" '%s' not found" % media_dir) self.log.error(" '%s' not found" % media_dir)
else: else:
self.log.error(" no media dir found: string: %s" % string) self.log.error(" no media dir found: string: %s" % string)
'''
if DEBUG: if DEBUG:
self.log.info(" %s %s" % (__appname__, __version__)) self.log.info(" %s %s" % (__appname__, __version__))
self.log.info(" [Windows %s - %s (%s), driver version %d.%d.%d]" % self.log.info(" [Windows %s - %s (%s), driver version %d.%d.%d]" %
(self.iTunes.Windows[0].name, self.iTunes.Version, self.initial_status, (self.iTunes.Windows[0].name, self.iTunes.Version, self.initial_status,
self.version[0],self.version[1],self.version[2])) self.version[0],self.version[1],self.version[2]))
self.log.info(" iTunes_media: %s" % self.iTunes_media)
self.log.info(" calibre_library_path: %s" % self.calibre_library_path) self.log.info(" calibre_library_path: %s" % self.calibre_library_path)
def _purge_orphans(self,library_books, cached_books): def _purge_orphans(self,library_books, cached_books):
@ -2477,13 +2457,14 @@ class ITUNES(DriverBase):
(self.cached_books[book]['title'] == metadata.title and \ (self.cached_books[book]['title'] == metadata.title and \
self.cached_books[book]['author'] == authors_to_string(metadata.authors)): self.cached_books[book]['author'] == authors_to_string(metadata.authors)):
self.update_list.append(self.cached_books[book]) self.update_list.append(self.cached_books[book])
self._remove_from_device(self.cached_books[book])
if DEBUG: if DEBUG:
self.log.info( " deleting device book '%s'" % (metadata.title)) self.log.info( " deleting device book '%s'" % (metadata.title))
if not getattr(file, 'deleted_after_upload', False): self._remove_from_device(self.cached_books[book])
self._remove_from_iTunes(self.cached_books[book])
if DEBUG: if DEBUG:
self.log.info(" deleting library book '%s'" % metadata.title) self.log.info(" deleting library book '%s'" % metadata.title)
self._remove_from_iTunes(self.cached_books[book])
break break
else: else:
if DEBUG: if DEBUG:
@ -2496,9 +2477,9 @@ class ITUNES(DriverBase):
(self.cached_books[book]['title'] == metadata.title and \ (self.cached_books[book]['title'] == metadata.title and \
self.cached_books[book]['author'] == authors_to_string(metadata.authors)): self.cached_books[book]['author'] == authors_to_string(metadata.authors)):
self.update_list.append(self.cached_books[book]) self.update_list.append(self.cached_books[book])
self._remove_from_iTunes(self.cached_books[book])
if DEBUG: if DEBUG:
self.log.info( " deleting library book '%s'" % metadata.title) self.log.info( " deleting library book '%s'" % metadata.title)
self._remove_from_iTunes(self.cached_books[book])
break break
else: else:
if DEBUG: if DEBUG:
@ -2529,96 +2510,105 @@ class ITUNES(DriverBase):
def _remove_from_iTunes(self, cached_book): def _remove_from_iTunes(self, cached_book):
''' '''
iTunes does not delete books from storage when removing from database iTunes does not delete books from storage when removing from database via automation
We only want to delete stored copies if the file is stored in iTunes
We don't want to delete files stored outside of iTunes.
Also confirm that storage_path does not point into calibre's storage.
''' '''
if DEBUG: if DEBUG:
self.log.info(" ITUNES._remove_from_iTunes():") self.log.info(" ITUNES._remove_from_iTunes():")
if isosx: if isosx:
''' Manually remove the book from iTunes storage '''
try: try:
storage_path = os.path.split(cached_book['lib_book'].location().path) fp = cached_book['lib_book'].location().path
if cached_book['lib_book'].location().path.startswith(self.iTunes_media) and \ if DEBUG:
not storage_path[0].startswith(prefs['library_path']): self.log.info(" processing %s" % fp)
title_storage_path = storage_path[0] if fp.startswith(prefs['library_path']):
if DEBUG: self.log.info(" '%s' stored in calibre database, not removed" % cached_book['title'])
self.log.info(" removing title_storage_path: %s" % title_storage_path)
try:
shutil.rmtree(title_storage_path)
except:
self.log.info(" '%s' not empty" % title_storage_path)
# Clean up title/author directories
author_storage_path = os.path.split(title_storage_path)[0]
self.log.info(" author_storage_path: %s" % author_storage_path)
author_files = os.listdir(author_storage_path)
if '.DS_Store' in author_files:
author_files.pop(author_files.index('.DS_Store'))
if not author_files:
shutil.rmtree(author_storage_path)
if DEBUG:
self.log.info(" removing empty author_storage_path")
else:
if DEBUG:
self.log.info(" author_storage_path not empty (%d objects):" % len(author_files))
self.log.info(" %s" % '\n'.join(author_files))
else: else:
self.log.info(" '%s' (stored external to iTunes, no files deleted)" % cached_book['title']) if os.path.exists(fp):
os.remove(fp)
if DEBUG:
self.log.info(" deleting from iTunes storage")
author_storage_path = os.path.split(fp)[0]
try:
os.rmdir(author_storage_path)
if DEBUG:
self.log.info(" removing empty author directory")
except:
author_files = os.listdir(author_storage_path)
if '.DS_Store' in author_files:
author_files.pop(author_files.index('.DS_Store'))
if not author_files:
os.rmdir(author_storage_path)
if DEBUG:
self.log.info(" removing empty author directory")
'''
else:
if DEBUG:
self.log.info(" author_storage_path not empty:")
self.log.info(" %s" % '\n'.join(author_files))
'''
else:
self.log.info(" '%s' does not exist at storage location" % cached_book['title'])
except: except:
# We get here if there was an error with .location().path # We get here if there was an error with .location().path
if DEBUG: if DEBUG:
self.log.info(" '%s' not in iTunes storage" % cached_book['title']) self.log.info(" '%s' not found in iTunes storage" % cached_book['title'])
# Delete the book from the iTunes database
try: try:
self.iTunes.delete(cached_book['lib_book']) self.iTunes.delete(cached_book['lib_book'])
if DEBUG:
self.log.info(" removing from iTunes database")
except: except:
if DEBUG: if DEBUG:
self.log.info(" unable to remove '%s' from iTunes" % cached_book['title']) self.log.info(" unable to remove from iTunes database")
elif iswindows: elif iswindows:
''' '''
Assume we're wrapped in a pythoncom Assume we're wrapped in a pythoncom
Windows stores the book under a common author directory, so we just delete the .epub Windows stores the book under a common author directory, so we just delete the .epub
''' '''
fp = None
try: try:
book = cached_book['lib_book'] book = cached_book['lib_book']
path = book.Location fp = book.Location
except: except:
book = self._find_library_book(cached_book) book = self._find_library_book(cached_book)
if book: if book:
path = book.Location fp = book.Location
if book: if book:
if self.iTunes_media and path.startswith(self.iTunes_media) and \ if DEBUG:
not path.startswith(prefs['library_path']): self.log.info(" processing %s" % fp)
storage_path = os.path.split(path) if fp.startswith(prefs['library_path']):
if DEBUG: self.log.info(" '%s' stored in calibre database, not removed" % cached_book['title'])
self.log.info(" removing '%s' at %s" %
(cached_book['title'], path))
try:
os.remove(path)
except:
self.log.warning(" '%s' not in iTunes storage" % path)
try:
os.rmdir(storage_path[0])
self.log.info(" removed folder '%s'" % storage_path[0])
except:
self.log.info(" folder '%s' not found or not empty" % storage_path[0])
# Delete from iTunes database
else: else:
self.log.info(" '%s' (stored external to iTunes, no files deleted)" % cached_book['title']) if os.path.exists(fp):
os.remove(fp)
if DEBUG:
self.log.info(" deleting from iTunes storage")
author_storage_path = os.path.split(fp)[0]
try:
os.rmdir(author_storage_path)
if DEBUG:
self.log.info(" removing empty author directory")
except:
pass
else:
self.log.info(" '%s' does not exist at storage location" % cached_book['title'])
else: else:
if DEBUG: if DEBUG:
self.log.info(" '%s' not found in iTunes" % cached_book['title']) self.log.info(" '%s' not found in iTunes storage" % cached_book['title'])
# Delete the book from the iTunes database
try: try:
book.Delete() book.Delete()
if DEBUG:
self.log.info(" removing from iTunes database")
except: except:
if DEBUG: if DEBUG:
self.log.info(" unable to remove '%s' from iTunes" % cached_book['title']) self.log.info(" unable to remove from iTunes database")
def title_sorter(self, title): def title_sorter(self, title):
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip() return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip()
@ -2797,7 +2787,7 @@ class ITUNES(DriverBase):
if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]: if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]:
if DEBUG: if DEBUG:
self.log.info(" ITUNES._update_iTunes_metadata()") self.log.info(" ITUNES._update_iTunes_metadata()")
self.log.info(" using Series name as Genre") self.log.info(" using Series name '%s' as Genre" % metadata_x.series)
# Format the index as a sort key # Format the index as a sort key
index = metadata_x.series_index index = metadata_x.series_index
@ -2977,8 +2967,8 @@ class ITUNES(DriverBase):
newmi = book.deepcopy_metadata() newmi = book.deepcopy_metadata()
newmi.template_to_attribute(book, pb) newmi.template_to_attribute(book, pb)
if pb is not None and DEBUG: if pb is not None and DEBUG:
self.log.info(" transforming %s using %s:" % (format, pb)) #self.log.info(" transforming %s using %s:" % (format, pb))
self.log.info(" title: %s %s" % (book.title, ">>> %s" % self.log.info(" title: '%s' %s" % (book.title, ">>> '%s'" %
newmi.title if book.title != newmi.title else '')) newmi.title if book.title != newmi.title else ''))
self.log.info(" title_sort: %s %s" % (book.title_sort, ">>> %s" % self.log.info(" title_sort: %s %s" % (book.title_sort, ">>> %s" %
newmi.title_sort if book.title_sort != newmi.title_sort else '')) newmi.title_sort if book.title_sort != newmi.title_sort else ''))
@ -3082,12 +3072,12 @@ class ITUNES_ASYNC(ITUNES):
this_book.datetime = parse_date(str(library_books[book].date_added())).timetuple() this_book.datetime = parse_date(str(library_books[book].date_added())).timetuple()
except: except:
this_book.datetime = time.gmtime() this_book.datetime = time.gmtime()
this_book.db_id = None
this_book.device_collections = [] this_book.device_collections = []
#this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None #this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
this_book.library_id = library_books[book] this_book.library_id = library_books[book]
this_book.size = library_books[book].size() this_book.size = library_books[book].size()
this_book.uuid = library_books[book].composer() this_book.uuid = library_books[book].composer()
this_book.cid = None
# Hack to discover if we're running in GUI environment # Hack to discover if we're running in GUI environment
if self.report_progress is not None: if self.report_progress is not None:
this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book]) this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book])
@ -3123,11 +3113,11 @@ class ITUNES_ASYNC(ITUNES):
this_book.datetime = parse_date(str(library_books[book].DateAdded)).timetuple() this_book.datetime = parse_date(str(library_books[book].DateAdded)).timetuple()
except: except:
this_book.datetime = time.gmtime() this_book.datetime = time.gmtime()
this_book.db_id = None
this_book.device_collections = [] this_book.device_collections = []
this_book.library_id = library_books[book] this_book.library_id = library_books[book]
this_book.size = library_books[book].Size this_book.size = library_books[book].Size
this_book.uuid = library_books[book].Composer this_book.uuid = library_books[book].Composer
this_book.cid = None
# Hack to discover if we're running in GUI environment # Hack to discover if we're running in GUI environment
if self.report_progress is not None: if self.report_progress is not None:
this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book]) this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book])

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): def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType, expired, favouritesindex):
changed = False changed = False
try: try:
lpath = path.partition(self.normalize_path(prefix))[2] lpath = path.partition(self.normalize_path(prefix))[2]
@ -111,12 +111,23 @@ class KOBO(USBMS):
playlist_map = {} playlist_map = {}
if lpath not in playlist_map:
playlist_map[lpath] = []
if readstatus == 1: if readstatus == 1:
playlist_map[lpath]= "Im_Reading" playlist_map[lpath].append('Im_Reading')
elif readstatus == 2: elif readstatus == 2:
playlist_map[lpath]= "Read" playlist_map[lpath].append('Read')
elif readstatus == 3: elif readstatus == 3:
playlist_map[lpath]= "Closed" playlist_map[lpath].append('Closed')
# Related to a bug in the Kobo firmware that leaves an expired row for deleted books
# this shows an expired Collection so the user can decide to delete the book
if expired == 3:
playlist_map[lpath].append('Expired')
# A SHORTLIST is supported on the touch but the data field is there on most earlier models
if favouritesindex == 1:
playlist_map[lpath].append('Shortlist')
path = self.normalize_path(path) path = self.normalize_path(path)
# print "Normalized FileName: " + path # print "Normalized FileName: " + path
@ -126,7 +137,13 @@ class KOBO(USBMS):
bl_cache[lpath] = None bl_cache[lpath] = None
if ImageID is not None: if ImageID is not None:
imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed') imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed')
if not os.path.exists(imagename):
# Try the Touch version if the image does not exist
imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - N3_LIBRARY_FULL.parsed')
#print "Image name Normalized: " + imagename #print "Image name Normalized: " + imagename
if not os.path.exists(imagename):
debug_print("Strange - The image name does not exist - title: ", title)
if imagename is not None: if imagename is not None:
bl[idx].thumbnail = ImageWrapper(imagename) bl[idx].thumbnail = ImageWrapper(imagename)
if (ContentType != '6' and MimeType != 'Shortcover'): if (ContentType != '6' and MimeType != 'Shortcover'):
@ -138,7 +155,7 @@ class KOBO(USBMS):
debug_print(" Strange: The file: ", prefix, lpath, " does mot exist!") debug_print(" Strange: The file: ", prefix, lpath, " does mot exist!")
if lpath in playlist_map and \ if lpath in playlist_map and \
playlist_map[lpath] not in bl[idx].device_collections: playlist_map[lpath] not in bl[idx].device_collections:
bl[idx].device_collections.append(playlist_map[lpath]) bl[idx].device_collections = playlist_map.get(lpath,[])
else: else:
if ContentType == '6' and MimeType == 'Shortcover': if ContentType == '6' and MimeType == 'Shortcover':
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576) book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
@ -157,7 +174,7 @@ class KOBO(USBMS):
raise raise
# print 'Update booklist' # print 'Update booklist'
book.device_collections = [playlist_map[lpath]] if lpath in playlist_map else [] book.device_collections = playlist_map.get(lpath,[])# if lpath in playlist_map else []
if bl.add_book(book, replace_metadata=False): if bl.add_book(book, replace_metadata=False):
changed = True changed = True
@ -186,24 +203,30 @@ class KOBO(USBMS):
result = cursor.fetchone() result = cursor.fetchone()
self.dbversion = result[0] self.dbversion = result[0]
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \ if self.dbversion >= 14:
'ImageID, ReadStatus from content where BookID is Null' 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) cursor.execute (query)
changed = False changed = False
for i, row in enumerate(cursor): for i, row in enumerate(cursor):
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...')) # self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
if row[3].startswith("file:///usr/local/Kobo/help/"):
# These are internal to the Kobo device and do not exist
continue
path = self.path_from_contentid(row[3], row[5], row[4], oncard) path = self.path_from_contentid(row[3], row[5], row[4], oncard)
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip' mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
# debug_print("mime:", mime) # debug_print("mime:", mime)
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"): if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4]) changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9])
# print "shortbook: " + path # print "shortbook: " + path
elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"): elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4]) changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9])
if changed: if changed:
need_sync = True need_sync = True
@ -267,8 +290,12 @@ class KOBO(USBMS):
cursor.execute('delete from content_keys where volumeid = ?', t) cursor.execute('delete from content_keys where volumeid = ?', t)
# Delete the chapters associated with the book next # Delete the chapters associated with the book next
t = (ContentID,ContentID,) t = (ContentID,)
cursor.execute('delete from content where BookID = ? or ContentID = ?', t) # Kobo does not delete the Book row (ie the row where the BookID is Null)
# The next server sync should remove the row
cursor.execute('delete from content where BookID = ?', t)
cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0, ___ExpirationStatus=3 ' \
'where BookID is Null and ContentID =?',t)
connection.commit() connection.commit()
@ -286,7 +313,7 @@ class KOBO(USBMS):
path_prefix = '.kobo/images/' path_prefix = '.kobo/images/'
path = self._main_prefix + path_prefix + ImageID path = self._main_prefix + path_prefix + ImageID
file_endings = (' - iPhoneThumbnail.parsed', ' - bbMediumGridList.parsed', ' - NickelBookCover.parsed',) file_endings = (' - iPhoneThumbnail.parsed', ' - bbMediumGridList.parsed', ' - NickelBookCover.parsed', ' - N3_LIBRARY_FULL.parsed', ' - N3_LIBRARY_GRID.parsed', ' - N3_LIBRARY_LIST.parsed', ' - N3_SOCIAL_CURRENTREAD.parsed',)
for ending in file_endings: for ending in file_endings:
fpath = path + ending fpath = path + ending
@ -450,7 +477,10 @@ class KOBO(USBMS):
path = self._main_prefix + path + '.kobo' path = self._main_prefix + path + '.kobo'
# print "Path: " + path # print "Path: " + path
elif (ContentType == "6" or ContentType == "10") and MimeType == 'application/x-kobo-epub+zip': elif (ContentType == "6" or ContentType == "10") and MimeType == 'application/x-kobo-epub+zip':
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 # print "Internal: " + path
else: else:
# if path.startswith("file:///mnt/onboard/"): # if path.startswith("file:///mnt/onboard/"):
@ -527,6 +557,7 @@ class KOBO(USBMS):
if collections: if collections:
# Process any collections that exist # Process any collections that exist
for category, books in collections.items(): for category, books in collections.items():
# debug_print (category)
if category == 'Im_Reading': if category == 'Im_Reading':
# Reset Im_Reading list in the database # Reset Im_Reading list in the database
if oncard == 'carda': if oncard == 'carda':
@ -545,7 +576,8 @@ class KOBO(USBMS):
for book in books: for book in books:
# debug_print('Title:', book.title, 'lpath:', book.path) # debug_print('Title:', book.title, 'lpath:', book.path)
book.device_collections = ['Im_Reading'] if 'Im_Reading' not in book.device_collections:
book.device_collections.append('Im_Reading')
extension = os.path.splitext(book.path)[1] extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path) ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
@ -588,7 +620,8 @@ class KOBO(USBMS):
for book in books: for book in books:
# debug_print('Title:', book.title, 'lpath:', book.path) # debug_print('Title:', book.title, 'lpath:', book.path)
book.device_collections = ['Read'] if 'Read' not in book.device_collections:
book.device_collections.append('Read')
extension = os.path.splitext(book.path)[1] extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path) ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
@ -624,7 +657,8 @@ class KOBO(USBMS):
for book in books: for book in books:
# debug_print('Title:', book.title, 'lpath:', book.path) # debug_print('Title:', book.title, 'lpath:', book.path)
book.device_collections = ['Closed'] if 'Closed' not in book.device_collections:
book.device_collections.append('Closed')
extension = os.path.splitext(book.path)[1] extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path) ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
@ -642,6 +676,44 @@ class KOBO(USBMS):
else: else:
connection.commit() connection.commit()
# debug_print('Database: Commit set ReadStatus as Closed') # debug_print('Database: Commit set ReadStatus as Closed')
if category == 'Shortlist':
# Reset FavouritesIndex list in the database
if oncard == 'carda':
query= 'update content set FavouritesIndex=-1 where BookID is Null and ContentID like \'file:///mnt/sd/%\''
elif oncard != 'carda' and oncard != 'cardb':
query= 'update content set FavouritesIndex=-1 where BookID is Null and ContentID not like \'file:///mnt/sd/%\''
try:
cursor.execute (query)
except:
debug_print('Database Exception: Unable to reset Shortlist list')
raise
else:
# debug_print('Commit: Reset Shortlist list')
connection.commit()
for book in books:
# debug_print('Title:', book.title, 'lpath:', book.path)
if 'Shortlist' not in book.device_collections:
book.device_collections.append('Shortlist')
# debug_print ("Shortlist found for: ", book.title)
extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
ContentID = self.contentid_from_path(book.path, ContentType)
# datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
t = (ContentID,)
try:
cursor.execute('update content set FavouritesIndex=1 where BookID is Null and ContentID = ?', t)
except:
debug_print('Database Exception: Unable set book as Shortlist')
raise
else:
connection.commit()
# debug_print('Database: Commit set Shortlist as Shortlist')
else: # No collections else: # No collections
# Since no collections exist the ReadStatus needs to be reset to 0 (Unread) # Since no collections exist the ReadStatus needs to be reset to 0 (Unread)
print "Reseting ReadStatus to 0" print "Reseting ReadStatus to 0"

View File

@ -19,8 +19,9 @@ class TECLAST_K3(USBMS):
PRODUCT_ID = [0x3203] PRODUCT_ID = [0x3203]
BCD = [0x0000, 0x0100] BCD = [0x0000, 0x0100]
VENDOR_NAME = 'TECLAST' VENDOR_NAME = ['TECLAST', 'IMAGIN']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['DIGITAL_PLAYER', 'TL-K5'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['DIGITAL_PLAYER', 'TL-K5',
'EREADER']
MAIN_MEMORY_VOLUME_LABEL = 'K3 Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'K3 Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'K3 Storage Card' STORAGE_CARD_VOLUME_LABEL = 'K3 Storage Card'

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,27 +201,35 @@ 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:
if isbytestring(fpath): try:
fpath = fpath.decode(filesystem_encoding) self.db.copy_format_to(i, fmt, f, index_is_id=True)
formats[fmt.lower()] = fpath 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()] data[i] = [opf, cpath, formats, mi.last_modified.isoformat()]
return data return data
@ -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

@ -0,0 +1,40 @@
#!/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'), 'search.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()

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,12 +445,14 @@ 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
self.rq = Queue() self.rq = Queue()
self.ids = [x for x in map(db.id, [r.row() for r in rows]) if x is not None] self.ids = [x for x in map(db.id, [r.row() for r in rows]) if x is not None]
self.pd.set_max(len(self.ids)) self.pd_max = len(self.ids)
self.pd.set_max(0)
self.pd.value = 0 self.pd.value = 0
self.failures = set([]) self.failures = set([])
@ -509,6 +511,8 @@ class Saver(QObject): # {{{
id, title, ok, tb = self.rq.get_nowait() id, title, ok, tb = self.rq.get_nowait()
except Empty: except Empty:
return return
if self.pd.max != self.pd_max:
self.pd.max = self.pd_max
self.pd.value += 1 self.pd.value += 1
self.ids.remove(id) self.ids.remove(id)
if not isinstance(title, unicode): if not isinstance(title, unicode):

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

@ -25,7 +25,7 @@ class Base(object):
def __init__(self, db, col_id, parent=None): def __init__(self, db, col_id, parent=None):
self.db, self.col_id = db, col_id self.db, self.col_id = db, col_id
self.col_metadata = db.custom_column_num_map[col_id] self.col_metadata = db.custom_column_num_map[col_id]
self.initial_val = None self.initial_val = self.widgets = None
self.setup_ui(parent) self.setup_ui(parent)
def initialize(self, book_id): def initialize(self, book_id):
@ -54,6 +54,9 @@ class Base(object):
def normalize_ui_val(self, val): def normalize_ui_val(self, val):
return val return val
def break_cycles(self):
self.db = self.widgets = self.initial_val = None
class Bool(Base): class Bool(Base):
def setup_ui(self, parent): def setup_ui(self, parent):

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__)
return self.device.upload_books(files, names, on_card, try:
metadata=metadata, end_session=False) 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, 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()
for f in files: if files:
getattr(f, 'close', lambda : True)() 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): 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])
@ -361,7 +362,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
fm = self.db.field_metadata fm = self.db.field_metadata
for f in fm: for f in fm:
if (f in ['author_sort'] or if (f in ['author_sort'] or
(fm[f]['datatype'] in ['text', 'series', 'enumeration'] (fm[f]['datatype'] in ['text', 'series', 'enumeration', 'comments']
and fm[f].get('search_terms', None) and fm[f].get('search_terms', None)
and f not in ['formats', 'ondevice']) or and f not in ['formats', 'ondevice']) or
(fm[f]['datatype'] in ['int', 'float', 'bool'] and (fm[f]['datatype'] in ['int', 'float', 'bool'] and

View File

@ -53,6 +53,13 @@ class ProgressDialog(QDialog, Ui_Dialog):
def set_max(self, max): def set_max(self, max):
self.bar.setMaximum(max) self.bar.setMaximum(max)
@dynamic_property
def max(self):
def fget(self): return self.bar.maximum()
def fset(self, val): self.bar.setMaximum(val)
return property(fget=fget, fset=fset)
def _canceled(self, *args): def _canceled(self, *args):
self.canceled = True self.canceled = True
self.button_box.setDisabled(True) self.button_box.setDisabled(True)

View File

@ -0,0 +1,237 @@
#!/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, QCoreApplication)
from calibre.gui2.dialogs.quickview_ui import Ui_Quickview
from calibre.utils.icu import sort_key
from calibre.gui2 import gprefs
class TableItem(QTableWidgetItem):
'''
A QTableWidgetItem that sorts on a separate string and uses ICU rules
'''
def __init__(self, val, sort):
self.sort = sort
QTableWidgetItem.__init__(self, val)
self.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable)
def __ge__(self, other):
return sort_key(self.sort) >= sort_key(other.sort)
def __lt__(self, other):
return sort_key(self.sort) < sort_key(other.sort)
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
self.books_table_column_widths = None
try:
self.books_table_column_widths = \
gprefs.get('quickview_dialog_books_table_widths', None)
geom = gprefs.get('quickview_dialog_geometry', bytearray(''))
self.restoreGeometry(QByteArray(geom))
except:
pass
# Remove the help button from the window title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.db = view.model().db
self.view = view
self.gui = gui
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.items.setSelectionMode(QAbstractItemView.SingleSelection)
self.items.currentTextChanged.connect(self.item_selected)
# Set up the books table columns
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.books_table.sortByColumn(0, Qt.AscendingOrder)
# get the standard table row height. Do this here because calling
# resizeRowsToContents can word wrap long cell contents, creating
# double-high rows
self.books_table.setRowCount(1)
self.books_table.setItem(0, 0, TableItem('A', ''))
self.books_table.resizeRowsToContents()
self.books_table_row_height = self.books_table.rowHeight(0)
self.books_table.setRowCount(0)
# Add the data
self.refresh(row)
self.view.selectionModel().currentChanged[QModelIndex,QModelIndex].connect(self.slave)
QCoreApplication.instance().aboutToQuit.connect(self.save_state)
self.search_button.clicked.connect(self.do_search)
# search button
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)
# clicks on the items listWidget
def item_selected(self, txt):
self.fill_in_books_box(unicode(txt))
# Given a cell in the library view, display the information
def refresh(self, idx):
bv_row = idx.row()
key = self.view.model().column_map[idx.column()]
book_id = self.view.model().id(bv_row)
# Double-clicking on a book to show it in the library view will result
# in a signal emitted for column 1 of the book row. Use the original
# column for this signal.
if self.use_current_key_for_next_refresh:
key = self.current_key
self.use_current_key_for_next_refresh = False
else:
# Only show items for categories
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.blockSignals(True)
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 vals:
if not isinstance(vals, list):
vals = [vals]
vals.sort(key=sort_key)
for v in vals:
a = QListWidgetItem(v)
self.items.addItem(a)
self.items.setCurrentRow(0)
self.current_book_id = book_id
self.current_key = key
self.fill_in_books_box(vals[0])
self.items.blockSignals(False)
def fill_in_books_box(self, selected_item):
# Do a bit of fix-up on the items so that the search works.
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_item = 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, mi.title_sort)
a.setData(Qt.UserRole, b)
self.books_table.setItem(row, 0, a)
if b == self.current_book_id:
select_item = a
a = TableItem(' & '.join(mi.authors), mi.author_sort)
self.books_table.setItem(row, 1, a)
series = mi.format_field('series')[1]
if series is None:
series = ''
a = TableItem(series, series)
self.books_table.setItem(row, 2, a)
self.books_table.setRowHeight(row, self.books_table_row_height)
self.books_table.setSortingEnabled(True)
if select_item is not None:
self.books_table.setCurrentItem(select_item)
self.books_table.scrollToItem(select_item, QAbstractItemView.PositionAtCenter)
# Deal with sizing the table columns. Done here because the numbers are not
# correct until the first paint.
def resizeEvent(self, *args):
QDialog.resizeEvent(self, *args)
if self.books_table_column_widths is not None:
for c,w in enumerate(self.books_table_column_widths):
self.books_table.setColumnWidth(c, w)
else:
# the vertical scroll bar might not be rendered, so might not yet
# have a width. Assume 25. Not a problem because user-changed column
# widths will be remembered
w = self.books_table.width() - 25 - self.books_table.verticalHeader().width()
w /= self.books_table.columnCount()
for c in range(0, self.books_table.columnCount()):
self.books_table.setColumnWidth(c, w)
self.save_state()
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]])
# called when a book is clicked on the library view
def slave(self, current, previous):
if self.is_closed:
return
self.refresh(current)
self.view.activateWindow()
def save_state(self):
if self.is_closed:
return
self.books_table_column_widths = []
for c in range(0, self.books_table.columnCount()):
self.books_table_column_widths.append(self.books_table.columnWidth(c))
gprefs['quickview_dialog_books_table_widths'] = self.books_table_column_widths
gprefs['quickview_dialog_geometry'] = bytearray(self.saveGeometry())
def close(self):
self.save_state()
# clean up to prevent memory leaks
self.db = self.view = self.gui = None
self.is_closed = True
# called by the window system
def closeEvent(self, *args):
self.close()
QDialog.closeEvent(self, *args)
# called by the close button
def reject(self):
self.close()
QDialog.reject(self)

View 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>

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,17 @@ 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) try:
ans = db.format_path(i, fmt, index_is_id=True)
except:
ans = None
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,9 +602,10 @@ 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) try:
if ans is not None: ans = db.format_path(i, f, index_is_id=True)
break except:
ans = None
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)

View File

@ -21,9 +21,10 @@ from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import (title_sort, authors_to_string, from calibre.ebooks.metadata import (title_sort, authors_to_string,
string_to_authors, check_isbn, authors_to_sort_string) string_to_authors, check_isbn, authors_to_sort_string)
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE,
choose_files, error_dialog, choose_images) choose_files, error_dialog, choose_images)
from calibre.utils.date import local_tz, qt_to_dt from calibre.utils.date import (local_tz, qt_to_dt, as_local_time,
UNDEFINED_DATE)
from calibre import strftime from calibre import strftime
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
@ -125,6 +126,9 @@ class TitleEdit(EnLineEdit):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def break_cycles(self):
self.dialog = None
class TitleSortEdit(TitleEdit): class TitleSortEdit(TitleEdit):
TITLE_ATTR = 'title_sort' TITLE_ATTR = 'title_sort'
@ -150,6 +154,7 @@ class TitleSortEdit(TitleEdit):
self.title_edit.textChanged.connect(self.update_state) self.title_edit.textChanged.connect(self.update_state)
self.textChanged.connect(self.update_state) self.textChanged.connect(self.update_state)
self.autogen_button = autogen_button
autogen_button.clicked.connect(self.auto_generate) autogen_button.clicked.connect(self.auto_generate)
self.update_state() self.update_state()
@ -168,6 +173,9 @@ class TitleSortEdit(TitleEdit):
def auto_generate(self, *args): def auto_generate(self, *args):
self.current_val = title_sort(self.title_edit.current_val) self.current_val = title_sort(self.title_edit.current_val)
self.title_edit.textChanged.disconnect()
self.textChanged.disconnect()
self.autogen_button.clicked.disconnect()
# }}} # }}}
@ -185,6 +193,7 @@ class AuthorsEdit(MultiCompleteComboBox):
self.setWhatsThis(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP)
self.setEditable(True) self.setEditable(True)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
self.manage_authors_signal = manage_authors
manage_authors.triggered.connect(self.manage_authors) manage_authors.triggered.connect(self.manage_authors)
def manage_authors(self): def manage_authors(self):
@ -269,6 +278,10 @@ class AuthorsEdit(MultiCompleteComboBox):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def break_cycles(self):
self.db = self.dialog = None
self.manage_authors_signal.triggered.disconnect()
class AuthorSortEdit(EnLineEdit): class AuthorSortEdit(EnLineEdit):
TOOLTIP = _('Specify how the author(s) of this book should be sorted. ' TOOLTIP = _('Specify how the author(s) of this book should be sorted. '
@ -297,6 +310,10 @@ class AuthorSortEdit(EnLineEdit):
self.authors_edit.editTextChanged.connect(self.update_state_and_val) self.authors_edit.editTextChanged.connect(self.update_state_and_val)
self.textChanged.connect(self.update_state) self.textChanged.connect(self.update_state)
self.autogen_button = autogen_button
self.copy_a_to_as_action = copy_a_to_as_action
self.copy_as_to_a_action = copy_as_to_a_action
autogen_button.clicked.connect(self.auto_generate) autogen_button.clicked.connect(self.auto_generate)
copy_a_to_as_action.triggered.connect(self.auto_generate) copy_a_to_as_action.triggered.connect(self.auto_generate)
copy_as_to_a_action.triggered.connect(self.copy_to_authors) copy_as_to_a_action.triggered.connect(self.copy_to_authors)
@ -368,6 +385,15 @@ class AuthorSortEdit(EnLineEdit):
db.set_author_sort(id_, aus, notify=False, commit=False) db.set_author_sort(id_, aus, notify=False, commit=False)
return True return True
def break_cycles(self):
self.db = None
self.authors_edit.editTextChanged.disconnect()
self.textChanged.disconnect()
self.autogen_button.clicked.disconnect()
self.copy_a_to_as_action.triggered.disconnect()
self.copy_as_to_a_action.triggered.disconnect()
self.authors_edit = None
# }}} # }}}
# Series {{{ # Series {{{
@ -427,6 +453,10 @@ class SeriesEdit(MultiCompleteComboBox):
commit=True, allow_case_change=True) commit=True, allow_case_change=True)
return True return True
def break_cycles(self):
self.dialog = None
class SeriesIndexEdit(QDoubleSpinBox): class SeriesIndexEdit(QDoubleSpinBox):
TOOLTIP = '' TOOLTIP = ''
@ -488,6 +518,11 @@ class SeriesIndexEdit(QDoubleSpinBox):
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def break_cycles(self):
self.series_edit.currentIndexChanged.disconnect()
self.series_edit.editTextChanged.disconnect()
self.series_edit.lineEdit().editingFinished.disconnect()
self.db = self.series_edit = self.dialog = None
# }}} # }}}
@ -688,7 +723,8 @@ class FormatsManager(QWidget): # {{{
else: else:
stream = open(fmt.path, 'r+b') stream = open(fmt.path, 'r+b')
try: try:
mi = get_metadata(stream, ext) with stream:
mi = get_metadata(stream, ext)
return mi, ext return mi, ext
except: except:
error_dialog(self, _('Could not read metadata'), error_dialog(self, _('Could not read metadata'),
@ -698,6 +734,8 @@ class FormatsManager(QWidget): # {{{
if old != prefs['read_file_metadata']: if old != prefs['read_file_metadata']:
prefs['read_file_metadata'] = old prefs['read_file_metadata'] = old
def break_cycles(self):
self.dialog = None
# }}} # }}}
class Cover(ImageView): # {{{ class Cover(ImageView): # {{{
@ -859,6 +897,10 @@ class Cover(ImageView): # {{{
db.remove_cover(id_, notify=False, commit=False) db.remove_cover(id_, notify=False, commit=False)
return True return True
def break_cycles(self):
self.cover_changed.disconnect()
self.dialog = self._cdata = self.current_val = self.original_val = None
# }}} # }}}
class CommentsEdit(Editor): # {{{ class CommentsEdit(Editor): # {{{
@ -1210,6 +1252,7 @@ class DateEdit(QDateEdit): # {{{
def fset(self, val): def fset(self, val):
if val is None: if val is None:
val = UNDEFINED_DATE val = UNDEFINED_DATE
val = as_local_time(val)
self.setDate(QDate(val.year, val.month, val.day)) self.setDate(QDate(val.year, val.month, val.day))
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)

View File

@ -481,6 +481,13 @@ class MetadataSingleDialogBase(ResizableDialog):
x = getattr(self, b, None) x = getattr(self, b, None)
if x is not None: if x is not None:
disconnect(x.clicked) disconnect(x.clicked)
for widget in self.basic_metadata_widgets:
bc = getattr(widget, 'break_cycles', None)
if bc is not None and callable(bc):
bc()
for widget in getattr(self, 'custom_metadata_widgets', []):
widget.break_cycles()
# }}} # }}}
class Splitter(QSplitter): class Splitter(QSplitter):

View File

@ -72,19 +72,27 @@ class ConditionEditor(QWidget): # {{{
self.l = l = QGridLayout(self) self.l = l = QGridLayout(self)
self.setLayout(l) self.setLayout(l)
self.l1 = l1 = QLabel(_('If the ')) texts = _('If the ___ column ___ values')
try:
one, two, three = texts.split('___')
except:
one, two, three = 'If the ', ' column ', ' value '
self.l1 = l1 = QLabel(one)
l.addWidget(l1, 0, 0) l.addWidget(l1, 0, 0)
self.column_box = QComboBox(self) self.column_box = QComboBox(self)
l.addWidget(self.column_box, 0, 1) l.addWidget(self.column_box, 0, 1)
self.l2 = l2 = QLabel(_(' column '))
self.l2 = l2 = QLabel(two)
l.addWidget(l2, 0, 2) l.addWidget(l2, 0, 2)
self.action_box = QComboBox(self) self.action_box = QComboBox(self)
l.addWidget(self.action_box, 0, 3) l.addWidget(self.action_box, 0, 3)
self.l3 = l3 = QLabel(_(' value ')) self.l3 = l3 = QLabel(three)
l.addWidget(l3, 0, 4) l.addWidget(l3, 0, 4)
self.value_box = QLineEdit(self) self.value_box = QLineEdit(self)

View File

@ -10,6 +10,7 @@ from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting
from calibre.gui2.preferences.misc_ui import Ui_Form from calibre.gui2.preferences.misc_ui import Ui_Form
from calibre.gui2 import error_dialog, config, open_local_file, info_dialog from calibre.gui2 import error_dialog, config, open_local_file, info_dialog
from calibre.constants import isosx from calibre.constants import isosx
from calibre import get_proxies
class WorkersSetting(Setting): class WorkersSetting(Setting):
@ -33,6 +34,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.user_defined_device_button.clicked.connect(self.user_defined_device) self.user_defined_device_button.clicked.connect(self.user_defined_device)
self.button_osx_symlinks.clicked.connect(self.create_symlinks) self.button_osx_symlinks.clicked.connect(self.create_symlinks)
self.button_osx_symlinks.setVisible(isosx) self.button_osx_symlinks.setVisible(isosx)
proxies = get_proxies(debug=False)
txt = _('No proxies used')
if proxies:
lines = ['<br><code>%s: %s</code>'%(t, p) for t, p in
proxies.iteritems()]
txt = _('<b>Using proxies:</b>') + ''.join(lines)
self.proxies.setText(txt)
def debug_device_detection(self, *args): def debug_device_detection(self, *args):
from calibre.gui2.preferences.device_debug import DebugDevice from calibre.gui2.preferences.device_debug import DebugDevice

View File

@ -118,7 +118,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="20" column="0"> <item row="21" column="0">
<spacer name="verticalSpacer_9"> <spacer name="verticalSpacer_9">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -131,6 +131,13 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="10" column="0" colspan="2">
<widget class="QLabel" name="proxies">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -149,7 +149,8 @@ class TagsView(QTreeView): # {{{
hidden_categories=self.hidden_categories, hidden_categories=self.hidden_categories,
search_restriction=None, search_restriction=None,
drag_drop_finished=self.drag_drop_finished, drag_drop_finished=self.drag_drop_finished,
collapse_model=self.collapse_model) collapse_model=self.collapse_model,
state_map={})
self.pane_is_visible = True # because TagsModel.init did a recount self.pane_is_visible = True # because TagsModel.init did a recount
self.sort_by = sort_by self.sort_by = sort_by
self.tag_match = tag_match self.tag_match = tag_match
@ -173,6 +174,7 @@ class TagsView(QTreeView): # {{{
self.made_connections = True self.made_connections = True
self.refresh_signal_processed = True self.refresh_signal_processed = True
db.add_listener(self.database_changed) db.add_listener(self.database_changed)
self.expanded.connect(self.item_expanded)
def database_changed(self, event, ids): def database_changed(self, event, ids):
if self.refresh_signal_processed: if self.refresh_signal_processed:
@ -541,6 +543,10 @@ class TagsView(QTreeView): # {{{
return self.isExpanded(idx) return self.isExpanded(idx)
def recount(self, *args): def recount(self, *args):
'''
Rebuild the category tree, expand any categories that were expanded,
reset the search states, and reselect the current node.
'''
if self.disable_recounting or not self.pane_is_visible: if self.disable_recounting or not self.pane_is_visible:
return return
self.refresh_signal_processed = True self.refresh_signal_processed = True
@ -548,18 +554,23 @@ class TagsView(QTreeView): # {{{
if not ci.isValid(): if not ci.isValid():
ci = self.indexAt(QPoint(10, 10)) ci = self.indexAt(QPoint(10, 10))
path = self.model().path_for_index(ci) if self.is_visible(ci) else None path = self.model().path_for_index(ci) if self.is_visible(ci) else None
try: expanded_categories, state_map = self.model().get_state()
if not self.model().refresh(): # categories changed! self.set_new_model(state_map=state_map)
self.set_new_model() for category in expanded_categories:
path = None self.expand(self.model().index_for_category(category))
except: #Database connection could be closed if an integrity check is happening
pass
self._model.show_item_at_path(path) self._model.show_item_at_path(path)
# If the number of user categories changed, if custom columns have come or def item_expanded(self, idx):
# gone, or if columns have been hidden or restored, we must rebuild the '''
# model. Reason: it is much easier than reconstructing the browser tree. Called by the expanded signal
def set_new_model(self, filter_categories_by=None): '''
self.setCurrentIndex(idx)
def set_new_model(self, filter_categories_by=None, state_map={}):
'''
There are cases where we need to rebuild the category tree without
attempting to reposition the current node.
'''
try: try:
old = getattr(self, '_model', None) old = getattr(self, '_model', None)
if old is not None: if old is not None:
@ -569,7 +580,8 @@ class TagsView(QTreeView): # {{{
search_restriction=self.search_restriction, search_restriction=self.search_restriction,
drag_drop_finished=self.drag_drop_finished, drag_drop_finished=self.drag_drop_finished,
filter_categories_by=filter_categories_by, filter_categories_by=filter_categories_by,
collapse_model=self.collapse_model) collapse_model=self.collapse_model,
state_map=state_map)
self.setModel(self._model) self.setModel(self._model)
except: except:
# The DB must be gone. Set the model to None and hope that someone # The DB must be gone. Set the model to None and hope that someone
@ -627,7 +639,8 @@ class TagTreeItem(object): # {{{
except: except:
pass pass
self.parent = self.icon_state_map = self.bold_font = self.tag = \ self.parent = self.icon_state_map = self.bold_font = self.tag = \
self.icon = self.children = None self.icon = self.children = self.tooltip = \
self.py_name = self.id_set = self.category_key = None
def __str__(self): def __str__(self):
if self.type == self.ROOT: if self.type == self.ROOT:
@ -751,7 +764,8 @@ class TagsModel(QAbstractItemModel): # {{{
def __init__(self, db, parent, hidden_categories=None, def __init__(self, db, parent, hidden_categories=None,
search_restriction=None, drag_drop_finished=None, search_restriction=None, drag_drop_finished=None,
filter_categories_by=None, collapse_model='disable'): filter_categories_by=None, collapse_model='disable',
state_map={}):
QAbstractItemModel.__init__(self, parent) QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication # must do this here because 'QPixmap: Must construct a QApplication
@ -775,10 +789,10 @@ class TagsModel(QAbstractItemModel): # {{{
self.filter_categories_by = filter_categories_by self.filter_categories_by = filter_categories_by
self.collapse_model = collapse_model self.collapse_model = collapse_model
# get_node_tree cannot return None here, because row_map is empty. Note # Note that _get_category_nodes can indirectly change the
# that get_node_tree can indirectly change the user_categories dict. # user_categories dict.
data = self.get_node_tree(config['sort_tags_by']) data = self._get_category_nodes(config['sort_tags_by'])
gst = db.prefs.get('grouped_search_terms', {}) gst = db.prefs.get('grouped_search_terms', {})
self.root_item = TagTreeItem(icon_map=self.icon_state_map) self.root_item = TagTreeItem(icon_map=self.icon_state_map)
self.category_nodes = [] self.category_nodes = []
@ -843,7 +857,7 @@ class TagsModel(QAbstractItemModel): # {{{
category_node_map[key] = node category_node_map[key] = node
last_category_node = node last_category_node = node
self.category_nodes.append(node) self.category_nodes.append(node)
self.refresh(data=data) self._create_node_tree(data, state_map)
def break_cycles(self): def break_cycles(self):
self.root_item.break_cycles() self.root_item.break_cycles()
@ -1120,8 +1134,10 @@ class TagsModel(QAbstractItemModel): # {{{
def set_search_restriction(self, s): def set_search_restriction(self, s):
self.search_restriction = s self.search_restriction = s
def get_node_tree(self, sort): def _get_category_nodes(self, sort):
old_row_map = self.row_map[:] '''
Called by __init__. Do not directly call this method.
'''
self.row_map = [] self.row_map = []
self.categories = {} self.categories = {}
@ -1175,19 +1191,27 @@ class TagsModel(QAbstractItemModel): # {{{
if category in data: # The search category can come and go if category in data: # The search category can come and go
self.row_map.append(category) self.row_map.append(category)
self.categories[category] = tb_categories[category]['name'] self.categories[category] = tb_categories[category]['name']
if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map):
# A category has been added or removed. We must force a rebuild of
# the model
return None
return data return data
def refresh(self, data=None): def refresh(self, data=None):
'''
Here to trap usages of refresh in the old architecture. Can eventually
be removed.
'''
print 'TagsModel: refresh called!'
traceback.print_stack()
return False
def _create_node_tree(self, data, state_map):
'''
Called by __init__. Do not directly call this method.
'''
sort_by = config['sort_tags_by'] sort_by = config['sort_tags_by']
if data is None: if data is None:
data = self.get_node_tree(sort_by) # get category data print '_create_node_tree: no data!'
if data is None: traceback.print_stack()
return False return
collapse = gprefs['tags_browser_collapse_at'] collapse = gprefs['tags_browser_collapse_at']
collapse_model = self.collapse_model collapse_model = self.collapse_model
@ -1353,26 +1377,23 @@ class TagsModel(QAbstractItemModel): # {{{
# }}} # }}}
for category in self.category_nodes: for category in self.category_nodes:
if len(category.children) > 0: process_one_node(category, state_map.get(category.py_name, {}))
child_map = category.children
states = [c.tag.state for c in category.child_tags()]
names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
state_map = dict(izip(names, states))
# temporary sub-categories (the partitioning ones) must follow
# the permanent sub-categories. This will happen naturally if
# the temp ones are added by process_node
ctags = [c for c in child_map if
c.type == TagTreeItem.CATEGORY and not c.temporary]
start = len(ctags)
self.beginRemoveRows(self.createIndex(category.row(), 0, category),
start, len(child_map)-1)
category.children = ctags
self.endRemoveRows()
else:
state_map = {}
process_one_node(category, state_map) def get_state(self):
return True state_map = {}
expanded_categories = []
for row, category in enumerate(self.category_nodes):
if self.tags_view.isExpanded(self.index(row, 0, QModelIndex())):
expanded_categories.append(category.py_name)
states = [c.tag.state for c in category.child_tags()]
names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
state_map[category.py_name] = dict(izip(names, states))
return expanded_categories, state_map
def index_for_category(self, name):
for row, category in enumerate(self.category_nodes):
if category.py_name == name:
return self.index(row, 0, QModelIndex())
def columnCount(self, parent): def columnCount(self, parent):
return 1 return 1
@ -1472,7 +1493,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.tags_view.tag_item_renamed.emit() self.tags_view.tag_item_renamed.emit()
item.tag.name = val item.tag.name = val
self.rename_item_in_all_user_categories(name, key, val) self.rename_item_in_all_user_categories(name, key, val)
self.refresh() # Should work, because no categories can have disappeared self.refresh_required.emit()
self.show_item_at_path(path) self.show_item_at_path(path)
return True return True
@ -1785,19 +1806,22 @@ class TagsModel(QAbstractItemModel): # {{{
return v return v
return None return None
def show_item_at_path(self, path, box=False): def show_item_at_path(self, path, box=False,
position=QTreeView.PositionAtCenter):
''' '''
Scroll the browser and open categories to show the item referenced by Scroll the browser and open categories to show the item referenced by
path. If possible, the item is placed in the center. If box=True, a path. If possible, the item is placed in the center. If box=True, a
box is drawn around the item. box is drawn around the item.
''' '''
if path: if path:
self.show_item_at_index(self.index_for_path(path), box) self.show_item_at_index(self.index_for_path(path), box=box,
position=position)
def show_item_at_index(self, idx, box=False): def show_item_at_index(self, idx, box=False,
position=QTreeView.PositionAtCenter):
if idx.isValid(): if idx.isValid():
self.tags_view.setCurrentIndex(idx) self.tags_view.setCurrentIndex(idx)
self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter) self.tags_view.scrollTo(idx, position)
if box: if box:
tag_item = idx.internalPointer() tag_item = idx.internalPointer()
tag_item.boxed = True tag_item.boxed = True

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

@ -7,13 +7,11 @@ __docformat__ = 'restructuredtext en'
The database used to store ebook metadata The database used to store ebook metadata
''' '''
import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, \ import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, \
json, uuid, tempfile json, uuid, tempfile, hashlib
import threading, random 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 stream.seek(0)
with tempfile.SpooledTemporaryFile(max_size=30*(1024**2)) as stream: except NoSuchFormat:
with f: continue
shutil.copyfileobj(f, stream)
stream.seek(0)
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))
@ -661,32 +659,53 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
continue continue
def cover(self, index, index_is_id=False, as_file=False, as_image=False, 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. 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')
if as_image: with f:
img = QImage() if as_path:
img.loadFromData(f.read()) pt = PersistentTemporaryFile('_dbcover.jpg')
f.close() with pt:
return img shutil.copyfileobj(f, pt)
ans = f if as_file else f.read() return pt.name
if ans is not f: if as_file:
f.close() ret = tempfile.SpooledTemporaryFile(SPOOL_SIZE)
return ans 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. ### 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,7 +953,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.user_categories = user_cat_vals mi.user_categories = user_cat_vals
if get_cover: 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 return mi
def has_book(self, mi): def has_book(self, mi):
@ -1094,12 +1118,59 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return self.format_abspath(index, format, index_is_id) is not None return self.format_abspath(index, format, index_is_id) is not None
def format_last_modified(self, id_, fmt): def format_last_modified(self, id_, fmt):
m = self.format_metadata(id_, fmt)
if m:
return m['mtime']
def format_metadata(self, id_, fmt):
path = self.format_abspath(id_, fmt, index_is_id=True) path = self.format_abspath(id_, fmt, index_is_id=True)
ans = {}
if path is not None: if path is not None:
return utcfromtimestamp(os.stat(path).st_mtime) stat = os.stat(path)
ans['size'] = stat.st_size
ans['mtime'] = utcfromtimestamp(stat.st_mtime)
return ans
def format_hash(self, id_, fmt):
path = self.format_abspath(id_, fmt, index_is_id=True)
if path is None:
raise NoSuchFormat('Record %d has no fmt: %s'%(id_, fmt))
sha = hashlib.sha256()
with lopen(path, 'rb') as f:
while True:
raw = f.read(SPOOL_SIZE)
sha.update(raw)
if len(raw) < SPOOL_SIZE:
break
return sha.hexdigest()
def format_path(self, index, fmt, index_is_id=False):
'''
This method is intended to be used only in those rare situations, like
Drag'n Drop, when you absolutely need the path to the original file.
Otherwise, use format(..., as_path=True).
Note that a networked backend will always return None.
'''
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 format: %s'%(id_, fmt))
return path
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 +1190,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:
try: if as_path:
ret = f if as_file else f.read() if preserve_filename:
except IOError: bd = base_dir()
f.seek(0) d = os.path.join(bd, 'format_abspath')
out = cStringIO.StringIO() try:
shutil.copyfileobj(f, out) os.makedirs(d)
ret = out.getvalue() except:
if not as_file: pass
f.close() 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 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,
@ -1929,13 +2038,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def authors_with_sort_strings(self, id, index_is_id=False): def authors_with_sort_strings(self, id, index_is_id=False):
id = id if index_is_id else self.id(id) id = id if index_is_id else self.id(id)
aut_strings = self.conn.get(''' aut_strings = self.conn.get('''
SELECT authors.name, authors.sort SELECT authors.id, authors.name, authors.sort
FROM authors, books_authors_link as bl FROM authors, books_authors_link as bl
WHERE bl.book=? and authors.id=bl.author WHERE bl.book=? and authors.id=bl.author
ORDER BY bl.id''', (id,)) ORDER BY bl.id''', (id,))
result = [] result = []
for (author, sort,) in aut_strings: for (id_, author, sort,) in aut_strings:
result.append((author.replace('|', ','), sort)) result.append((id_, author.replace('|', ','), sort))
return result return result
# Given a book, return the author_sort string for authors of the book # Given a book, return the author_sort string for authors of the book
@ -1943,6 +2052,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
auts = self.authors_sort_strings(id, index_is_id) auts = self.authors_sort_strings(id, index_is_id)
return ' & '.join(auts).replace('|', ',') return ' & '.join(auts).replace('|', ',')
# Given an author, return a list of books with that author
def books_for_author(self, id_, index_is_id=False):
id_ = id_ if index_is_id else self.id(id_)
books = self.conn.get('''
SELECT bl.book
FROM books_authors_link as bl
WHERE bl.author=?''', (id_,))
return [b[0] for b in books]
# Given a list of authors, return the author_sort string for the authors, # Given a list of authors, return the author_sort string for the authors,
# preferring the author sort associated with the author over the computed # preferring the author sort associated with the author over the computed
# string # string
@ -1966,7 +2084,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
aum = self.authors_with_sort_strings(id_, index_is_id=True) aum = self.authors_with_sort_strings(id_, index_is_id=True)
self.data.set(id_, self.FIELD_MAP['au_map'], self.data.set(id_, self.FIELD_MAP['au_map'],
':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (au, aus) in aum]), ':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (_, au, aus) in aum]),
row_is_id=True) row_is_id=True)
def _set_authors(self, id, authors, allow_case_change=False): def _set_authors(self, id, authors, allow_case_change=False):

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
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) 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
# }}} # }}}

View File

@ -633,6 +633,7 @@ TXT input supports a number of options to differentiate how paragraphs are detec
:guilabel:`Formatting Style: None` :guilabel:`Formatting Style: None`
Applies no special formatting to the text, the document is converted to html with no other changes. Applies no special formatting to the text, the document is converted to html with no other changes.
.. _pdfconversion:
Convert PDF documents Convert PDF documents
~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -35,29 +35,11 @@ What are the best source formats to convert?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order of decreasing preference: LIT, MOBI, EPUB, FB2, HTML, PRC, RTF, PDB, TXT, PDF In order of decreasing preference: LIT, MOBI, EPUB, FB2, HTML, PRC, RTF, PDB, TXT, PDF
Why does the PDF conversion lose some images/tables? I converted a PDF file, but the result has various problems?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The PDF conversion tries to extract the text and images from the PDF file and convert them to and HTML based ebook. Some PDF files have images in a format that cannot be extracted (vector images). All tables
are also represented as vector diagrams, thus they cannot be extracted.
How do I convert a collection of HTML files in a specific order? PDF is a terrible format to convert from. For a list of the various issues you will encounter when converting PDF, see: :ref:`pdfconversion`.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to convert a collection of HTML files in a specific oder, you have to create a table of contents file. That is, another HTML file that contains links to all the other files in the desired order. Such a file looks like::
<html>
<body>
<h1>Table of Contents</h1>
<p style="text-indent:0pt">
<a href="file1.html">First File</a><br/>
<a href="file2.html">Second File</a><br/>
.
.
.
</p>
</body>
</html>
Then just add this HTML file to the GUI and use the convert button to create your ebook.
.. _char-encoding-faq: .. _char-encoding-faq:
@ -85,6 +67,26 @@ If you have a hand edited TOC in the input document, you can use the TOC detecti
Finally, I encourage you to ditch the content TOC and only have a metadata TOC in your ebooks. Metadata TOCs will give the people reading your ebooks a much superior navigation experience (except on the Kindle, where they are essentially the same as a content TOC). Finally, I encourage you to ditch the content TOC and only have a metadata TOC in your ebooks. Metadata TOCs will give the people reading your ebooks a much superior navigation experience (except on the Kindle, where they are essentially the same as a content TOC).
How do I convert a collection of HTML files in a specific order?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to convert a collection of HTML files in a specific oder, you have to create a table of contents file. That is, another HTML file that contains links to all the other files in the desired order. Such a file looks like::
<html>
<body>
<h1>Table of Contents</h1>
<p style="text-indent:0pt">
<a href="file1.html">First File</a><br/>
<a href="file2.html">Second File</a><br/>
.
.
.
</p>
</body>
</html>
Then just add this HTML file to the GUI and use the convert button to create your ebook.
How do I use some of the advanced features of the conversion tools? How do I use some of the advanced features of the conversion tools?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can get help on any individual feature of the converters by mousing over it in the GUI or running ``ebook-convert dummy.html .epub -h`` at a terminal. A good place to start is to look at the following demo files that demonstrate some of the advanced features: You can get help on any individual feature of the converters by mousing over it in the GUI or running ``ebook-convert dummy.html .epub -h`` at a terminal. A good place to start is to look at the following demo files that demonstrate some of the advanced features:

File diff suppressed because it is too large Load Diff

View File

@ -123,6 +123,14 @@ def isoformat(date_time, assume_utc=False, as_utc=True, sep='T'):
date_time = date_time.astimezone(_utc_tz if as_utc else _local_tz) date_time = date_time.astimezone(_utc_tz if as_utc else _local_tz)
return unicode(date_time.isoformat(sep)) return unicode(date_time.isoformat(sep))
def as_local_time(date_time, assume_utc=True):
if not hasattr(date_time, 'tzinfo'):
return date_time
if date_time.tzinfo is None:
date_time = date_time.replace(tzinfo=_utc_tz if assume_utc else
_local_tz)
return date_time.astimezone(_local_tz)
def now(): def now():
return datetime.now().replace(tzinfo=_local_tz) return datetime.now().replace(tzinfo=_local_tz)

View File

@ -8,61 +8,157 @@ __docformat__ = 'restructuredtext en'
''' '''
Measure memory usage of the current process. Measure memory usage of the current process.
The key function is memory() which returns the current memory usage in bytes. The key function is memory() which returns the current memory usage in MB.
You can pass a number to memory and it will be subtracted from the returned You can pass a number to memory and it will be subtracted from the returned
value. value.
''' '''
import gc, os import gc, os, re
from calibre.constants import iswindows, islinux from calibre.constants import iswindows, islinux
if islinux: if islinux:
## {{{ http://code.activestate.com/recipes/286222/ (r1) # Taken, with thanks, from:
# http://wingolog.org/archives/2007/11/27/reducing-the-footprint-of-python-applications
_proc_status = '/proc/%d/status' % os.getpid() def permute(args):
ret = []
if args:
first = args.pop(0)
for y in permute(args):
for x in first:
ret.append(x + y)
else:
ret.append('')
return ret
_scale = {'kB': 1024.0, 'mB': 1024.0*1024.0, def parsed_groups(match, *types):
'KB': 1024.0, 'MB': 1024.0*1024.0} groups = match.groups()
assert len(groups) == len(types)
return tuple([type(group) for group, type in zip(groups, types)])
def _VmB(VmKey): class VMA(dict):
'''Private. def __init__(self, *args):
''' (self.start, self.end, self.perms, self.offset,
global _proc_status, _scale self.major, self.minor, self.inode, self.filename) = args
# get pseudo file /proc/<pid>/status
try:
t = open(_proc_status)
v = t.read()
t.close()
except:
return 0.0 # non-Linux?
# get VmKey line e.g. 'VmRSS: 9999 kB\n ...'
i = v.index(VmKey)
v = v[i:].split(None, 3) # whitespace
if len(v) < 3:
return 0.0 # invalid format?
# convert Vm value to bytes
return float(v[1]) * _scale[v[2]]
def parse_smaps(pid):
with open('/proc/%s/smaps'%pid, 'r') as maps:
hex = lambda s: int(s, 16)
ret = []
header = re.compile(r'^([0-9a-f]+)-([0-9a-f]+) (....) ([0-9a-f]+) '
r'(..):(..) (\d+) *(.*)$')
detail = re.compile(r'^(.*): +(\d+) kB')
for line in maps:
m = header.match(line)
if m:
vma = VMA(*parsed_groups(m, hex, hex, str, hex, str, str, int, str))
ret.append(vma)
else:
m = detail.match(line)
if m:
k, v = parsed_groups(m, str, int)
assert k not in vma
vma[k] = v
else:
print 'unparseable line:', line
return ret
perms = permute(['r-', 'w-', 'x-', 'ps'])
def make_summary_dicts(vmas):
mapped = {}
anon = {}
for d in mapped, anon:
# per-perm
for k in perms:
d[k] = {}
d[k]['Size'] = 0
for y in 'Shared', 'Private':
d[k][y] = {}
for z in 'Clean', 'Dirty':
d[k][y][z] = 0
# totals
for y in 'Shared', 'Private':
d[y] = {}
for z in 'Clean', 'Dirty':
d[y][z] = 0
for vma in vmas:
if vma.major == '00' and vma.minor == '00':
d = anon
else:
d = mapped
for y in 'Shared', 'Private':
for z in 'Clean', 'Dirty':
d[vma.perms][y][z] += vma.get(y + '_' + z, 0)
d[y][z] += vma.get(y + '_' + z, 0)
d[vma.perms]['Size'] += vma.get('Size', 0)
return mapped, anon
def values(d, args):
if args:
ret = ()
first = args[0]
for k in first:
ret += values(d[k], args[1:])
return ret
else:
return (d,)
def print_summary(dicts_and_titles):
def desc(title, perms):
ret = {('Anonymous', 'rw-p'): 'Data (malloc, mmap)',
('Anonymous', 'rwxp'): 'Writable code (stack)',
('Mapped', 'r-xp'): 'Code',
('Mapped', 'rwxp'): 'Writable code (jump tables)',
('Mapped', 'r--p'): 'Read-only data',
('Mapped', 'rw-p'): 'Data'}.get((title, perms), None)
if ret:
return ' -- ' + ret
else:
return ''
for d, title in dicts_and_titles:
print title, 'memory:'
print ' Shared Private'
print ' Clean Dirty Clean Dirty'
for k in perms:
if d[k]['Size']:
print (' %s %7d %7d %7d %7d%s'
% ((k,)
+ values(d[k], (('Shared', 'Private'),
('Clean', 'Dirty')))
+ (desc(title, k),)))
print (' total %7d %7d %7d %7d'
% values(d, (('Shared', 'Private'),
('Clean', 'Dirty'))))
print ' ' + '-' * 40
print (' total %7d %7d %7d %7d'
% tuple(map(sum, zip(*[values(d, (('Shared', 'Private'),
('Clean', 'Dirty')))
for d, title in dicts_and_titles]))))
def print_stats(pid=None):
if pid is None:
pid = os.getpid()
vmas = parse_smaps(pid)
mapped, anon = make_summary_dicts(vmas)
print_summary(((mapped, "Mapped"), (anon, "Anonymous")))
def linux_memory(since=0.0): def linux_memory(since=0.0):
'''Return memory usage in bytes. vmas = parse_smaps(os.getpid())
''' mapped, anon = make_summary_dicts(vmas)
return _VmB('VmSize:') - since dicts_and_titles = ((mapped, "Mapped"), (anon, "Anonymous"))
totals = tuple(map(sum, zip(*[values(d, (('Shared', 'Private'),
('Clean', 'Dirty')))
for d, title in dicts_and_titles])))
return (totals[-1]/1024.) - since
def resident(since=0.0):
'''Return resident memory usage in bytes.
'''
return _VmB('VmRSS:') - since
def stacksize(since=0.0):
'''Return stack size in bytes.
'''
return _VmB('VmStk:') - since
## end of http://code.activestate.com/recipes/286222/ }}}
memory = linux_memory memory = linux_memory
elif iswindows: elif iswindows:
import win32process import win32process
import win32con import win32con
@ -95,7 +191,7 @@ elif iswindows:
def win_memory(since=0.0): def win_memory(since=0.0):
info = meminfo(get_handle(os.getpid())) info = meminfo(get_handle(os.getpid()))
return info['WorkingSetSize'] - since return (info['WorkingSetSize']/1024.**2) - since
memory = win_memory memory = win_memory
@ -112,6 +208,8 @@ def gc_histogram():
def diff_hists(h1, h2): def diff_hists(h1, h2):
"""Prints differences between two results of gc_histogram().""" """Prints differences between two results of gc_histogram()."""
for k in h1: for k in h1:
if k not in h2:
h2[k] = 0
if h1[k] != h2[k]: if h1[k] != h2[k]:
print "%s: %d -> %d (%s%d)" % ( print "%s: %d -> %d (%s%d)" % (
k, h1[k], h2[k], h2[k] > h1[k] and "+" or "", h2[k] - h1[k]) k, h1[k], h2[k], h2[k] > h1[k] and "+" or "", h2[k] - h1[k])