Merge from custcol trunk

This commit is contained in:
Charles Haley 2010-05-17 09:18:19 +01:00
commit 1eabbcfb9a
16 changed files with 689 additions and 136 deletions

View File

@ -0,0 +1,40 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
boljevac.blogspot.com
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class AgroGerila(BasicNewsRecipe):
title = 'Agro Gerila'
__author__ = 'Darko Miletic'
description = 'Politicki nekorektan blog.'
oldest_article = 45
max_articles_per_feed = 100
language = 'sr'
encoding = 'utf-8'
no_stylesheets = True
use_embedded_content = True
publication_type = 'blog'
extra_css = ' @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: "Trebuchet MS",Trebuchet,Verdana,sans1,sans-serif} .article_description{font-family: sans1, sans-serif} img{margin-bottom: 0.8em; border: 1px solid #333333; padding: 4px } '
conversion_options = {
'comment' : description
, 'tags' : 'film, blog, srbija'
, 'publisher': 'Dry-Na-Nord'
, 'language' : language
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
feeds = [(u'Posts', u'http://boljevac.blogspot.com/feeds/posts/default')]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)

View File

@ -15,7 +15,7 @@ class LiberoNews(BasicNewsRecipe):
description = 'Italian daily newspaper' description = 'Italian daily newspaper'
cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif' cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif'
title = u'Libero ' title = u'Libero'
publisher = 'EDITORIALE LIBERO s.r.l 2006' publisher = 'EDITORIALE LIBERO s.r.l 2006'
category = 'News, politics, culture, economy, general interest' category = 'News, politics, culture, economy, general interest'
@ -48,7 +48,7 @@ class LiberoNews(BasicNewsRecipe):
(u'Tecnologia', u'http://www.libero-news.it/rss.jsp?sezione=20'), (u'Tecnologia', u'http://www.libero-news.it/rss.jsp?sezione=20'),
(u'LifeStyle', u'http://www.libero-news.it/rss.jsp?sezione=22'), (u'LifeStyle', u'http://www.libero-news.it/rss.jsp?sezione=22'),
(u'Sport', u'http://www.libero-news.it/rss.jsp?sezione=23'), (u'Sport', u'http://www.libero-news.it/rss.jsp?sezione=23'),
(u'Costume e Società', u' http://www.libero-news.it/rss.jsp?sezione=24'), (u'Costume e Societ\xc3\xa0', u' http://www.libero-news.it/rss.jsp?sezione=24'),
(u'Milano', u'http://www.libero-news.it/rss.jsp?sezione=26'), (u'Milano', u'http://www.libero-news.it/rss.jsp?sezione=26'),
(u'Roma', u'http://www.libero-news.it/rss.jsp?sezione=27'), (u'Roma', u'http://www.libero-news.it/rss.jsp?sezione=27'),
(u'Alimentazione', u'http://www.libero-news.it/rss.jsp?sezione=29') (u'Alimentazione', u'http://www.libero-news.it/rss.jsp?sezione=29')

View File

@ -5,7 +5,7 @@ __copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
www.nin.co.rs www.nin.co.rs
''' '''
import re, urllib import re
from calibre import strftime from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
@ -16,13 +16,13 @@ class Nin(BasicNewsRecipe):
publisher = 'NIN d.o.o.' publisher = 'NIN d.o.o.'
category = 'news, politics, Serbia' category = 'news, politics, Serbia'
no_stylesheets = True no_stylesheets = True
delay = 1
oldest_article = 15 oldest_article = 15
encoding = 'utf-8' encoding = 'utf-8'
needs_subscription = True needs_subscription = True
remove_empty_feeds = True remove_empty_feeds = True
PREFIX = 'http://www.nin.co.rs' PREFIX = 'http://www.nin.co.rs'
INDEX = PREFIX + '/?change_lang=ls' INDEX = PREFIX + '/?change_lang=ls'
LOGIN = PREFIX + '/?logout=true'
use_embedded_content = False use_embedded_content = False
language = 'sr' language = 'sr'
publication_type = 'magazine' publication_type = 'magazine'
@ -41,14 +41,12 @@ class Nin(BasicNewsRecipe):
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
br.open(self.INDEX)
if self.username is not None and self.password is not None: if self.username is not None and self.password is not None:
data = urllib.urlencode({ 'login_name':self.username br.open(self.INDEX)
,'login_password':self.password br.select_form(name='form1')
,'imageField.x':'32' br['login_name' ] = self.username
,'imageField.y':'15' br['login_password'] = self.password
}) br.submit()
br.open(self.LOGIN,data)
return br return br
keep_only_tags =[dict(name='td', attrs={'width':'520'})] keep_only_tags =[dict(name='td', attrs={'width':'520'})]

