mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from custcol trunk
This commit is contained in:
commit
1eabbcfb9a
40
resources/recipes/agrogerila.recipe
Normal file
40
resources/recipes/agrogerila.recipe
Normal 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)
|
||||||
|
|
||||||
|
|
@ -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')
|
||||||
|
@ -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'})]
|
||||||
|
@ -58,14 +58,6 @@ class FOLDER_DEVICE(USBMS):
|
|||||||
self.booklist_class = BookList
|
self.booklist_class = BookList
|
||||||
self.is_connected = True
|
self.is_connected = True
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_gui_name(cls):
|
|
||||||
if hasattr(cls, 'gui_name'):
|
|
||||||
return cls.gui_name
|
|
||||||
if hasattr(cls, '__name__'):
|
|
||||||
return cls.__name__
|
|
||||||
return cls.name
|
|
||||||
|
|
||||||
def disconnect_from_folder(self):
|
def disconnect_from_folder(self):
|
||||||
self._main_prefix = ''
|
self._main_prefix = ''
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
@ -85,9 +77,6 @@ class FOLDER_DEVICE(USBMS):
|
|||||||
def card_prefix(self, end_session=True):
|
def card_prefix(self, end_session=True):
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
def get_main_ebook_dir(self):
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def eject(self):
|
def eject(self):
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
174
src/calibre/gui2/add_wizard/__init__.py
Normal file
174
src/calibre/gui2/add_wizard/__init__.py
Normal 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_()
|
||||||
|
# }}}
|
||||||
|
|
25
src/calibre/gui2/add_wizard/scan.ui
Normal file
25
src/calibre/gui2/add_wizard/scan.ui
Normal 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>
|
134
src/calibre/gui2/add_wizard/welcome.ui
Normal file
134
src/calibre/gui2/add_wizard/welcome.ui
Normal 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><p>calibre can scan your computer for existing books automatically. These books will then be <b>copied</b> into the calibre library. This wizard will help you customize the scanning and import process for your existing book collection.</p>
|
||||||
|
<p>Choose a root folder. Books will be searched for only inside this folder and any sub-folders.</p>
|
||||||
|
<p>Make sure that the folder you chose for your calibre library <b>is not</b> under the root folder you choose.</p></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>&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>&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>&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>
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
178
src/calibre/library/add_to_library.py
Normal file
178
src/calibre/library/add_to_library.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user