mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merge main branch
This commit is contained in:
commit
498c34bca0
@ -17,7 +17,7 @@ from calibre.ebooks.metadata import authors_to_string, MetaInformation
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
from calibre.ebooks.metadata.epub import set_metadata
|
||||
from calibre.library.server.utils import strftime
|
||||
from calibre.utils.config import config_dir
|
||||
from calibre.utils.config import config_dir, prefs
|
||||
from calibre.utils.date import isoformat, now, parse_date
|
||||
from calibre.utils.localization import get_lang
|
||||
from calibre.utils.logging import Log
|
||||
@ -68,6 +68,8 @@ class ITUNES(DriverBase):
|
||||
Delete:
|
||||
delete_books()
|
||||
remove_books_from_metadata()
|
||||
use_plugboard_ext()
|
||||
set_plugboard()
|
||||
sync_booklists()
|
||||
card_prefix()
|
||||
free_space()
|
||||
@ -76,6 +78,8 @@ class ITUNES(DriverBase):
|
||||
set_progress_reporter()
|
||||
upload_books()
|
||||
add_books_to_metadata()
|
||||
use_plugboard_ext()
|
||||
set_plugboard()
|
||||
set_progress_reporter()
|
||||
sync_booklists()
|
||||
card_prefix()
|
||||
@ -106,6 +110,9 @@ class ITUNES(DriverBase):
|
||||
PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a]
|
||||
BCD = [0x01]
|
||||
|
||||
# Plugboard ID
|
||||
DEVICE_PLUGBOARD_NAME = 'APPLE'
|
||||
|
||||
# iTunes enumerations
|
||||
Audiobooks = [
|
||||
'Audible file',
|
||||
@ -164,6 +171,7 @@ class ITUNES(DriverBase):
|
||||
# Properties
|
||||
cached_books = {}
|
||||
cache_dir = os.path.join(config_dir, 'caches', 'itunes')
|
||||
calibre_library_path = prefs['library_path']
|
||||
archive_path = os.path.join(cache_dir, "thumbs.zip")
|
||||
description_prefix = "added by calibre"
|
||||
ejected = False
|
||||
@ -173,6 +181,8 @@ class ITUNES(DriverBase):
|
||||
log = Log()
|
||||
manual_sync_mode = False
|
||||
path_template = 'iTunes/%s - %s.%s'
|
||||
plugboards = None
|
||||
plugboard_func = None
|
||||
problem_titles = []
|
||||
problem_msg = None
|
||||
report_progress = None
|
||||
@ -814,6 +824,15 @@ class ITUNES(DriverBase):
|
||||
'''
|
||||
self.report_progress = report_progress
|
||||
|
||||
def set_plugboards(self, plugboards, pb_func):
|
||||
# This method is called with the plugboard that matches the format
|
||||
# declared in use_plugboard_ext and a device name of ITUNES
|
||||
if DEBUG:
|
||||
self.log.info("ITUNES.set_plugboard()")
|
||||
#self.log.info(' using plugboard %s' % plugboards)
|
||||
self.plugboards = plugboards
|
||||
self.plugboard_func = pb_func
|
||||
|
||||
def sync_booklists(self, booklists, end_session=True):
|
||||
'''
|
||||
Update metadata on device.
|
||||
@ -977,7 +996,6 @@ class ITUNES(DriverBase):
|
||||
self._dump_cached_books(header="after upload_books()",indent=2)
|
||||
return (new_booklist, [], [])
|
||||
|
||||
|
||||
# Private methods
|
||||
def _add_device_book(self,fpath, metadata):
|
||||
'''
|
||||
@ -1256,7 +1274,10 @@ class ITUNES(DriverBase):
|
||||
self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0]))
|
||||
self.log.error(" error converting '%s' to thumb for '%s'" % (metadata.cover,metadata.title))
|
||||
finally:
|
||||
zfw.close()
|
||||
try:
|
||||
zfw.close()
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
if DEBUG:
|
||||
self.log.info(" no cover defined in metadata for '%s'" % metadata.title)
|
||||
@ -1273,10 +1294,10 @@ class ITUNES(DriverBase):
|
||||
this_book.db_id = None
|
||||
this_book.device_collections = []
|
||||
this_book.format = format
|
||||
this_book.library_id = lb_added
|
||||
this_book.library_id = lb_added # ??? GR
|
||||
this_book.path = path
|
||||
this_book.thumbnail = thumb
|
||||
this_book.iTunes_id = lb_added
|
||||
this_book.iTunes_id = lb_added # ??? GR
|
||||
this_book.uuid = metadata.uuid
|
||||
|
||||
if isosx:
|
||||
@ -1322,8 +1343,8 @@ class ITUNES(DriverBase):
|
||||
plist = None
|
||||
if plist:
|
||||
if DEBUG:
|
||||
self.log.info(" _delete_iTunesMetadata_plist():")
|
||||
self.log.info(" deleting '%s'\n from '%s'" % (pl_name,fpath))
|
||||
self.log.info(" _delete_iTunesMetadata_plist():")
|
||||
self.log.info(" deleting '%s'\n from '%s'" % (pl_name,fpath))
|
||||
zf.delete(pl_name)
|
||||
zf.close()
|
||||
|
||||
@ -2213,6 +2234,7 @@ class ITUNES(DriverBase):
|
||||
(self.iTunes.name(), self.iTunes.version(), self.initial_status,
|
||||
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)
|
||||
|
||||
if iswindows:
|
||||
'''
|
||||
@ -2266,6 +2288,7 @@ class ITUNES(DriverBase):
|
||||
(self.iTunes.Windows[0].name, self.iTunes.Version, self.initial_status,
|
||||
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)
|
||||
|
||||
def _purge_orphans(self,library_books, cached_books):
|
||||
'''
|
||||
@ -2368,7 +2391,8 @@ class ITUNES(DriverBase):
|
||||
'''
|
||||
iTunes does not delete books from storage when removing from database
|
||||
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
|
||||
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:
|
||||
self.log.info(" ITUNES._remove_from_iTunes():")
|
||||
@ -2376,7 +2400,8 @@ class ITUNES(DriverBase):
|
||||
if isosx:
|
||||
try:
|
||||
storage_path = os.path.split(cached_book['lib_book'].location().path)
|
||||
if cached_book['lib_book'].location().path.startswith(self.iTunes_media):
|
||||
if cached_book['lib_book'].location().path.startswith(self.iTunes_media) and \
|
||||
not storage_path[0].startswith(self.calibre_library_path):
|
||||
title_storage_path = storage_path[0]
|
||||
if DEBUG:
|
||||
self.log.info(" removing title_storage_path: %s" % title_storage_path)
|
||||
@ -2427,7 +2452,8 @@ class ITUNES(DriverBase):
|
||||
path = book.Location
|
||||
|
||||
if book:
|
||||
if self.iTunes_media and path.startswith(self.iTunes_media):
|
||||
if self.iTunes_media and path.startswith(self.iTunes_media) and \
|
||||
not path.startswith(self.calibre_library_path):
|
||||
storage_path = os.path.split(path)
|
||||
if DEBUG:
|
||||
self.log.info(" removing '%s' at %s" %
|
||||
@ -2454,11 +2480,17 @@ class ITUNES(DriverBase):
|
||||
if DEBUG:
|
||||
self.log.info(" unable to remove '%s' from iTunes" % cached_book['title'])
|
||||
|
||||
def title_sorter(self, title):
|
||||
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip()
|
||||
|
||||
def _update_epub_metadata(self, fpath, metadata):
|
||||
'''
|
||||
'''
|
||||
self.log.info(" ITUNES._update_epub_metadata()")
|
||||
|
||||
# Fetch plugboard updates
|
||||
metadata_x = self._xform_metadata_via_plugboard(metadata, 'epub')
|
||||
|
||||
# Refresh epub metadata
|
||||
with open(fpath,'r+b') as zfo:
|
||||
# Touch the OPF timestamp
|
||||
@ -2490,9 +2522,14 @@ class ITUNES(DriverBase):
|
||||
self.log.info(" add timestamp: %s" % metadata.timestamp)
|
||||
|
||||
# Force the language declaration for iBooks 1.1
|
||||
metadata.language = get_lang().replace('_', '-')
|
||||
#metadata.language = get_lang().replace('_', '-')
|
||||
|
||||
# Updates from metadata plugboard (ignoring publisher)
|
||||
metadata.language = metadata_x.language
|
||||
|
||||
if DEBUG:
|
||||
self.log.info(" rewriting language: <dc:language>%s</dc:language>" % metadata.language)
|
||||
if metadata.language != metadata_x.language:
|
||||
self.log.info(" rewriting language: <dc:language>%s</dc:language>" % metadata.language)
|
||||
|
||||
zf_opf.close()
|
||||
|
||||
@ -2570,75 +2607,97 @@ class ITUNES(DriverBase):
|
||||
if DEBUG:
|
||||
self.log.info(" ITUNES._update_iTunes_metadata()")
|
||||
|
||||
strip_tags = re.compile(r'<[^<]*?/?>')
|
||||
STRIP_TAGS = re.compile(r'<[^<]*?/?>')
|
||||
|
||||
# Update metadata from plugboard
|
||||
# If self.plugboard is None (no transforms), original metadata is returned intact
|
||||
metadata_x = self._xform_metadata_via_plugboard(metadata, this_book.format)
|
||||
|
||||
if isosx:
|
||||
if lb_added:
|
||||
lb_added.album.set(metadata.title)
|
||||
lb_added.artist.set(authors_to_string(metadata.authors))
|
||||
lb_added.composer.set(metadata.uuid)
|
||||
lb_added.name.set(metadata_x.title)
|
||||
lb_added.album.set(metadata_x.title)
|
||||
lb_added.artist.set(authors_to_string(metadata_x.authors))
|
||||
lb_added.composer.set(metadata_x.uuid)
|
||||
lb_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
||||
lb_added.enabled.set(True)
|
||||
lb_added.sort_artist.set(metadata.author_sort.title())
|
||||
lb_added.sort_name.set(this_book.title_sorter)
|
||||
if this_book.format == 'pdf':
|
||||
lb_added.name.set(metadata.title)
|
||||
lb_added.sort_artist.set(metadata_x.author_sort.title())
|
||||
lb_added.sort_name.set(metadata.title_sort)
|
||||
|
||||
|
||||
if db_added:
|
||||
db_added.album.set(metadata.title)
|
||||
db_added.artist.set(authors_to_string(metadata.authors))
|
||||
db_added.composer.set(metadata.uuid)
|
||||
db_added.name.set(metadata_x.title)
|
||||
db_added.album.set(metadata_x.title)
|
||||
db_added.artist.set(authors_to_string(metadata_x.authors))
|
||||
db_added.composer.set(metadata_x.uuid)
|
||||
db_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
||||
db_added.enabled.set(True)
|
||||
db_added.sort_artist.set(metadata.author_sort.title())
|
||||
db_added.sort_name.set(this_book.title_sorter)
|
||||
if this_book.format == 'pdf':
|
||||
db_added.name.set(metadata.title)
|
||||
db_added.sort_artist.set(metadata_x.author_sort.title())
|
||||
db_added.sort_name.set(metadata.title_sort)
|
||||
|
||||
if metadata.comments:
|
||||
if metadata_x.comments:
|
||||
if lb_added:
|
||||
lb_added.comment.set(strip_tags.sub('',metadata.comments))
|
||||
lb_added.comment.set(STRIP_TAGS.sub('',metadata_x.comments))
|
||||
if db_added:
|
||||
db_added.comment.set(strip_tags.sub('',metadata.comments))
|
||||
db_added.comment.set(STRIP_TAGS.sub('',metadata_x.comments))
|
||||
|
||||
if metadata.rating:
|
||||
if metadata_x.rating:
|
||||
if lb_added:
|
||||
lb_added.rating.set(metadata.rating*10)
|
||||
lb_added.rating.set(metadata_x.rating*10)
|
||||
# iBooks currently doesn't allow setting rating ... ?
|
||||
try:
|
||||
if db_added:
|
||||
db_added.rating.set(metadata.rating*10)
|
||||
db_added.rating.set(metadata_x.rating*10)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Set genre from series if available, else first alpha tag
|
||||
# Otherwise iTunes grabs the first dc:subject from the opf metadata
|
||||
if metadata.series and self.settings().read_metadata:
|
||||
# self.settings().read_metadata is used as a surrogate for "Use Series name as Genre"
|
||||
if metadata_x.series and self.settings().read_metadata:
|
||||
if DEBUG:
|
||||
self.log.info(" ITUNES._update_iTunes_metadata()")
|
||||
self.log.info(" using Series name as Genre")
|
||||
|
||||
# Format the index as a sort key
|
||||
index = metadata.series_index
|
||||
index = metadata_x.series_index
|
||||
integer = int(index)
|
||||
fraction = index-integer
|
||||
series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
|
||||
if lb_added:
|
||||
lb_added.sort_name.set("%s %s" % (metadata.series, series_index))
|
||||
lb_added.genre.set(metadata.series)
|
||||
lb_added.episode_ID.set(metadata.series)
|
||||
lb_added.episode_number.set(metadata.series_index)
|
||||
lb_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index))
|
||||
lb_added.episode_ID.set(metadata_x.series)
|
||||
lb_added.episode_number.set(metadata_x.series_index)
|
||||
|
||||
# If no plugboard transform applied to tags, change the Genre/Category to Series
|
||||
if metadata.tags == metadata_x.tags:
|
||||
lb_added.genre.set(self.title_sorter(metadata_x.series))
|
||||
else:
|
||||
for tag in metadata_x.tags:
|
||||
if self._is_alpha(tag[0]):
|
||||
lb_added.genre.set(tag)
|
||||
break
|
||||
|
||||
if db_added:
|
||||
db_added.sort_name.set("%s %s" % (metadata.series, series_index))
|
||||
db_added.genre.set(metadata.series)
|
||||
db_added.episode_ID.set(metadata.series)
|
||||
db_added.episode_number.set(metadata.series_index)
|
||||
db_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index))
|
||||
db_added.episode_ID.set(metadata_x.series)
|
||||
db_added.episode_number.set(metadata_x.series_index)
|
||||
|
||||
elif metadata.tags:
|
||||
# If no plugboard transform applied to tags, change the Genre/Category to Series
|
||||
if metadata.tags == metadata_x.tags:
|
||||
db_added.genre.set(self.title_sorter(metadata_x.series))
|
||||
else:
|
||||
for tag in metadata_x.tags:
|
||||
if self._is_alpha(tag[0]):
|
||||
db_added.genre.set(tag)
|
||||
break
|
||||
|
||||
|
||||
elif metadata_x.tags is not None:
|
||||
if DEBUG:
|
||||
self.log.info(" %susing Tag as Genre" %
|
||||
"no Series name available, " if self.settings().read_metadata else '')
|
||||
for tag in metadata.tags:
|
||||
for tag in metadata_x.tags:
|
||||
if self._is_alpha(tag[0]):
|
||||
if lb_added:
|
||||
lb_added.genre.set(tag)
|
||||
@ -2648,40 +2707,38 @@ class ITUNES(DriverBase):
|
||||
|
||||
elif iswindows:
|
||||
if lb_added:
|
||||
lb_added.Album = metadata.title
|
||||
lb_added.Artist = authors_to_string(metadata.authors)
|
||||
lb_added.Composer = metadata.uuid
|
||||
lb_added.Name = metadata_x.title
|
||||
lb_added.Album = metadata_x.title
|
||||
lb_added.Artist = authors_to_string(metadata_x.authors)
|
||||
lb_added.Composer = metadata_x.uuid
|
||||
lb_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
||||
lb_added.Enabled = True
|
||||
lb_added.SortArtist = (metadata.author_sort.title())
|
||||
lb_added.SortName = (this_book.title_sorter)
|
||||
if this_book.format == 'pdf':
|
||||
lb_added.Name = metadata.title
|
||||
lb_added.SortArtist = metadata_x.author_sort.title()
|
||||
lb_added.SortName = metadata.title_sort
|
||||
|
||||
if db_added:
|
||||
db_added.Album = metadata.title
|
||||
db_added.Artist = authors_to_string(metadata.authors)
|
||||
db_added.Composer = metadata.uuid
|
||||
db_added.Name = metadata_x.title
|
||||
db_added.Album = metadata_x.title
|
||||
db_added.Artist = authors_to_string(metadata_x.authors)
|
||||
db_added.Composer = metadata_x.uuid
|
||||
db_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
||||
db_added.Enabled = True
|
||||
db_added.SortArtist = (metadata.author_sort.title())
|
||||
db_added.SortName = (this_book.title_sorter)
|
||||
if this_book.format == 'pdf':
|
||||
db_added.Name = metadata.title
|
||||
db_added.SortArtist = metadata_x.author_sort.title()
|
||||
db_added.SortName = metadata.title_sort
|
||||
|
||||
if metadata.comments:
|
||||
if metadata_x.comments:
|
||||
if lb_added:
|
||||
lb_added.Comment = (strip_tags.sub('',metadata.comments))
|
||||
lb_added.Comment = (STRIP_TAGS.sub('',metadata_x.comments))
|
||||
if db_added:
|
||||
db_added.Comment = (strip_tags.sub('',metadata.comments))
|
||||
db_added.Comment = (STRIP_TAGS.sub('',metadata_x.comments))
|
||||
|
||||
if metadata.rating:
|
||||
if metadata_x.rating:
|
||||
if lb_added:
|
||||
lb_added.AlbumRating = (metadata.rating*10)
|
||||
lb_added.AlbumRating = (metadata_x.rating*10)
|
||||
# iBooks currently doesn't allow setting rating ... ?
|
||||
try:
|
||||
if db_added:
|
||||
db_added.AlbumRating = (metadata.rating*10)
|
||||
db_added.AlbumRating = (metadata_x.rating*10)
|
||||
except:
|
||||
if DEBUG:
|
||||
self.log.warning(" iTunes automation interface reported an error"
|
||||
@ -2691,36 +2748,54 @@ class ITUNES(DriverBase):
|
||||
# Otherwise iBooks uses first <dc:subject> from opf
|
||||
# iTunes balks on setting EpisodeNumber, but it sticks (9.1.1.12)
|
||||
|
||||
if metadata.series and self.settings().read_metadata:
|
||||
if metadata_x.series and self.settings().read_metadata:
|
||||
if DEBUG:
|
||||
self.log.info(" using Series name as Genre")
|
||||
# Format the index as a sort key
|
||||
index = metadata.series_index
|
||||
index = metadata_x.series_index
|
||||
integer = int(index)
|
||||
fraction = index-integer
|
||||
series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
|
||||
if lb_added:
|
||||
lb_added.SortName = "%s %s" % (metadata.series, series_index)
|
||||
lb_added.Genre = metadata.series
|
||||
lb_added.EpisodeID = metadata.series
|
||||
lb_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
|
||||
lb_added.EpisodeID = metadata_x.series
|
||||
try:
|
||||
lb_added.EpisodeNumber = metadata.series_index
|
||||
lb_added.EpisodeNumber = metadata_x.series_index
|
||||
except:
|
||||
pass
|
||||
|
||||
# If no plugboard transform applied to tags, change the Genre/Category to Series
|
||||
if metadata.tags == metadata_x.tags:
|
||||
lb_added.Genre = self.title_sorter(metadata_x.series)
|
||||
else:
|
||||
for tag in metadata_x.tags:
|
||||
if self._is_alpha(tag[0]):
|
||||
lb_added.Genre = tag
|
||||
break
|
||||
|
||||
if db_added:
|
||||
db_added.SortName = "%s %s" % (metadata.series, series_index)
|
||||
db_added.Genre = metadata.series
|
||||
db_added.EpisodeID = metadata.series
|
||||
db_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
|
||||
db_added.EpisodeID = metadata_x.series
|
||||
try:
|
||||
db_added.EpisodeNumber = metadata.series_index
|
||||
db_added.EpisodeNumber = metadata_x.series_index
|
||||
except:
|
||||
if DEBUG:
|
||||
self.log.warning(" iTunes automation interface reported an error"
|
||||
" setting EpisodeNumber on iDevice")
|
||||
elif metadata.tags:
|
||||
|
||||
# If no plugboard transform applied to tags, change the Genre/Category to Series
|
||||
if metadata.tags == metadata_x.tags:
|
||||
db_added.Genre = self.title_sorter(metadata_x.series)
|
||||
else:
|
||||
for tag in metadata_x.tags:
|
||||
if self._is_alpha(tag[0]):
|
||||
db_added.Genre = tag
|
||||
break
|
||||
|
||||
elif metadata_x.tags is not None:
|
||||
if DEBUG:
|
||||
self.log.info(" using Tag as Genre")
|
||||
for tag in metadata.tags:
|
||||
for tag in metadata_x.tags:
|
||||
if self._is_alpha(tag[0]):
|
||||
if lb_added:
|
||||
lb_added.Genre = tag
|
||||
@ -2728,6 +2803,36 @@ class ITUNES(DriverBase):
|
||||
db_added.Genre = tag
|
||||
break
|
||||
|
||||
def _xform_metadata_via_plugboard(self, book, format):
|
||||
''' Transform book metadata from plugboard templates '''
|
||||
if DEBUG:
|
||||
self.log.info(" ITUNES._update_metadata_from_plugboard()")
|
||||
|
||||
if self.plugboard_func:
|
||||
pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards)
|
||||
newmi = book.deepcopy_metadata()
|
||||
newmi.template_to_attribute(book, pb)
|
||||
if DEBUG:
|
||||
self.log.info(" transforming %s using %s:" % (format, pb))
|
||||
self.log.info(" title: %s %s" % (book.title, ">>> %s" %
|
||||
newmi.title if book.title != newmi.title else ''))
|
||||
self.log.info(" title_sort: %s %s" % (book.title_sort, ">>> %s" %
|
||||
newmi.title_sort if book.title_sort != newmi.title_sort else ''))
|
||||
self.log.info(" authors: %s %s" % (book.authors, ">>> %s" %
|
||||
newmi.authors if book.authors != newmi.authors else ''))
|
||||
self.log.info(" author_sort: %s %s" % (book.author_sort, ">>> %s" %
|
||||
newmi.author_sort if book.author_sort != newmi.author_sort else ''))
|
||||
self.log.info(" language: %s %s" % (book.language, ">>> %s" %
|
||||
newmi.language if book.language != newmi.language else ''))
|
||||
self.log.info(" publisher: %s %s" % (book.publisher, ">>> %s" %
|
||||
newmi.publisher if book.publisher != newmi.publisher else ''))
|
||||
self.log.info(" tags: %s %s" % (book.tags, ">>> %s" %
|
||||
newmi.tags if book.tags != newmi.tags else ''))
|
||||
else:
|
||||
newmi = book
|
||||
return newmi
|
||||
|
||||
|
||||
class ITUNES_ASYNC(ITUNES):
|
||||
'''
|
||||
This subclass allows the user to interact directly with iTunes via a menu option
|
||||
@ -2738,6 +2843,9 @@ class ITUNES_ASYNC(ITUNES):
|
||||
icon = I('devices/itunes.png')
|
||||
description = _('Communicate with iTunes.')
|
||||
|
||||
# Plugboard ID
|
||||
DEVICE_PLUGBOARD_NAME = 'APPLE'
|
||||
|
||||
connected = False
|
||||
|
||||
def __init__(self,path):
|
||||
@ -3012,15 +3120,9 @@ class BookList(list):
|
||||
class Book(Metadata):
|
||||
'''
|
||||
A simple class describing a book in the iTunes Books Library.
|
||||
- See ebooks.metadata.__init__ for all fields
|
||||
See ebooks.metadata.book.base
|
||||
'''
|
||||
def __init__(self,title,author):
|
||||
|
||||
Metadata.__init__(self, title, authors=[author])
|
||||
|
||||
@dynamic_property
|
||||
def title_sorter(self):
|
||||
doc = '''String to sort the title. If absent, title is returned'''
|
||||
def fget(self):
|
||||
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip()
|
||||
return property(doc=doc, fget=fget)
|
||||
|
@ -411,6 +411,22 @@ class DevicePlugin(Plugin):
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_plugboards(self, plugboards, pb_func):
|
||||
'''
|
||||
provide the driver the current set of plugboards and a function to
|
||||
select a specific plugboard. This method is called immediately before
|
||||
add_books and sync_booklists.
|
||||
|
||||
pb_func is a callable with the following signature:
|
||||
def pb_func(device_name, format, plugboards)
|
||||
You give it the current device name (either the class name or
|
||||
DEVICE_PLUGBOARD_NAME), the format you are interested in (a 'real'
|
||||
format or 'device_db'), and the plugboards (you were given those by
|
||||
set_plugboards, the same place you got this method).
|
||||
|
||||
Return value: None or a single plugboard instance.
|
||||
'''
|
||||
pass
|
||||
|
||||
class BookList(list):
|
||||
'''
|
||||
|
@ -83,7 +83,7 @@ class ISBNDBMetadata(Metadata):
|
||||
|
||||
summ = tostring(book.find('summary'))
|
||||
if summ:
|
||||
self.comments = 'SUMMARY:\n'+summ.string
|
||||
self.comments = 'SUMMARY:\n'+summ
|
||||
|
||||
|
||||
def build_isbn(base_url, opts):
|
||||
|
@ -21,6 +21,7 @@ class Worker(Thread):
|
||||
def __init__(self, ids, db, loc, progress, done):
|
||||
Thread.__init__(self)
|
||||
self.ids = ids
|
||||
self.processed = set([])
|
||||
self.db = db
|
||||
self.loc = loc
|
||||
self.error = None
|
||||
@ -71,6 +72,7 @@ class Worker(Thread):
|
||||
co = self.db.conversion_options(x, 'PIPE')
|
||||
if co is not None:
|
||||
newdb.set_conversion_options(x, 'PIPE', co)
|
||||
self.processed.add(x)
|
||||
|
||||
|
||||
class CopyToLibraryAction(InterfaceAction):
|
||||
@ -107,9 +109,13 @@ class CopyToLibraryAction(InterfaceAction):
|
||||
for name, loc in locations:
|
||||
self.menu.addAction(name, partial(self.copy_to_library,
|
||||
loc))
|
||||
self.menu.addAction(name + ' ' + _('(delete after copy)'),
|
||||
partial(self.copy_to_library, loc, delete_after=True))
|
||||
self.menu.addSeparator()
|
||||
|
||||
self.qaction.setVisible(bool(locations))
|
||||
|
||||
def copy_to_library(self, loc):
|
||||
def copy_to_library(self, loc, delete_after=False):
|
||||
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||
if not rows or len(rows) == 0:
|
||||
return error_dialog(self.gui, _('Cannot copy'),
|
||||
@ -140,7 +146,16 @@ class CopyToLibraryAction(InterfaceAction):
|
||||
else:
|
||||
self.gui.status_bar.show_message(_('Copied %d books to %s') %
|
||||
(len(ids), loc), 2000)
|
||||
|
||||
if delete_after and self.worker.processed:
|
||||
v = self.gui.library_view
|
||||
ci = v.currentIndex()
|
||||
row = None
|
||||
if ci.isValid():
|
||||
row = ci.row()
|
||||
|
||||
v.model().delete_books_by_id(self.worker.processed)
|
||||
self.gui.iactions['Remove Books'].library_ids_deleted(
|
||||
self.worker.processed, row)
|
||||
|
||||
|
||||
|
||||
|
@ -149,6 +149,18 @@ class DeleteAction(InterfaceAction):
|
||||
self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
|
||||
self.gui.library_view.currentIndex())
|
||||
|
||||
|
||||
def library_ids_deleted(self, ids_deleted, current_row=None):
|
||||
view = self.gui.library_view
|
||||
for v in (self.gui.memory_view, self.gui.card_a_view, self.gui.card_b_view):
|
||||
if v is None:
|
||||
continue
|
||||
v.model().clear_ondevice(ids_deleted)
|
||||
if current_row is not None:
|
||||
ci = view.model().index(current_row, 0)
|
||||
if ci.isValid():
|
||||
view.set_current_row(current_row)
|
||||
|
||||
def delete_books(self, *args):
|
||||
'''
|
||||
Delete selected books from device or library.
|
||||
@ -168,14 +180,7 @@ class DeleteAction(InterfaceAction):
|
||||
if ci.isValid():
|
||||
row = ci.row()
|
||||
ids_deleted = view.model().delete_books(rows)
|
||||
for v in (self.gui.memory_view, self.gui.card_a_view, self.gui.card_b_view):
|
||||
if v is None:
|
||||
continue
|
||||
v.model().clear_ondevice(ids_deleted)
|
||||
if row is not None:
|
||||
ci = view.model().index(row, 0)
|
||||
if ci.isValid():
|
||||
view.set_current_row(row)
|
||||
self.library_ids_deleted(ids_deleted, row)
|
||||
else:
|
||||
if not confirm('<p>'+_('The selected books will be '
|
||||
'<b>permanently deleted</b> '
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>707</width>
|
||||
<height>340</height>
|
||||
<width>931</width>
|
||||
<height>389</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -23,7 +23,7 @@ Use this dialog to define a 'plugboard' for a format (or all formats) and a devi
|
||||
|
||||
Often templates will contain simple references to composite columns, but this is not necessary. You can use any template in a source box that you can use elsewhere in calibre.
|
||||
|
||||
One possible use for a plugboard is to alter the title to contain series informaton. Another would be to change the author sort, something that mobi users might do to force it to use the ';' that the kindle requires. A third would be to specify the language.</string>
|
||||
One possible use for a plugboard is to alter the title to contain series information. Another would be to change the author sort, something that mobi users might do to force it to use the ';' that the kindle requires. A third would be to specify the language.</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
@ -41,8 +41,7 @@ One possible use for a plugboard is to alter the title to contain series informa
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QLabel" name="device_label">
|
||||
</widget>
|
||||
<widget class="QLabel" name="device_label"/>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="Line" name="line">
|
||||
|
@ -166,6 +166,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
def __init__(self, pathtoebook=None, debug_javascript=False):
|
||||
MainWindow.__init__(self, None)
|
||||
self.setupUi(self)
|
||||
self.show_toc_on_open = False
|
||||
self.current_book_has_toc = False
|
||||
self.base_window_title = unicode(self.windowTitle())
|
||||
self.iterator = None
|
||||
self.current_page = None
|
||||
@ -214,11 +216,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
self.action_metadata.setCheckable(True)
|
||||
self.action_metadata.setShortcut(Qt.CTRL+Qt.Key_I)
|
||||
self.action_table_of_contents.setCheckable(True)
|
||||
self.toc.setMinimumWidth(80)
|
||||
self.action_reference_mode.setCheckable(True)
|
||||
self.connect(self.action_reference_mode, SIGNAL('triggered(bool)'),
|
||||
lambda x: self.view.reference_mode(x))
|
||||
self.connect(self.action_metadata, SIGNAL('triggered(bool)'), lambda x:self.metadata.setVisible(x))
|
||||
self.connect(self.action_table_of_contents, SIGNAL('triggered(bool)'), lambda x:self.toc.setVisible(x))
|
||||
self.connect(self.action_table_of_contents, SIGNAL('toggled(bool)'), lambda x:self.toc.setVisible(x))
|
||||
self.connect(self.action_copy, SIGNAL('triggered(bool)'), self.copy)
|
||||
self.connect(self.action_font_size_larger, SIGNAL('triggered(bool)'),
|
||||
self.font_size_larger)
|
||||
@ -259,7 +262,6 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
f = functools.partial(self.load_ebook, pathtoebook)
|
||||
QTimer.singleShot(50, f)
|
||||
self.view.setMinimumSize(100, 100)
|
||||
self.splitter.setSizes([1, 300])
|
||||
self.toc.setCursor(Qt.PointingHandCursor)
|
||||
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
@ -285,6 +287,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
def save_state(self):
|
||||
state = str(self.saveState(self.STATE_VERSION))
|
||||
dynamic['viewer_toolbar_state'] = state
|
||||
dynamic.set('viewer_window_geometry', self.saveGeometry())
|
||||
if self.current_book_has_toc:
|
||||
dynamic.set('viewer_toc_isvisible', bool(self.toc.isVisible()))
|
||||
if self.toc.isVisible():
|
||||
dynamic.set('viewer_splitter_state',
|
||||
bytearray(self.splitter.saveState()))
|
||||
|
||||
def restore_state(self):
|
||||
state = dynamic.get('viewer_toolbar_state', None)
|
||||
@ -609,10 +617,15 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
title = self.iterator.opf.title
|
||||
if not title:
|
||||
title = os.path.splitext(os.path.basename(pathtoebook))[0]
|
||||
self.action_table_of_contents.setDisabled(not self.iterator.toc)
|
||||
if self.iterator.toc:
|
||||
self.toc_model = TOC(self.iterator.toc)
|
||||
self.toc.setModel(self.toc_model)
|
||||
if self.show_toc_on_open:
|
||||
self.action_table_of_contents.setChecked(True)
|
||||
else:
|
||||
self.action_table_of_contents.setChecked(False)
|
||||
self.action_table_of_contents.setDisabled(not self.iterator.toc)
|
||||
self.current_book_has_toc = bool(self.iterator.toc)
|
||||
self.current_title = title
|
||||
self.setWindowTitle(self.base_window_title+' - '+title)
|
||||
self.pos.setMaximum(sum(self.iterator.pages))
|
||||
@ -656,22 +669,21 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.write_settings()
|
||||
if self.iterator is not None:
|
||||
self.save_current_position()
|
||||
self.iterator.__exit__(*args)
|
||||
|
||||
def write_settings(self):
|
||||
dynamic.set('viewer_window_geometry', self.saveGeometry())
|
||||
|
||||
def read_settings(self):
|
||||
c = config().parse()
|
||||
wg = dynamic['viewer_window_geometry']
|
||||
if wg is not None and c.remember_window_size:
|
||||
self.restoreGeometry(wg)
|
||||
|
||||
|
||||
|
||||
self.splitter.setSizes([1, 300])
|
||||
if c.remember_window_size:
|
||||
wg = dynamic.get('viewer_window_geometry', None)
|
||||
if wg is not None:
|
||||
self.restoreGeometry(wg)
|
||||
ss = dynamic.get('viewer_splitter_state', None)
|
||||
if ss is not None:
|
||||
self.splitter.restoreState(ss)
|
||||
self.show_toc_on_open = dynamic.get('viewer_toc_isvisible', False)
|
||||
|
||||
def config(defaults=None):
|
||||
desc = _('Options to control the ebook viewer')
|
||||
|
Loading…
x
Reference in New Issue
Block a user