View File

@ -1,96 +1,85 @@
''' '''
Created on 15 May 2010 Created on 15 May 2010
@author: charles @author: charles
''' '''
import os import os
from calibre.devices.usbms.driver import USBMS, BookList from calibre.devices.usbms.driver import USBMS, BookList
# This class is added to the standard device plugin chain, so that it can # This class is added to the standard device plugin chain, so that it can
# be configured. It has invalid vendor_id etc, so it will never match a # be configured. It has invalid vendor_id etc, so it will never match a
# device. The 'real' FOLDER_DEVICE will use the config from it. # device. The 'real' FOLDER_DEVICE will use the config from it.
class FOLDER_DEVICE_FOR_CONFIG(USBMS): class FOLDER_DEVICE_FOR_CONFIG(USBMS):
name = 'Folder Device Interface' name = 'Folder Device Interface'
gui_name = 'Folder Device' gui_name = 'Folder Device'
description = _('Use an arbitrary folder as a device.') description = _('Use an arbitrary folder as a device.')
author = 'John Schember/Charles Haley' author = 'John Schember/Charles Haley'
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb'] FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb']
VENDOR_ID = 0xffff VENDOR_ID = 0xffff
PRODUCT_ID = 0xffff PRODUCT_ID = 0xffff
BCD = 0xffff BCD = 0xffff
class FOLDER_DEVICE(USBMS): class FOLDER_DEVICE(USBMS):
type = _('Device Interface') type = _('Device Interface')
name = 'Folder Device Interface' name = 'Folder Device Interface'
gui_name = 'Folder Device' gui_name = 'Folder Device'
description = _('Use an arbitrary folder as a device.') description = _('Use an arbitrary folder as a device.')
author = 'John Schember/Charles Haley' author = 'John Schember/Charles Haley'
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb'] FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb']
VENDOR_ID = 0xffff VENDOR_ID = 0xffff
PRODUCT_ID = 0xffff PRODUCT_ID = 0xffff
BCD = 0xffff BCD = 0xffff
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
CAN_SET_METADATA = True CAN_SET_METADATA = True
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
#: Icon for this device #: Icon for this device
icon = I('sd.svg') icon = I('sd.svg')
METADATA_CACHE = '.metadata.calibre' METADATA_CACHE = '.metadata.calibre'
_main_prefix = '' _main_prefix = ''
_card_a_prefix = None _card_a_prefix = None
_card_b_prefix = None _card_b_prefix = None
is_connected = False is_connected = False
def __init__(self, path): def __init__(self, path):
if not os.path.isdir(path): if not os.path.isdir(path):
raise IOError, 'Path is not a folder' raise IOError, 'Path is not a folder'
self._main_prefix = path self._main_prefix = path
self.booklist_class = BookList self.booklist_class = BookList
self.is_connected = True self.is_connected = True
@classmethod def disconnect_from_folder(self):
def get_gui_name(cls): self._main_prefix = ''
if hasattr(cls, 'gui_name'): self.is_connected = False
return cls.gui_name
if hasattr(cls, '__name__'): def is_usb_connected(self, devices_on_system, debug=False,
return cls.__name__ only_presence=False):
return cls.name return self.is_connected, self
def disconnect_from_folder(self): def open(self):
self._main_prefix = '' if not self._main_prefix:
self.is_connected = False return False
return True
def is_usb_connected(self, devices_on_system, debug=False,
only_presence=False): def set_progress_reporter(self, report_progress):
return self.is_connected, self self.report_progress = report_progress
def open(self): def card_prefix(self, end_session=True):
if not self._main_prefix: return (None, None)
return False
return True def eject(self):
self.is_connected = False
def set_progress_reporter(self, report_progress):
self.report_progress = report_progress @classmethod
def settings(self):
def card_prefix(self, end_session=True): return FOLDER_DEVICE_FOR_CONFIG._config().parse()
return (None, None)
def get_main_ebook_dir(self):
return ''
def eject(self):
self.is_connected = False
@classmethod
def settings(self):
return FOLDER_DEVICE_FOR_CONFIG._config().parse()

