diff --git a/recipes/metro_uk.recipe b/recipes/metro_uk.recipe
index 2d5155ef29..287af47f5c 100644
--- a/recipes/metro_uk.recipe
+++ b/recipes/metro_uk.recipe
@@ -1,3 +1,4 @@
+import re
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1306097511(BasicNewsRecipe):
title = u'Metro UK'
@@ -10,6 +11,7 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe):
remove_empty_feeds = True
remove_javascript = True
+ preprocess_regexps = [(re.compile(r'Tweet'), lambda a : '')]
language = 'en_GB'
diff --git a/recipes/perfil.recipe b/recipes/perfil.recipe
index 1104202318..af7072c6f6 100644
--- a/recipes/perfil.recipe
+++ b/recipes/perfil.recipe
@@ -26,6 +26,7 @@ class Perfil(BasicNewsRecipe):
.foto1 h1{font-size: x-small}
h1{font-family: Georgia,"Times New Roman",serif}
img{margin-bottom: 0.4em}
+ .hora{font-size: x-small; color: red}
"""
conversion_options = {
@@ -60,7 +61,26 @@ class Perfil(BasicNewsRecipe):
,(u'Tecnologia' , u'http://www.perfil.com/rss/tecnologia.xml' )
]
+ def get_article_url(self, article):
+ return article.get('guid', None)
+
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
+ for item in soup.findAll('a'):
+ limg = item.find('img')
+ if item.string is not None:
+ str = item.string
+ item.replaceWith(str)
+ else:
+ if limg:
+ item.name = 'div'
+ item.attrs = []
+ else:
+ str = self.tag_to_string(item)
+ item.replaceWith(str)
+ for item in soup.findAll('img'):
+ if not item.has_key('alt'):
+ item['alt'] = 'image'
return soup
+
\ No newline at end of file
diff --git a/recipes/wsj.recipe b/recipes/wsj.recipe
index cf84722bac..a3bc041d25 100644
--- a/recipes/wsj.recipe
+++ b/recipes/wsj.recipe
@@ -51,7 +51,7 @@ class WallStreetJournal(BasicNewsRecipe):
br['password'] = self.password
res = br.submit()
raw = res.read()
- if 'Welcome,' not in raw:
+ if 'Welcome,' not in raw and '>Logout<' not in raw:
raise ValueError('Failed to log in to wsj.com, check your '
'username and password')
return br
diff --git a/src/calibre/devices/hanvon/driver.py b/src/calibre/devices/hanvon/driver.py
index 3798257c2d..3ce0fedac0 100644
--- a/src/calibre/devices/hanvon/driver.py
+++ b/src/calibre/devices/hanvon/driver.py
@@ -61,7 +61,7 @@ class LIBREAIR(N516):
BCD = [0x399]
VENDOR_NAME = 'ALURATEK'
- WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGET'
+ WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET'
EBOOK_DIR_MAIN = 'Books'
class ALEX(N516):
diff --git a/src/calibre/ebooks/html/input.py b/src/calibre/ebooks/html/input.py
index ce6c46c6cf..69eb493c7d 100644
--- a/src/calibre/ebooks/html/input.py
+++ b/src/calibre/ebooks/html/input.py
@@ -457,7 +457,7 @@ class HTMLInput(InputFormatPlugin):
href=bhref)
guessed = self.guess_type(href)[0]
media_type = guessed or self.BINARY_MIME
- if 'text' in media_type:
+ if media_type == 'text/plain':
self.log.warn('Ignoring link to text file %r'%link_)
return None
diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py
index db83fca496..d75620adbd 100644
--- a/src/calibre/ebooks/oeb/base.py
+++ b/src/calibre/ebooks/oeb/base.py
@@ -1055,6 +1055,12 @@ class Manifest(object):
and len(a) == 0 and not a.text:
remove_elem(a)
+ # Convert
s with content into paragraphs as ADE can't handle
+ # them
+ for br in xpath(data, '//h:br'):
+ if len(br) > 0 or br.text:
+ br.tag = XHTML('div')
+
return data
def _parse_txt(self, data):
@@ -1156,7 +1162,7 @@ class Manifest(object):
data = self._parse_xml(data)
elif self.media_type.lower() in OEB_STYLES:
data = self._parse_css(data)
- elif 'text' in self.media_type.lower():
+ elif self.media_type.lower() == 'text/plain':
self.oeb.log.warn('%s contains data in TXT format'%self.href,
'converting to HTML')
data = self._parse_txt(data)
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index c9c79095f3..99f795cdda 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -119,6 +119,7 @@ class DeviceManager(Thread): # {{{
self.sleep_time = sleep_time
self.connected_slot = connected_slot
self.jobs = Queue.Queue(0)
+ self.job_steps = Queue.Queue(0)
self.keep_going = True
self.job_manager = job_manager
self.reported_errors = set([])
@@ -235,6 +236,12 @@ class DeviceManager(Thread): # {{{
self.connected_device.unmount_device()
def next(self):
+ if not self.job_steps.empty():
+ try:
+ return self.job_steps.get_nowait()
+ except Queue.Empty:
+ pass
+
if not self.jobs.empty():
try:
return self.jobs.get_nowait()
@@ -271,13 +278,20 @@ class DeviceManager(Thread): # {{{
break
time.sleep(self.sleep_time)
- def create_job(self, func, done, description, args=[], kwargs={}):
+ def create_job_step(self, func, done, description, to_job, args=[], kwargs={}):
job = DeviceJob(func, done, self.job_manager,
args=args, kwargs=kwargs, description=description)
self.job_manager.add_job(job)
- self.jobs.put(job)
+ if (done is None or isinstance(done, FunctionDispatcher)) and \
+ (to_job is not None and to_job == self.current_job):
+ self.job_steps.put(job)
+ else:
+ self.jobs.put(job)
return job
+ def create_job(self, func, done, description, args=[], kwargs={}):
+ return self.create_job_step(func, done, description, None, args, kwargs)
+
def has_card(self):
try:
return bool(self.device.card_prefix())
@@ -295,10 +309,10 @@ class DeviceManager(Thread): # {{{
self._device_information = {'info': info, 'prefixes': cp, 'freespace': fs}
return info, cp, fs
- def get_device_information(self, done):
+ def get_device_information(self, done, add_as_step_to_job=None):
'''Get device information and free space on device'''
- return self.create_job(self._get_device_information, done,
- description=_('Get device information'))
+ return self.create_job_step(self._get_device_information, done,
+ description=_('Get device information'), to_job=add_as_step_to_job)
def get_current_device_information(self):
return self._device_information
@@ -310,36 +324,38 @@ class DeviceManager(Thread): # {{{
cardblist = self.device.books(oncard='cardb')
return (mainlist, cardalist, cardblist)
- def books(self, done):
+ def books(self, done, add_as_step_to_job=None):
'''Return callable that returns the list of books on device as two booklists'''
- return self.create_job(self._books, done, description=_('Get list of books on device'))
+ return self.create_job_step(self._books, done,
+ description=_('Get list of books on device'), to_job=add_as_step_to_job)
def _annotations(self, path_map):
return self.device.get_annotations(path_map)
- def annotations(self, done, path_map):
+ def annotations(self, done, path_map, add_as_step_to_job=None):
'''Return mapping of ids to annotations. Each annotation is of the
form (type, location_info, content). path_map is a mapping of
ids to paths on the device.'''
- return self.create_job(self._annotations, done, args=[path_map],
- description=_('Get annotations from device'))
+ return self.create_job_step(self._annotations, done, args=[path_map],
+ description=_('Get annotations from device'), to_job=add_as_step_to_job)
def _sync_booklists(self, booklists):
'''Sync metadata to device'''
self.device.sync_booklists(booklists, end_session=False)
return self.device.card_prefix(end_session=False), self.device.free_space()
- def sync_booklists(self, done, booklists, plugboards):
+ def sync_booklists(self, done, booklists, plugboards, add_as_step_to_job=None):
if hasattr(self.connected_device, 'set_plugboards') and \
callable(self.connected_device.set_plugboards):
self.connected_device.set_plugboards(plugboards, find_plugboard)
- return self.create_job(self._sync_booklists, done, args=[booklists],
- description=_('Send metadata to device'))
+ return self.create_job_step(self._sync_booklists, done, args=[booklists],
+ description=_('Send metadata to device'), to_job=add_as_step_to_job)
- def upload_collections(self, done, booklist, on_card):
- return self.create_job(booklist.rebuild_collections, done,
+ def upload_collections(self, done, booklist, on_card, add_as_step_to_job=None):
+ return self.create_job_step(booklist.rebuild_collections, done,
args=[booklist, on_card],
- description=_('Send collections to device'))
+ description=_('Send collections to device'),
+ to_job=add_as_step_to_job)
def _upload_books(self, files, names, on_card=None, metadata=None, plugboards=None):
'''Upload books to device: '''
@@ -374,11 +390,12 @@ class DeviceManager(Thread): # {{{
metadata=metadata, end_session=False)
def upload_books(self, done, files, names, on_card=None, titles=None,
- metadata=None, plugboards=None):
+ metadata=None, plugboards=None, add_as_step_to_job=None):
desc = _('Upload %d books to device')%len(names)
if titles:
desc += u':' + u', '.join(titles)
- return self.create_job(self._upload_books, done, args=[files, names],
+ return self.create_job_step(self._upload_books, done, to_job=add_as_step_to_job,
+ args=[files, names],
kwargs={'on_card':on_card,'metadata':metadata,'plugboards':plugboards}, description=desc)
def add_books_to_metadata(self, locations, metadata, booklists):
@@ -388,9 +405,10 @@ class DeviceManager(Thread): # {{{
'''Remove books from device'''
self.device.delete_books(paths, end_session=True)
- def delete_books(self, done, paths):
- return self.create_job(self._delete_books, done, args=[paths],
- description=_('Delete books from device'))
+ def delete_books(self, done, paths, add_as_step_to_job=None):
+ return self.create_job_step(self._delete_books, done, args=[paths],
+ description=_('Delete books from device'),
+ to_job=add_as_step_to_job)
def remove_books_from_metadata(self, paths, booklists):
self.device.remove_books_from_metadata(paths, booklists)
@@ -405,9 +423,10 @@ class DeviceManager(Thread): # {{{
self.device.get_file(path, f)
f.close()
- def save_books(self, done, paths, target):
- return self.create_job(self._save_books, done, args=[paths, target],
- description=_('Download books from device'))
+ def save_books(self, done, paths, target, add_as_step_to_job=None):
+ return self.create_job_step(self._save_books, done, args=[paths, target],
+ description=_('Download books from device'),
+ to_job=add_as_step_to_job)
def _view_book(self, path, target):
f = open(target, 'wb')
@@ -415,9 +434,9 @@ class DeviceManager(Thread): # {{{
f.close()
return target
- def view_book(self, done, path, target):
- return self.create_job(self._view_book, done, args=[path, target],
- description=_('View book on device'))
+ def view_book(self, done, path, target, add_as_step_to_job=None):
+ return self.create_job_step(self._view_book, done, args=[path, target],
+ description=_('View book on device'), to_job=add_as_step_to_job)
def set_current_library_uuid(self, uuid):
self.current_library_uuid = uuid
@@ -778,7 +797,8 @@ class DeviceMixin(object): # {{{
self.device_manager.device.icon)
self.bars_manager.update_bars()
self.status_bar.device_connected(info[0])
- self.device_manager.books(FunctionDispatcher(self.metadata_downloaded))
+ self.device_manager.books(FunctionDispatcher(self.metadata_downloaded),
+ add_as_step_to_job=job)
def metadata_downloaded(self, job):
'''
@@ -788,7 +808,7 @@ class DeviceMixin(object): # {{{
self.device_job_exception(job)
return
# set_books_in_library might schedule a sync_booklists job
- self.set_books_in_library(job.result, reset=True)
+ self.set_books_in_library(job.result, reset=True, add_as_step_to_job=job)
mainlist, cardalist, cardblist = job.result
self.memory_view.set_database(mainlist)
self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA,
@@ -843,8 +863,8 @@ class DeviceMixin(object): # {{{
# set_books_in_library even though books were not added because
# the deleted book might have been an exact match. Upload the booklists
# if set_books_in_library did not.
- if not self.set_books_in_library(self.booklists(), reset=True):
- self.upload_booklists()
+ if not self.set_books_in_library(self.booklists(), reset=True, add_as_step_to_job=job):
+ self.upload_booklists(job)
self.book_on_device(None, reset=True)
# We need to reset the ondevice flags in the library. Use a big hammer,
# so we don't need to worry about whether some succeeded or not.
@@ -1193,13 +1213,14 @@ class DeviceMixin(object): # {{{
self.device_manager.sync_booklists(Dispatcher(lambda x: x),
self.booklists(), plugboards)
- def upload_booklists(self):
+ def upload_booklists(self, add_as_step_to_job=None):
'''
Upload metadata to device.
'''
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
self.device_manager.sync_booklists(FunctionDispatcher(self.metadata_synced),
- self.booklists(), plugboards)
+ self.booklists(), plugboards,
+ add_as_step_to_job=add_as_step_to_job)
def metadata_synced(self, job):
'''
@@ -1274,8 +1295,8 @@ class DeviceMixin(object): # {{{
# because the UUID changed. Force both the device and the library view
# to refresh the flags. Set_books_in_library could upload the booklists.
# If it does not, then do it here.
- if not self.set_books_in_library(self.booklists(), reset=True):
- self.upload_booklists()
+ if not self.set_books_in_library(self.booklists(), reset=True, add_as_step_to_job=job):
+ self.upload_booklists(job)
with self.library_view.preserve_selected_books:
self.book_on_device(None, reset=True)
self.refresh_ondevice()
@@ -1335,7 +1356,7 @@ class DeviceMixin(object): # {{{
loc[4] |= self.book_db_uuid_path_map[id]
return loc
- def set_books_in_library(self, booklists, reset=False):
+ def set_books_in_library(self, booklists, reset=False, add_as_step_to_job=None):
'''
Set the ondevice indications in the device database.
This method should be called before book_on_device is called, because
@@ -1487,7 +1508,7 @@ class DeviceMixin(object): # {{{
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
self.device_manager.sync_booklists(
FunctionDispatcher(self.metadata_synced), booklists,
- plugboards)
+ plugboards, add_as_step_to_job)
return update_metadata
# }}}
diff --git a/src/calibre/gui2/jobs.py b/src/calibre/gui2/jobs.py
index 20154a298b..589b28d520 100644
--- a/src/calibre/gui2/jobs.py
+++ b/src/calibre/gui2/jobs.py
@@ -432,6 +432,10 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.jobs_view.horizontalHeader().restoreState(QByteArray(state))
except:
pass
+ idx = self.jobs_view.model().index(0, 0)
+ if idx.isValid():
+ sm = self.jobs_view.selectionModel()
+ sm.select(idx, sm.ClearAndSelect|sm.Rows)
def save_state(self):
try:
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index a6a852fbdd..3b8c27866c 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -727,6 +727,15 @@ class TagTreeItem(object): # {{{
else:
self.tag.state = set_to
+ def all_children(self):
+ res = []
+ def recurse(nodes, res):
+ for t in nodes:
+ res.append(t)
+ recurse(t.children, res)
+ recurse(self.children, res)
+ return res
+
def child_tags(self):
res = []
def recurse(nodes, res):
@@ -1269,6 +1278,7 @@ class TagsModel(QAbstractItemModel): # {{{
category_icon = category_node.icon,
category_key=category_node.category_key,
icon_map=self.icon_state_map)
+ sub_cat.tag.is_searchable = False
self.endInsertRows()
else: # by 'first letter'
cl = cl_list[idx]
@@ -1644,14 +1654,23 @@ class TagsModel(QAbstractItemModel): # {{{
if node.tag.state:
if node.category_key == "news":
if node_searches[node.tag.state] == 'true':
- ans.append('tags:=news')
+ ans.append('tags:"=' + _('News') + '"')
else:
- ans.append('( not tags:=news )')
+ ans.append('( not tags:"=' + _('News') + '")')
else:
ans.append('%s:%s'%(node.category_key, node_searches[node.tag.state]))
key = node.category_key
- for tag_item in node.child_tags():
+ for tag_item in node.all_children():
+ if tag_item.type == TagTreeItem.CATEGORY:
+ if self.collapse_model == 'first letter' and \
+ tag_item.temporary and not key.startswith('@') \
+ and tag_item.tag.state:
+ if node_searches[tag_item.tag.state] == 'true':
+ ans.append('%s:~^%s'%(key, tag_item.py_name))
+ else:
+ ans.append('(not %s:~^%s )'%(key, tag_item.py_name))
+ continue
tag = tag_item.tag
if tag.state != TAG_SEARCH_STATES['clear']:
if tag.state == TAG_SEARCH_STATES['mark_minus'] or \
diff --git a/src/calibre/gui2/update.py b/src/calibre/gui2/update.py
index aacf30fe10..c9a908bdf6 100644
--- a/src/calibre/gui2/update.py
+++ b/src/calibre/gui2/update.py
@@ -179,7 +179,7 @@ class UpdateMixin(object):
def plugin_update_found(self, number_of_updates):
# Change the plugin icon to indicate there are updates available
- plugin = self.iactions.get('Plugin Updates', None)
+ plugin = self.iactions.get('Plugin Updater', None)
if not plugin:
return
if number_of_updates:
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index 601071a2ce..b9dd2f3ed7 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -145,7 +145,7 @@ def _match(query, value, matchkind):
return True
elif query == t:
return True
- elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I)) or ### search unanchored
+ elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I|re.UNICODE)) or ### search unanchored
(matchkind == CONTAINS_MATCH and query in t)):
return True
except re.error:
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index 97454c90e2..006b381214 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -3,7 +3,7 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Greg Riker'
-import codecs, datetime, htmlentitydefs, os, re, shutil, time, zlib
+import codecs, datetime, htmlentitydefs, os, re, shutil, zlib
from collections import namedtuple
from copy import deepcopy
from xml.sax.saxutils import escape
@@ -25,7 +25,7 @@ from calibre.utils.html2text import html2text
from calibre.utils.icu import capitalize
from calibre.utils.logging import default_log as log
from calibre.utils.magick.draw import thumbnail
-from calibre.utils.zipfile import ZipFile, ZipInfo
+from calibre.utils.zipfile import ZipFile
FIELDS = ['all', 'title', 'title_sort', 'author_sort', 'authors', 'comments',
'cover', 'formats','id', 'isbn', 'ondevice', 'pubdate', 'publisher',
@@ -4704,24 +4704,33 @@ Author '{0}':
to be replaced.
'''
+ def open_archive(mode='r'):
+ try:
+ return ZipFile(self.__archive_path, mode=mode)
+ except:
+ # Happens on windows if the file is opened by another
+ # process
+ pass
+
# Generate crc for current cover
#self.opts.log.info(" generateThumbnail():")
- data = open(title['cover'], 'rb').read()
+ with open(title['cover'], 'rb') as f:
+ data = f.read()
cover_crc = hex(zlib.crc32(data))
# Test cache for uuid
- with ZipFile(self.__archive_path, mode='r') as zfr:
- try:
- t_info = zfr.getinfo(title['uuid'])
- except:
- pass
- else:
- if t_info.comment == cover_crc:
+ zf = open_archive()
+ if zf is not None:
+ with zf:
+ try:
+ zf.getinfo(title['uuid']+cover_crc)
+ except:
+ pass
+ else:
# uuid found in cache with matching crc
- thumb_data = zfr.read(title['uuid'])
- zfr.extract(title['uuid'],image_dir)
- os.rename(os.path.join(image_dir,title['uuid']),
- os.path.join(image_dir,thumb_file))
+ thumb_data = zf.read(title['uuid'])
+ with open(os.path.join(image_dir, thumb_file), 'wb') as f:
+ f.write(thumb_data)
return
@@ -4732,10 +4741,13 @@ Author '{0}':
f.write(thumb_data)
# Save thumb to archive
- t_info = ZipInfo(title['uuid'],time.localtime()[0:6])
- t_info.comment = cover_crc
- with ZipFile(self.__archive_path, mode='a') as zfw:
- zfw.writestr(t_info, thumb_data)
+ if zf is not None: # Ensure that the read succeeded
+ # If we failed to open the zip file for reading,
+ # we dont know if it contained the thumb or not
+ zf = open_archive('a')
+ if zf is not None:
+ with zf:
+ zf.writestr(title['uuid']+cover_crc, thumb_data)
def getFriendlyGenreTag(self, genre):
# Find the first instance of friendly_tag matching genre
diff --git a/src/calibre/library/restore.py b/src/calibre/library/restore.py
index 20065309aa..8bd7174849 100644
--- a/src/calibre/library/restore.py
+++ b/src/calibre/library/restore.py
@@ -24,6 +24,7 @@ NON_EBOOK_EXTENSIONS = frozenset([
class RestoreDatabase(LibraryDatabase2):
PATH_LIMIT = 10
+ WINDOWS_LIBRARY_PATH_LIMIT = 180
def set_path(self, *args, **kwargs):
pass
diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst
index fea828f706..9244109ba7 100644
--- a/src/calibre/manual/conversion.rst
+++ b/src/calibre/manual/conversion.rst
@@ -657,6 +657,7 @@ Some limitations of PDF input are:
* Some PDFs store their images upside down with a rotation instruction, |app| currently doesn't support that instruction, so the images will be rotated in the output as well.
* Links and Tables of Contents are not supported
* PDFs that use embedded non-unicode fonts to represent non-English characters will result in garbled output for those characters
+ * Some PDFs are made up of photographs of the page with OCRed text behind them. In such cases |app| uses the OCRed text, which can be very different from what you see when you view the PDF file
To re-iterate **PDF is a really, really bad** format to use as input. If you absolutely must use PDF, then be prepared for an
output ranging anywhere from decent to unusable, depending on the input PDF.
diff --git a/src/calibre/manual/develop.rst b/src/calibre/manual/develop.rst
index c49176ceb2..fecdf28a47 100644
--- a/src/calibre/manual/develop.rst
+++ b/src/calibre/manual/develop.rst
@@ -28,7 +28,7 @@ For example, adding support for a new device to |app| typically involves writing
a device driver plugin. You can browse the
`built-in drivers `_. Similarly, adding support
for new conversion formats involves writing input/output format plugins. Another example of the modular design is the :ref:`recipe system ` for
-fetching news.
+fetching news. For more examples of plugins designed to add features to |app|, see the `plugin index `_.
Code layout
^^^^^^^^^^^^^^
@@ -36,10 +36,21 @@ Code layout
All the |app| python code is in the ``calibre`` package. This package contains the following main sub-packages
* devices - All the device drivers. Just look through some of the built-in drivers to get an idea for how they work.
- * ebooks - All the ebook conversion code. A good starting point is ``calibre.ebooks.conversion.cli`` which is the
- module powering the :command:`ebook-convert` command.
- * library - The database backed and the content server.
- * gui2 - The Graphical User Interface.
+
+ * For details, see: devices.interface which defines the interface supported by device drivers and devices.usbms which
+ defines a generic driver that connects to a USBMS device. All USBMS based drivers in calibre inherit from it.
+
+ * ebooks - All the ebook conversion/metadata code. A good starting point is ``calibre.ebooks.conversion.cli`` which is the
+ module powering the :command:`ebook-convert` command. The conversion process is controlled via conversion.plumber.
+ The format independent code is all in ebooks.oeb and the format dependent stuff is in ebooks.format_name.
+
+ * Metadata reading writing and downloading is all in ebooks.metadata
+
+ * library - The database backed and the content server. See library.database2 for the interface to the calibre library. library.server is the calibre Content Server.
+ * gui2 - The Graphical User Interface. GUI initialization happens in gui2.main and gui2.ui. The ebook-viewer is in gui2.viewer.
+
+If you need help understanding the code, post in the `development forum `_
+and you will most likely get help from one of |app|'s many developers.
Getting the code
------------------
@@ -82,9 +93,9 @@ Now whenever you commit changes to your branch with the command::
bzr commit -m "Comment describing your change"
-I can merge it directly from you branch into the main |app| source tree. You should also subscribe to the |app|
-developers mailing list `calibre-devs `_. Before making major changes, you should
-discuss them on the mailing list or the #calibre IRC channel on Freenode to ensure that the changes will be accepted once you're done.
+I can merge it directly from you branch into the main |app| source tree. You should also keep an eye on the |app|
+`development forum `. Before making major changes, you should
+discuss them in the forum or contact Kovid directly (his email address is all over the source code).
Windows development environment
---------------------------------
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 4b527e169c..733adb65ee 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -131,7 +131,7 @@ Follow these steps to find the problem:
* Make sure that you are connecting only a single device to your computer at a time. Do not have another |app| supported device like an iPhone/iPad etc. at the same time.
* If you are connecting an Apple iDevice (iPad, iPod Touch, iPhone), use the 'Connect to iTunes' method in the 'Getting started' instructions in `Calibre + Apple iDevices: Start here `_.
* Make sure you are running the latest version of |app|. The latest version can always be downloaded from `the calibre website `_.
- * Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is.
+ * Ensure your operating system is seeing the device. That is, the device should be mounted as a disk, that you can access using Windows explorer or whatever the file management program on your computer is. On Windows your device **must have been assigned a drive letter**, like K:.
* In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled, the plugin icon next to it should be green when it is enabled.
* If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `the calibre bug tracker `_.