View File

@ -387,7 +387,7 @@ class BookList(list):
__getslice__ = None __getslice__ = None
__setslice__ = None __setslice__ = None
def __init__(self, oncard, prefix): def __init__(self, oncard, prefix, settings):
pass pass
def supports_tags(self): def supports_tags(self):
@ -402,3 +402,17 @@ class BookList(list):
''' '''
raise NotImplementedError() raise NotImplementedError()
def add_book(self, book, replace_metadata):
'''
Add the book to the booklist. Intent is to maintain any device-internal
metadata. Return True if booklists must be sync'ed
'''
raise NotImplementedError()
def remove_book(self, book):
'''
Remove a book from the booklist. Correct any device metadata at the
same time
'''
raise NotImplementedError()

View File

@ -48,6 +48,7 @@ class KOBO(USBMS):
WINDOWS_MAIN_MEM = '.KOBOEREADER' WINDOWS_MAIN_MEM = '.KOBOEREADER'
EBOOK_DIR_MAIN = '' EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True
class AVANT(USBMS): class AVANT(USBMS):
name = 'Booq Avant Device Interface' name = 'Booq Avant Device Interface'

View File

@ -27,7 +27,7 @@ class PRS505(USBMS):
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
path_sep = '/' path_sep = '/'
booklist_class = PRS_BookList # See USBMS for some explanation of this booklist_class = PRS_BookList # See usbms.driver for some explanation of this
FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt'] FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt']

View File

@ -37,7 +37,7 @@ class Book(MetaInformation):
else: else:
self.lpath = lpath self.lpath = lpath
self.mime = mime_type_ext(path_to_ext(lpath)) self.mime = mime_type_ext(path_to_ext(lpath))
self.size = None # will be set later self.size = size # will be set later if None
self.datetime = time.gmtime() self.datetime = time.gmtime()
if other: if other:
@ -108,9 +108,6 @@ class Book(MetaInformation):
class BookList(_BookList): class BookList(_BookList):
def __init__(self, oncard, prefix, settings):
pass
def supports_tags(self): def supports_tags(self):
return True return True
@ -118,16 +115,9 @@ class BookList(_BookList):
book.tags = tags book.tags = tags
def add_book(self, book, replace_metadata): def add_book(self, book, replace_metadata):
'''
Add the book to the booklist. Intent is to maintain any device-internal
metadata. Return True if booklists must be sync'ed
'''
if book not in self: if book not in self:
self.append(book) self.append(book)
return True
def remove_book(self, book): def remove_book(self, book):
'''
Remove a book from the booklist. Correct any device metadata at the
same time
'''
self.remove(book) self.remove(book)

View File

@ -15,6 +15,7 @@ import re
import json import json
from itertools import cycle from itertools import cycle
from calibre import prints
from calibre.devices.usbms.cli import CLI from calibre.devices.usbms.cli import CLI
from calibre.devices.usbms.device import Device from calibre.devices.usbms.device import Device
from calibre.devices.usbms.books import BookList, Book from calibre.devices.usbms.books import BookList, Book
@ -45,15 +46,17 @@ class USBMS(CLI, Device):
def books(self, oncard=None, end_session=True): def books(self, oncard=None, end_session=True):
from calibre.ebooks.metadata.meta import path_to_ext from calibre.ebooks.metadata.meta import path_to_ext
dummy_bl = BookList(None, None, None)
if oncard == 'carda' and not self._card_a_prefix: if oncard == 'carda' and not self._card_a_prefix:
self.report_progress(1.0, _('Getting list of books on device...')) self.report_progress(1.0, _('Getting list of books on device...'))
return [] return dummy_bl
elif oncard == 'cardb' and not self._card_b_prefix: elif oncard == 'cardb' and not self._card_b_prefix:
self.report_progress(1.0, _('Getting list of books on device...')) self.report_progress(1.0, _('Getting list of books on device...'))
return [] return dummy_bl
elif oncard and oncard != 'carda' and oncard != 'cardb': elif oncard and oncard != 'carda' and oncard != 'cardb':
self.report_progress(1.0, _('Getting list of books on device...')) self.report_progress(1.0, _('Getting list of books on device...'))
return [] return dummy_bl
prefix = self._card_a_prefix if oncard == 'carda' else \ prefix = self._card_a_prefix if oncard == 'carda' else \
self._card_b_prefix if oncard == 'cardb' \ self._card_b_prefix if oncard == 'cardb' \
@ -87,7 +90,6 @@ class USBMS(CLI, Device):
self.count_found_in_bl += 1 self.count_found_in_bl += 1
else: else:
item = self.book_from_path(prefix, lpath) item = self.book_from_path(prefix, lpath)
changed = True
if metadata.add_book(item, replace_metadata=False): if metadata.add_book(item, replace_metadata=False):
changed = True changed = True
except: # Probably a filename encoding error except: # Probably a filename encoding error
@ -106,7 +108,7 @@ class USBMS(CLI, Device):
if self.SUPPORTS_SUB_DIRS: if self.SUPPORTS_SUB_DIRS:
for path, dirs, files in os.walk(ebook_dir): for path, dirs, files in os.walk(ebook_dir):
for filename in files: for filename in files:
self.report_progress(50.0, _('Getting list of books on device...')) self.report_progress(0.5, _('Getting list of books on device...'))
changed = update_booklist(filename, path, prefix) changed = update_booklist(filename, path, prefix)
if changed: if changed:
need_sync = True need_sync = True
@ -121,7 +123,7 @@ class USBMS(CLI, Device):
# if count != len(bl) then there were items in it that we did not # if count != len(bl) then there were items in it that we did not
# find on the device. If need_sync is True then there were either items # find on the device. If need_sync is True then there were either items
# on the device that were not in bl or some of the items were changed. # on the device that were not in bl or some of the items were changed.
print "count found in cache: %d, count of files in cache: %d, must_sync_cache: %s" % (self.count_found_in_bl, len(bl), need_sync) #print "count found in cache: %d, count of files in cache: %d, must_sync_cache: %s" % (self.count_found_in_bl, len(bl), need_sync)
if self.count_found_in_bl != len(bl) or need_sync: if self.count_found_in_bl != len(bl) or need_sync:
if oncard == 'cardb': if oncard == 'cardb':
self.sync_booklists((None, None, metadata)) self.sync_booklists((None, None, metadata))
@ -145,7 +147,9 @@ class USBMS(CLI, Device):
mdata, fname = metadata.next(), names.next() mdata, fname = metadata.next(), names.next()
filepath = self.normalize_path(self.create_upload_path(path, mdata, fname)) filepath = self.normalize_path(self.create_upload_path(path, mdata, fname))
paths.append(filepath) paths.append(filepath)
self.put_file(self.normalize_path(infile), filepath, replace_file=True) if not hasattr(infile, 'read'):
infile = self.normalize_path(infile)
self.put_file(infile, filepath, replace_file=True)
try: try:
self.upload_cover(os.path.dirname(filepath), self.upload_cover(os.path.dirname(filepath),
os.path.splitext(os.path.basename(filepath))[0], mdata) os.path.splitext(os.path.basename(filepath))[0], mdata)
@ -187,7 +191,8 @@ class USBMS(CLI, Device):
if not prefix and self._card_b_prefix: if not prefix and self._card_b_prefix:
prefix = self._card_b_prefix if path.startswith(self._card_b_prefix) else None prefix = self._card_b_prefix if path.startswith(self._card_b_prefix) else None
if prefix is None: if prefix is None:
print 'in add_books_to_metadata. Prefix is None!', path, self._main_prefix prints('in add_books_to_metadata. Prefix is None!', path,
self._main_prefix)
continue continue
lpath = path.partition(prefix)[2] lpath = path.partition(prefix)[2]
if lpath.startswith('/') or lpath.startswith('\\'): if lpath.startswith('/') or lpath.startswith('\\'):
@ -237,7 +242,8 @@ class USBMS(CLI, Device):
if prefix is not None and isinstance(booklists[listid], self.booklist_class): if prefix is not None and isinstance(booklists[listid], self.booklist_class):
if not os.path.exists(prefix): if not os.path.exists(prefix):
os.makedirs(self.normalize_path(prefix)) os.makedirs(self.normalize_path(prefix))
js = [item.to_json() for item in booklists[listid]] js = [item.to_json() for item in booklists[listid] if
hasattr(item, 'to_json')]
with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f: with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f:
json.dump(js, f, indent=2, encoding='utf-8') json.dump(js, f, indent=2, encoding='utf-8')
write_prefix(self._main_prefix, 0) write_prefix(self._main_prefix, 0)
@ -248,6 +254,7 @@ class USBMS(CLI, Device):
@classmethod @classmethod
def normalize_path(cls, path): def normalize_path(cls, path):
'Return path with platform native path separators'
if path is None: if path is None:
return None return None
if os.sep == '\\': if os.sep == '\\':
@ -312,6 +319,6 @@ class USBMS(CLI, Device):
if mi is None: if mi is None:
mi = MetaInformation(os.path.splitext(os.path.basename(path))[0], mi = MetaInformation(os.path.splitext(os.path.basename(path))[0],
[_('Unknown')]) [_('Unknown')])
mi.size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size
book = cls.book_class(prefix, path, other=mi) book = cls.book_class(prefix, path, other=mi, size=size)
return book return book

View File

@ -0,0 +1,174 @@
#!/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'
import os
from PyQt4.Qt import QWizard, QWizardPage, QIcon, QPixmap, Qt, QThread, \
pyqtSignal
from calibre.gui2 import error_dialog, choose_dir, gprefs
from calibre.constants import filesystem_encoding
from calibre.library.add_to_library import find_folders_under, \
find_books_in_folder, hash_merge_format_collections
class WizardPage(QWizardPage): # {{{
def __init__(self, db, parent):
QWizardPage.__init__(self, parent)
self.db = db
self.register = parent.register
self.setupUi(self)
self.do_init()
def do_init(self):
pass
# }}}
# Scan root folder Page {{{
from calibre.gui2.add_wizard.scan_ui import Ui_WizardPage as ScanWidget
class RecursiveFinder(QThread):
activity_changed = pyqtSignal(object, object) # description and total count
activity_iterated = pyqtSignal(object, object) # item desc, progress number
def __init__(self, parent=None):
QThread.__init__(self, parent)
self.canceled = False
self.cancel_callback = lambda : self.canceled
self.folders = set([])
self.books = []
def cancel(self, *args):
self.canceled = True
def set_params(self, root, db, one_per_folder):
self.root, self.db = root, db
self.one_per_folder = one_per_folder
def run(self):
self.activity_changed.emit(_('Searching for sub-folders'), 0)
self.folders = find_folders_under(self.root, self.db,
cancel_callback=self.cancel_callback)
if self.canceled:
return
self.activity_changed.emit(_('Searching for books'), len(self.folders))
for i, folder in enumerate(self.folders):
if self.canceled:
break
books_in_folder = find_books_in_folder(folder, self.one_per_folder,
cancel_callback=self.cancel_callback)
if self.canceled:
break
self.books.extend(books_in_folder)
self.activity_iterated.emit(folder, i)
self.activity_changed.emit(
_('Looking for duplicates based on file hash'), 0)
self.books = hash_merge_format_collections(self.books,
cancel_callback=self.cancel_callback)
class ScanPage(WizardPage, ScanWidget):
ID = 2
# }}}
# Welcome Page {{{
from calibre.gui2.add_wizard.welcome_ui import Ui_WizardPage as WelcomeWidget
class WelcomePage(WizardPage, WelcomeWidget):
ID = 1
def do_init(self):
# Root folder must be filled
self.registerField('root_folder*', self.opt_root_folder)
self.register['root_folder'] = self.get_root_folder
self.register['one_per_folder'] = self.get_one_per_folder
self.button_choose_root_folder.clicked.connect(self.choose_root_folder)
def choose_root_folder(self, *args):
x = self.get_root_folder()
if x is None:
x = '~'
x = choose_dir(self, 'add wizard choose root folder',
_('Choose root folder'), default_dir=x)
if x is not None:
self.opt_root_folder.setText(os.path.abspath(x))
def initializePage(self):
opf = gprefs.get('add wizard one per folder', True)
self.opt_one_per_folder.setChecked(opf)
self.opt_many_per_folder.setChecked(not opf)
add_dir = gprefs.get('add wizard root folder', None)
if add_dir is not None:
self.opt_root_folder.setText(add_dir)
def get_root_folder(self):
x = unicode(self.opt_root_folder.text()).strip()
if not x:
return None
return os.path.abspath(x.encode(filesystem_encoding))
def get_one_per_folder(self):
return self.opt_one_per_folder.isChecked()
def validatePage(self):
x = self.get_root_folder()
xu = x.decode(filesystem_encoding)
if x and os.access(x, os.R_OK) and os.path.isdir(x):
gprefs['add wizard root folder'] = xu
gprefs['add wizard one per folder'] = self.get_one_per_folder()
return True
error_dialog(self, _('Invalid root folder'),
xu + _('is not a valid root folder'), show=True)
return False
# }}}
class Wizard(QWizard): # {{{
def __init__(self, db, parent=None):
QWizard.__init__(self, parent)
self.setModal(True)
self.setWindowTitle(_('Add books to calibre'))
self.setWindowIcon(QIcon(I('add_book.svg')))
self.setPixmap(self.LogoPixmap, QPixmap(P('content_server/calibre.png')).scaledToHeight(80,
Qt.SmoothTransformation))
self.setPixmap(self.WatermarkPixmap,
QPixmap(I('welcome_wizard.svg')))
self.register = {}
for attr, cls in [
('welcome_page', WelcomePage),
('scan_page', ScanPage),
]:
setattr(self, attr, cls(db, self))
self.setPage(getattr(cls, 'ID'), getattr(self, attr))
# }}}
# Test Wizard {{{
if __name__ == '__main__':
from PyQt4.Qt import QApplication
from calibre.library import db
app = QApplication([])
w = Wizard(db())
w.exec_()
# }}}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WizardPage</class>
<widget class="QWizardPage" name="WizardPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<property name="title">
<string>Scanning root folder for books</string>
</property>
<property name="subTitle">
<string>This may take a few minutes</string>
</property>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WizardPage</class>
<widget class="QWizardPage" name="WizardPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>704</width>
<height>468</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<property name="title">
<string>Choose the location to add books from</string>
</property>
<property name="subTitle">
<string>Select a folder on your hard disk</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;p&gt;calibre can scan your computer for existing books automatically. These books will then be &lt;b&gt;copied&lt;/b&gt; into the calibre library. This wizard will help you customize the scanning and import process for your existing book collection.&lt;/p&gt;
&lt;p&gt;Choose a root folder. Books will be searched for only inside this folder and any sub-folders.&lt;/p&gt;
&lt;p&gt;Make sure that the folder you chose for your calibre library &lt;b&gt;is not&lt;/b&gt; under the root folder you choose.&lt;/p&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Root folder:</string>
</property>
<property name="buddy">
<cstring>opt_root_folder</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="opt_root_folder">
<property name="toolTip">
<string>This folder and its sub-folders will be scanned for books to import into calibre's library</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QToolButton" name="button_choose_root_folder">
<property name="toolTip">
<string>Choose root folder</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Handle multiple files per book</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QRadioButton" name="opt_one_per_folder">
<property name="text">
<string>&amp;One book per folder, assumes every ebook file in a folder is the same book in a different format</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="opt_many_per_folder">
<property name="text">
<string>&amp;Multiple books per folder, assumes every ebook file is a different book</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="5" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -426,12 +426,12 @@ class DeviceMenu(QMenu):
self.addMenu(self.email_to_menu) self.addMenu(self.email_to_menu)
self.addSeparator() self.addSeparator()
mitem = self.addAction(_('Connect to folder')) mitem = self.addAction(QIcon(I('document_open.svg')), _('Connect to folder'))
mitem.setEnabled(True) mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_folder.emit()) mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
self.connect_to_folder_action = mitem self.connect_to_folder_action = mitem
mitem = self.addAction(_('Disconnect from folder')) mitem = self.addAction(QIcon(I('eject.svg')), _('Disconnect from folder'))
mitem.setEnabled(False) mitem.setEnabled(False)
mitem.triggered.connect(lambda x : self.disconnect_from_folder.emit()) mitem.triggered.connect(lambda x : self.disconnect_from_folder.emit())
self.disconnect_from_folder_action = mitem self.disconnect_from_folder_action = mitem

View File

@ -109,6 +109,9 @@ class PluginModel(QAbstractItemModel):
self._data[plugin.type].append(plugin) self._data[plugin.type].append(plugin)
self.categories = sorted(self._data.keys()) self.categories = sorted(self._data.keys())
for plugins in self._data.values():
plugins.sort(cmp=lambda x, y: cmp(x.name.lower(), y.name.lower()))
def index(self, row, column, parent): def index(self, row, column, parent):
if not self.hasIndex(row, column, parent): if not self.hasIndex(row, column, parent):
return QModelIndex() return QModelIndex()

View File

@ -973,8 +973,8 @@ class BooksModel(QAbstractTableModel):
self.db.set(row, column, val) self.db.set(row, column, val)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
index, index) index, index)
if column == self.sorted_on[0]: #if column == self.sorted_on[0]:
self.resort() # self.resort()
return True return True
def set_search_restriction(self, s): def set_search_restriction(self, s):

View File

@ -0,0 +1,178 @@
#!/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'
import os
from hashlib import sha1
from calibre.constants import filesystem_encoding
from calibre.ebooks import BOOK_EXTENSIONS
def find_folders_under(root, db, add_root=True, # {{{
follow_links=False, cancel_callback=lambda : False):
'''
Find all folders under the specified root path, ignoring any folders under
the library path of db
root must be a bytestring in filesystem_encoding
If follow_links is True, follow symbolic links. WARNING; this can lead to
infinite recursion.
cancel_callback must be a no argument callable that returns True to cancel
the search
'''
assert not isinstance(root, unicode) # root must be in filesystem encoding
lp = db.library_path
if isinstance(lp, unicode):
try:
lp = lp.encode(filesystem_encoding)
except:
lp = None
if lp:
lp = os.path.abspath(lp)
root = os.path.abspath(root)
ans = set([])
for dirpath, dirnames, __ in os.walk(root, topdown=True, followlinks=follow_links):
if cancel_callback():
break
for x in list(dirnames):
path = os.path.join(dirpath, x)
if lp and path.startswith(lp):
dirnames.remove(x)
if lp and dirpath.startswith(lp):
continue
ans.add(dirpath)
if not add_root:
ans.remove(root)
return ans
# }}}
class FormatCollection(object): # {{{
def __init__(self, parent_folder, formats):
self.path_map = {}
for x in set(formats):
fmt = os.path.splitext(x)[1].lower()
if fmt:
fmt = fmt[1:]
self.path_map[fmt] = x
self.parent_folder = None
self.hash_map = {}
for fmt, path in self.format_map.items():
self.hash_map[fmt] = self.hash_of_file(path)
def hash_of_file(self, path):
with open(path, 'rb') as f:
return sha1(f.read()).digest()
@property
def hashes(self):
return frozenset(self.formats.values())
@property
def is_empty(self):
return len(self) == 0
def __iter__(self):
for x in self.path_map:
yield x
def __len__(self):
return len(self.path_map)
def remove(self, fmt):
self.hash_map.pop(fmt, None)
self.path_map.pop(fmt, None)
def matches(self, other):
if not self.hashes.intersection(other.hashes):
return False
for fmt in self:
if self.hash_map[fmt] != other.hash_map.get(fmt, False):
return False
return True
def merge(self, other):
for fmt in list(other):
self.path_map[fmt] = other.path_map[fmt]
self.hash_map[fmt] = other.hash_map[fmt]
other.remove(fmt)
# }}}
def books_in_folder(folder, one_per_folder, # {{{
cancel_callback=lambda : False):
assert not isinstance(folder, unicode)
dirpath = os.path.abspath(folder)
if one_per_folder:
formats = set([])
for path in os.listdir(dirpath):
if cancel_callback():
return []
path = os.path.abspath(os.path.join(dirpath, path))
if os.path.isdir(path) or not os.access(path, os.R_OK):
continue
ext = os.path.splitext(path)[1]
if not ext:
continue
ext = ext[1:].lower()
if ext not in BOOK_EXTENSIONS and ext != 'opf':
continue
formats.add(path)
return [FormatCollection(folder, formats)]
else:
books = {}
for path in os.listdir(dirpath):
if cancel_callback():
return
path = os.path.abspath(os.path.join(dirpath, path))
if os.path.isdir(path) or not os.access(path, os.R_OK):
continue
ext = os.path.splitext(path)[1]
if not ext:
continue
ext = ext[1:].lower()
if ext not in BOOK_EXTENSIONS:
continue
key = os.path.splitext(path)[0]
if not books.has_key(key):
books[key] = set([])
books[key].add(path)
return [FormatCollection(folder, x) for x in books.values() if x]
# }}}
def hash_merge_format_collections(collections, cancel_callback=lambda:False):
ans = []
collections = list(collections)
l = len(collections)
for i in range(l):
if cancel_callback():
return collections
one = collections[i]
if one.is_empty:
continue
for j in range(i+1, l):
if cancel_callback():
return collections
two = collections[j]
if two.is_empty:
continue
if one.matches(two):
one.merge(two)
ans.append(one)
return ans