mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sync to trunk.
This commit is contained in:
commit
086d08c9b6
BIN
resources/images/news/eluniversal_ve.png
Normal file
BIN
resources/images/news/eluniversal_ve.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 521 B |
@ -65,6 +65,9 @@ class TheAtlantic(BasicNewsRecipe):
|
||||
date = self.tag_to_string(byline) if byline else ''
|
||||
description = ''
|
||||
|
||||
self.log('\tFound article:', title)
|
||||
self.log('\t\t', url)
|
||||
|
||||
articles.append({
|
||||
'title':title,
|
||||
'date':date,
|
||||
|
52
resources/recipes/eluniversal_ve.recipe
Normal file
52
resources/recipes/eluniversal_ve.recipe
Normal file
@ -0,0 +1,52 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
www.eluniversal.com
|
||||
'''
|
||||
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class ElUniversal(BasicNewsRecipe):
|
||||
title = 'El Universal'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Noticias de Venezuela'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
encoding = 'cp1252'
|
||||
publisher = 'El Universal'
|
||||
category = 'news, Caracas, Venezuela, world'
|
||||
language = 'es'
|
||||
cover_url = strftime('http://static.eluniversal.com/%Y/%m/%d/portada.jpg')
|
||||
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
,'language' : language
|
||||
,'publisher' : publisher
|
||||
}
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'class':'Nota'})]
|
||||
remove_tags = [
|
||||
dict(name=['object','link','script','iframe'])
|
||||
,dict(name='div',attrs={'class':'Herramientas'})
|
||||
]
|
||||
|
||||
feeds = [
|
||||
(u'Ultimas Noticias', u'http://www.eluniversal.com/rss/avances.xml' )
|
||||
,(u'Economia' , u'http://www.eluniversal.com/rss/eco_avances.xml')
|
||||
,(u'Internacionales' , u'http://www.eluniversal.com/rss/int_avances.xml')
|
||||
,(u'Deportes' , u'http://www.eluniversal.com/rss/dep_avances.xml')
|
||||
,(u'Cultura' , u'http://www.eluniversal.com/rss/cul_avances.xml')
|
||||
,(u'Nacional y politica' , u'http://www.eluniversal.com/rss/pol_avances.xml')
|
||||
,(u'Ciencia y tecnologia', u'http://www.eluniversal.com/rss/cyt_avances.xml')
|
||||
,(u'Universo empresarial', u'http://www.eluniversal.com/rss/uni_avances.xml')
|
||||
,(u'Caracas' , u'http://www.eluniversal.com/rss/ccs_avances.xml')
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
rp,sep,rest = url.rpartition('/')
|
||||
return rp + sep + 'imp_' + rest
|
||||
|
31
resources/recipes/observer.recipe
Normal file
31
resources/recipes/observer.recipe
Normal file
@ -0,0 +1,31 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class NewsandObserver(BasicNewsRecipe):
|
||||
title = u'News and Observer'
|
||||
description = 'News from Raleigh, North Carolina'
|
||||
language = 'en'
|
||||
__author__ = 'Krittika Goyal'
|
||||
oldest_article = 5 #days
|
||||
max_articles_per_feed = 25
|
||||
|
||||
remove_stylesheets = True
|
||||
remove_tags_before = dict(name='h1', attrs={'id':'story_headline'})
|
||||
remove_tags_after = dict(name='div', attrs={'id':'story_text_remaining'})
|
||||
remove_tags = [
|
||||
dict(name='iframe'),
|
||||
dict(name='div', attrs={'id':['right-rail', 'story_tools']}),
|
||||
dict(name='ul', attrs={'class':'bold_tabs_nav'}),
|
||||
]
|
||||
|
||||
|
||||
feeds = [
|
||||
('Cover', 'http://www.newsobserver.com/100/index.rss'),
|
||||
('News', 'http://www.newsobserver.com/102/index.rss'),
|
||||
('Politics', 'http://www.newsobserver.com/105/index.rss'),
|
||||
('Business', 'http://www.newsobserver.com/104/index.rss'),
|
||||
('Sports', 'http://www.newsobserver.com/103/index.rss'),
|
||||
('College Sports', 'http://www.newsobserver.com/119/index.rss'),
|
||||
('Lifestyles', 'http://www.newsobserver.com/106/index.rss'),
|
||||
('Editorials', 'http://www.newsobserver.com/158/index.rss')]
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||
|
||||
class TheForce(BasicNewsRecipe):
|
||||
title = u'The Force'
|
||||
@ -21,11 +20,11 @@ class TheForce(BasicNewsRecipe):
|
||||
#dict(name='div', attrs={'class':['pt-box-title', 'pt-box-content', 'blog-entry-footer', 'item-list', 'article-sub-meta']}),
|
||||
#dict(name='div', attrs={'id':['block-td_search_160', 'block-cam_search_160']}),
|
||||
#dict(name='table', attrs={'cellspacing':'0'}),
|
||||
#dict(name='ul', attrs={'class':'articleTools'}),
|
||||
#dict(name='ul', attrs={'class':'articleTools'}),
|
||||
]
|
||||
|
||||
feeds = [
|
||||
('The Force',
|
||||
('The Force',
|
||||
'http://www.theforce.net/outnews/tfnrdf.xml'),
|
||||
]
|
||||
|
||||
|
12
resources/recipes/think_progress.recipe
Normal file
12
resources/recipes/think_progress.recipe
Normal file
@ -0,0 +1,12 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1263409732(BasicNewsRecipe):
|
||||
title = u'Think Progress'
|
||||
description = u'A compilation of progressive articles on social and economic justice, healthy communities, media accountability, global and domestic security.'
|
||||
__author__ = u'Xanthan Gum'
|
||||
language = 'en'
|
||||
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
|
||||
feeds = [(u'News Articles', u'http://thinkprogress.org/feed/')]
|
@ -48,7 +48,7 @@ class Plugin(object):
|
||||
#: the plugins are run in order of decreasing priority
|
||||
#: i.e. plugins with higher priority will be run first.
|
||||
#: The highest possible priority is ``sys.maxint``.
|
||||
#: Default pririty is 1.
|
||||
#: Default priority is 1.
|
||||
priority = 1
|
||||
|
||||
#: The earliest version of calibre this plugin requires
|
||||
@ -226,4 +226,75 @@ class MetadataWriterPlugin(Plugin):
|
||||
'''
|
||||
pass
|
||||
|
||||
class CatalogPlugin(Plugin):
|
||||
'''
|
||||
A plugin that implements a catalog generator.
|
||||
'''
|
||||
|
||||
#: Output file type for which this plugin should be run
|
||||
#: For example: 'epub' or 'xml'
|
||||
file_types = set([])
|
||||
|
||||
type = _('Catalog generator')
|
||||
|
||||
#: CLI parser options specific to this plugin, declared as namedtuple Option
|
||||
#:
|
||||
#: from collections import namedtuple
|
||||
#: Option = namedtuple('Option', 'option, default, dest, help')
|
||||
#: cli_options = [Option('--catalog-title',
|
||||
#: default = 'My Catalog',
|
||||
#: dest = 'catalog_title',
|
||||
#: help = (_('Title of generated catalog. \nDefault:') + " '" +
|
||||
#: '%default' + "'"))]
|
||||
|
||||
cli_options = []
|
||||
|
||||
def search_sort_db_as_dict(self, db, opts):
|
||||
if opts.search_text:
|
||||
db.search(opts.search_text)
|
||||
if opts.sort_by:
|
||||
# 2nd arg = ascending
|
||||
db.sort(opts.sort_by, True)
|
||||
|
||||
return db.get_data_as_dict()
|
||||
|
||||
def get_output_fields(self, opts):
|
||||
# Return a list of requested fields, with opts.sort_by first
|
||||
all_fields = set(
|
||||
['author_sort','authors','comments','cover','formats', 'id','isbn','pubdate','publisher','rating',
|
||||
'series_index','series','size','tags','timestamp',
|
||||
'title','uuid'])
|
||||
|
||||
fields = all_fields
|
||||
if opts.fields != 'all':
|
||||
# Make a list from opts.fields
|
||||
requested_fields = set(opts.fields.split(','))
|
||||
fields = list(all_fields & requested_fields)
|
||||
else:
|
||||
fields = list(all_fields)
|
||||
fields.sort()
|
||||
fields.insert(0,fields.pop(int(fields.index(opts.sort_by))))
|
||||
return fields
|
||||
|
||||
def run(self, path_to_output, opts, db):
|
||||
'''
|
||||
Run the plugin. Must be implemented in subclasses.
|
||||
It should generate the catalog in the format specified
|
||||
in file_types, returning the absolute path to the
|
||||
generated catalog file. If an error is encountered
|
||||
it should raise an Exception and return None. The default
|
||||
implementation simply returns None.
|
||||
|
||||
The generated catalog file should be created with the
|
||||
:meth:`temporary_file` method.
|
||||
|
||||
:param path_to_output: Absolute path to the generated catalog file.
|
||||
:param opts: A dictionary of keyword arguments
|
||||
:param db: A LibraryDatabase2 object
|
||||
|
||||
:return: None
|
||||
|
||||
'''
|
||||
# Default implementation does nothing
|
||||
raise NotImplementedError('CatalogPlugin.generate_catalog() default '
|
||||
'method, should be overridden in subclass')
|
||||
|
@ -421,7 +421,8 @@ from calibre.devices.binatone.driver import README
|
||||
from calibre.devices.hanvon.driver import N516
|
||||
|
||||
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon
|
||||
plugins = [HTML2ZIP, PML2PMLZ, GoogleBooks, ISBNDB, Amazon]
|
||||
from calibre.library.catalog import CSV_XML
|
||||
plugins = [HTML2ZIP, PML2PMLZ, GoogleBooks, ISBNDB, Amazon, CSV_XML]
|
||||
plugins += [
|
||||
ComicInput,
|
||||
EPUBInput,
|
||||
|
@ -5,8 +5,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import os, shutil, traceback, functools, sys, re
|
||||
from contextlib import closing
|
||||
|
||||
from calibre.customize import Plugin, FileTypePlugin, MetadataReaderPlugin, \
|
||||
MetadataWriterPlugin
|
||||
from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \
|
||||
MetadataReaderPlugin, MetadataWriterPlugin
|
||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||
from calibre.customize.builtins import plugins as builtin_plugins
|
||||
@ -300,6 +300,7 @@ def find_plugin(name):
|
||||
if plugin.name == name:
|
||||
return plugin
|
||||
|
||||
|
||||
def input_format_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, InputFormatPlugin):
|
||||
@ -328,6 +329,7 @@ def available_input_formats():
|
||||
formats.add('zip'), formats.add('rar')
|
||||
return formats
|
||||
|
||||
|
||||
def output_format_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, OutputFormatPlugin):
|
||||
@ -347,6 +349,27 @@ def available_output_formats():
|
||||
formats.add(plugin.file_type)
|
||||
return formats
|
||||
|
||||
|
||||
def catalog_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, CatalogPlugin):
|
||||
yield plugin
|
||||
|
||||
def available_catalog_formats():
|
||||
formats = set([])
|
||||
for plugin in catalog_plugins():
|
||||
if not is_disabled(plugin):
|
||||
for format in plugin.file_types:
|
||||
formats.add(format)
|
||||
return formats
|
||||
|
||||
def plugin_for_catalog_format(fmt):
|
||||
for plugin in catalog_plugins():
|
||||
if fmt.lower() in plugin.file_types:
|
||||
return plugin
|
||||
else:
|
||||
return None
|
||||
|
||||
def device_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, DevicePlugin):
|
||||
|
@ -881,7 +881,10 @@ class Text(LRFStream):
|
||||
open_containers.append(c)
|
||||
|
||||
if len(open_containers) > 0:
|
||||
raise LRFParseError('Malformed text stream %s'%([i.name for i in open_containers if isinstance(i, Text.TextTag)],))
|
||||
if len(open_containers) == 1:
|
||||
s += u'</%s>'%(open_containers[0].name,)
|
||||
else:
|
||||
raise LRFParseError('Malformed text stream %s'%([i.name for i in open_containers if isinstance(i, Text.TextTag)],))
|
||||
return s
|
||||
|
||||
def to_html(self):
|
||||
|
@ -7,34 +7,47 @@ __docformat__ = 'restructuredtext en'
|
||||
'''
|
||||
import textwrap, os
|
||||
|
||||
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl
|
||||
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl, QTimer, Qt
|
||||
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon, QDesktopServices
|
||||
|
||||
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
|
||||
from calibre.gui2 import dynamic
|
||||
from calibre import fit_image
|
||||
|
||||
class BookInfo(QDialog, Ui_BookInfo):
|
||||
|
||||
|
||||
def __init__(self, parent, view, row):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_BookInfo.__init__(self)
|
||||
self.setupUi(self)
|
||||
self.cover_pixmap = None
|
||||
desktop = QCoreApplication.instance().desktop()
|
||||
screen_height = desktop.availableGeometry().height() - 100
|
||||
self.resize(self.size().width(), screen_height)
|
||||
|
||||
|
||||
|
||||
|
||||
self.view = view
|
||||
self.current_row = None
|
||||
self.fit_cover.setChecked(dynamic.get('book_info_dialog_fit_cover',
|
||||
False))
|
||||
self.refresh(row)
|
||||
self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave)
|
||||
self.connect(self.next_button, SIGNAL('clicked()'), self.next)
|
||||
self.connect(self.previous_button, SIGNAL('clicked()'), self.previous)
|
||||
self.connect(self.text, SIGNAL('linkActivated(QString)'), self.open_book_path)
|
||||
|
||||
self.fit_cover.stateChanged.connect(self.toggle_cover_fit)
|
||||
self.cover.resizeEvent = self.cover_view_resized
|
||||
|
||||
def toggle_cover_fit(self, state):
|
||||
dynamic.set('book_info_dialog_fit_cover', self.fit_cover.isChecked())
|
||||
self.resize_cover()
|
||||
|
||||
def cover_view_resized(self, event):
|
||||
QTimer.singleShot(1, self.resize_cover)
|
||||
def slave(self, current, previous):
|
||||
row = current.row()
|
||||
self.refresh(row)
|
||||
|
||||
|
||||
def open_book_path(self, path):
|
||||
if os.sep in unicode(path):
|
||||
QDesktopServices.openUrl(QUrl('file:'+path))
|
||||
@ -43,41 +56,53 @@ class BookInfo(QDialog, Ui_BookInfo):
|
||||
path = self.view.model().db.format_abspath(self.current_row, format)
|
||||
if path is not None:
|
||||
QDesktopServices.openUrl(QUrl('file:'+path))
|
||||
|
||||
|
||||
|
||||
|
||||
def next(self):
|
||||
row = self.view.currentIndex().row()
|
||||
ni = self.view.model().index(row+1, 0)
|
||||
if ni.isValid():
|
||||
self.view.setCurrentIndex(ni)
|
||||
|
||||
|
||||
def previous(self):
|
||||
row = self.view.currentIndex().row()
|
||||
ni = self.view.model().index(row-1, 0)
|
||||
if ni.isValid():
|
||||
self.view.setCurrentIndex(ni)
|
||||
|
||||
|
||||
def resize_cover(self):
|
||||
if self.cover_pixmap is None:
|
||||
return
|
||||
self.setWindowIcon(QIcon(self.cover_pixmap))
|
||||
self.scene = QGraphicsScene()
|
||||
pixmap = self.cover_pixmap
|
||||
if self.fit_cover.isChecked():
|
||||
scaled, new_width, new_height = fit_image(pixmap.width(),
|
||||
pixmap.height(), self.cover.size().width()-10,
|
||||
self.cover.size().height()-10)
|
||||
if scaled:
|
||||
pixmap = pixmap.scaled(new_width, new_height,
|
||||
Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
self.scene.addPixmap(pixmap)
|
||||
self.cover.setScene(self.scene)
|
||||
|
||||
def refresh(self, row):
|
||||
if isinstance(row, QModelIndex):
|
||||
row = row.row()
|
||||
if row == self.current_row:
|
||||
return
|
||||
self.previous_button.setEnabled(False if row == 0 else True)
|
||||
self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
|
||||
self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
|
||||
self.current_row = row
|
||||
info = self.view.model().get_book_info(row)
|
||||
self.setWindowTitle(info[_('Title')])
|
||||
self.title.setText('<b>'+info.pop(_('Title')))
|
||||
self.comments.setText(info.pop(_('Comments'), ''))
|
||||
|
||||
|
||||
cdata = info.pop('cover', '')
|
||||
pixmap = QPixmap.fromImage(cdata)
|
||||
self.setWindowIcon(QIcon(pixmap))
|
||||
|
||||
self.scene = QGraphicsScene()
|
||||
self.scene.addPixmap(pixmap)
|
||||
self.cover.setScene(self.scene)
|
||||
|
||||
self.cover_pixmap = QPixmap.fromImage(cdata)
|
||||
self.resize_cover()
|
||||
|
||||
rows = u''
|
||||
self.text.setText('')
|
||||
self.data = info
|
||||
@ -94,4 +119,4 @@ class BookInfo(QDialog, Ui_BookInfo):
|
||||
txt = info[key]
|
||||
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
||||
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
||||
self.text.setText(u'<table>'+rows+'</table>')
|
||||
self.text.setText(u'<table>'+rows+'</table>')
|
||||
|
@ -1,7 +1,8 @@
|
||||
<ui version="4.0" >
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>BookInfo</class>
|
||||
<widget class="QDialog" name="BookInfo" >
|
||||
<property name="geometry" >
|
||||
<widget class="QDialog" name="BookInfo">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
@ -9,70 +10,77 @@
|
||||
<height>783</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle" >
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout" >
|
||||
<item row="0" column="0" colspan="2" >
|
||||
<widget class="QLabel" name="title" >
|
||||
<property name="text" >
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="title">
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
<property name="alignment" >
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" >
|
||||
<widget class="QGraphicsView" name="cover" />
|
||||
<item row="1" column="0">
|
||||
<widget class="QGraphicsView" name="cover"/>
|
||||
</item>
|
||||
<item row="1" column="1" >
|
||||
<layout class="QVBoxLayout" name="verticalLayout" >
|
||||
<item row="1" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="text" >
|
||||
<property name="text" >
|
||||
<widget class="QLabel" name="text">
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
<property name="alignment" >
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap" >
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox" >
|
||||
<property name="title" >
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Comments</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" >
|
||||
<item row="0" column="0" >
|
||||
<widget class="QTextBrowser" name="comments" />
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTextBrowser" name="comments"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout" >
|
||||
<widget class="QCheckBox" name="fit_cover">
|
||||
<property name="text">
|
||||
<string>Fit &cover to view</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="previous_button" >
|
||||
<property name="text" >
|
||||
<widget class="QPushButton" name="previous_button">
|
||||
<property name="text">
|
||||
<string>&Previous</string>
|
||||
</property>
|
||||
<property name="icon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/previous.svg</normaloff>:/images/previous.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="next_button" >
|
||||
<property name="text" >
|
||||
<widget class="QPushButton" name="next_button">
|
||||
<property name="text">
|
||||
<string>&Next</string>
|
||||
</property>
|
||||
<property name="icon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/next.svg</normaloff>:/images/next.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
@ -84,7 +92,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc" />
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -148,7 +148,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
bad_perms.append(_file)
|
||||
continue
|
||||
|
||||
_file = run_plugins_on_import(_file)
|
||||
nfile = run_plugins_on_import(_file)
|
||||
if nfile is not None:
|
||||
_file = nfile
|
||||
size = os.stat(_file).st_size
|
||||
ext = os.path.splitext(_file)[1].lower().replace('.', '')
|
||||
for row in range(self.formats.count()):
|
||||
|
209
src/calibre/library/catalog.py
Normal file
209
src/calibre/library/catalog.py
Normal file
@ -0,0 +1,209 @@
|
||||
import os
|
||||
|
||||
from calibre.customize import CatalogPlugin
|
||||
|
||||
class CSV_XML(CatalogPlugin):
|
||||
'CSV/XML catalog generator'
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
Option = namedtuple('Option', 'option, default, dest, help')
|
||||
|
||||
name = 'Catalog_CSV_XML'
|
||||
description = 'CSV/XML catalog generator'
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Greg Riker'
|
||||
version = (1, 0, 0)
|
||||
file_types = set(['csv','xml'])
|
||||
|
||||
cli_options = [
|
||||
Option('--fields',
|
||||
default = 'all',
|
||||
dest = 'fields',
|
||||
help = _('The fields to output when cataloging books in the '
|
||||
'database. Should be a comma-separated list of fields.\n'
|
||||
'Available fields: all, author_sort, authors, comments, '
|
||||
'cover, formats, id, isbn, pubdate, publisher, rating, '
|
||||
'series_index, series, size, tags, timestamp, title, uuid.\n'
|
||||
"Default: '%default'\n"
|
||||
"Applies to: CSV, XML output formats")),
|
||||
|
||||
Option('--sort-by',
|
||||
default = 'id',
|
||||
dest = 'sort_by',
|
||||
help = _('Output field to sort on.\n'
|
||||
'Available fields: author_sort, id, rating, size, timestamp, title.\n'
|
||||
"Default: '%default'\n"
|
||||
"Applies to: CSV, XML output formats"))]
|
||||
|
||||
def run(self, path_to_output, opts, db):
|
||||
from calibre.utils.logging import Log
|
||||
|
||||
log = Log()
|
||||
self.fmt = path_to_output[path_to_output.rfind('.') + 1:]
|
||||
if opts.verbose:
|
||||
log("%s:run" % self.name)
|
||||
log(" path_to_output: %s" % path_to_output)
|
||||
log(" Output format: %s" % self.fmt)
|
||||
|
||||
# Display opts
|
||||
opts_dict = vars(opts)
|
||||
keys = opts_dict.keys()
|
||||
keys.sort()
|
||||
log(" opts:")
|
||||
for key in keys:
|
||||
log(" %s: %s" % (key, opts_dict[key]))
|
||||
|
||||
# Get the sorted, filtered database as a dictionary
|
||||
data = self.search_sort_db_as_dict(db, opts)
|
||||
|
||||
if not len(data):
|
||||
log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
|
||||
raise SystemExit(1)
|
||||
|
||||
# Get the requested output fields as a list
|
||||
fields = self.get_output_fields(opts)
|
||||
|
||||
if self.fmt == 'csv':
|
||||
outfile = open(path_to_output, 'w')
|
||||
|
||||
# Output the field headers
|
||||
outfile.write('%s\n' % ','.join(fields))
|
||||
|
||||
# Output the entry fields
|
||||
for entry in data:
|
||||
outstr = ''
|
||||
for (x, field) in enumerate(fields):
|
||||
item = entry[field]
|
||||
if field in ['authors','tags','formats']:
|
||||
item = ', '.join(item)
|
||||
if x < len(fields) - 1:
|
||||
if item is not None:
|
||||
outstr += '"%s",' % str(item).replace('"','""')
|
||||
else:
|
||||
outstr += '"",'
|
||||
else:
|
||||
if item is not None:
|
||||
outstr += '"%s"\n' % str(item).replace('"','""')
|
||||
else:
|
||||
outstr += '""\n'
|
||||
outfile.write(outstr)
|
||||
outfile.close()
|
||||
|
||||
elif self.fmt == 'xml':
|
||||
from lxml import etree
|
||||
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
|
||||
PY_NAMESPACE = "http://genshi.edgewall.org/"
|
||||
PY = "{%s}" % PY_NAMESPACE
|
||||
NSMAP = {'py' : PY_NAMESPACE}
|
||||
root = etree.Element('calibredb', nsmap=NSMAP)
|
||||
py_for = etree.SubElement(root, PY + 'for', each="record in data")
|
||||
record = etree.SubElement(py_for, 'record')
|
||||
|
||||
if 'id' in fields:
|
||||
record_child = etree.SubElement(record, 'id')
|
||||
record_child.set(PY + "if", "record['id']")
|
||||
record_child.text = "${record['id']}"
|
||||
|
||||
if 'uuid' in fields:
|
||||
record_child = etree.SubElement(record, 'uuid')
|
||||
record_child.set(PY + "if", "record['uuid']")
|
||||
record_child.text = "${record['uuid']}"
|
||||
|
||||
if 'title' in fields:
|
||||
record_child = etree.SubElement(record, 'title')
|
||||
record_child.set(PY + "if", "record['title']")
|
||||
record_child.text = "${record['title']}"
|
||||
|
||||
if 'authors' in fields:
|
||||
record_child = etree.SubElement(record, 'authors', sort="${record['author_sort']}")
|
||||
record_subchild = etree.SubElement(record_child, PY + 'for', each="author in record['authors']")
|
||||
record_subsubchild = etree.SubElement(record_subchild, 'author')
|
||||
record_subsubchild.text = '$author'
|
||||
|
||||
if 'publisher' in fields:
|
||||
record_child = etree.SubElement(record, 'publisher')
|
||||
record_child.set(PY + "if", "record['publisher']")
|
||||
record_child.text = "${record['publisher']}"
|
||||
|
||||
if 'rating' in fields:
|
||||
record_child = etree.SubElement(record, 'rating')
|
||||
record_child.set(PY + "if", "record['rating']")
|
||||
record_child.text = "${record['rating']}"
|
||||
|
||||
if 'date' in fields:
|
||||
record_child = etree.SubElement(record, 'date')
|
||||
record_child.set(PY + "if", "record['date']")
|
||||
record_child.text = "${record['date']}"
|
||||
|
||||
if 'pubdate' in fields:
|
||||
record_child = etree.SubElement(record, 'pubdate')
|
||||
record_child.set(PY + "if", "record['pubdate']")
|
||||
record_child.text = "${record['pubdate']}"
|
||||
|
||||
if 'size' in fields:
|
||||
record_child = etree.SubElement(record, 'size')
|
||||
record_child.set(PY + "if", "record['size']")
|
||||
record_child.text = "${record['size']}"
|
||||
|
||||
if 'tags' in fields:
|
||||
# <tags py:if="record['tags']">
|
||||
# <py:for each="tag in record['tags']">
|
||||
# <tag>$tag</tag>
|
||||
# </py:for>
|
||||
# </tags>
|
||||
record_child = etree.SubElement(record, 'tags')
|
||||
record_child.set(PY + "if", "record['tags']")
|
||||
record_subchild = etree.SubElement(record_child, PY + 'for', each="tag in record['tags']")
|
||||
record_subsubchild = etree.SubElement(record_subchild, 'tag')
|
||||
record_subsubchild.text = '$tag'
|
||||
|
||||
if 'comments' in fields:
|
||||
record_child = etree.SubElement(record, 'comments')
|
||||
record_child.set(PY + "if", "record['comments']")
|
||||
record_child.text = "${record['comments']}"
|
||||
|
||||
if 'series' in fields:
|
||||
# <series py:if="record['series']" index="${record['series_index']}">
|
||||
# ${record['series']}
|
||||
# </series>
|
||||
record_child = etree.SubElement(record, 'series')
|
||||
record_child.set(PY + "if", "record['series']")
|
||||
record_child.set('index', "${record['series_index']}")
|
||||
record_child.text = "${record['series']}"
|
||||
|
||||
if 'isbn' in fields:
|
||||
record_child = etree.SubElement(record, 'isbn')
|
||||
record_child.set(PY + "if", "record['isbn']")
|
||||
record_child.text = "${record['isbn']}"
|
||||
|
||||
if 'cover' in fields:
|
||||
# <cover py:if="record['cover']">
|
||||
# ${record['cover'].replace(os.sep, '/')}
|
||||
# </cover>
|
||||
record_child = etree.SubElement(record, 'cover')
|
||||
record_child.set(PY + "if", "record['cover']")
|
||||
record_child.text = "${record['cover']}"
|
||||
|
||||
if 'formats' in fields:
|
||||
# <formats py:if="record['formats']">
|
||||
# <py:for each="path in record['formats']">
|
||||
# <format>${path.replace(os.sep, '/')}</format>
|
||||
# </py:for>
|
||||
# </formats>
|
||||
record_child = etree.SubElement(record, 'formats')
|
||||
record_child.set(PY + "if", "record['formats']")
|
||||
record_subchild = etree.SubElement(record_child, PY + 'for', each="path in record['formats']")
|
||||
record_subsubchild = etree.SubElement(record_subchild, 'format')
|
||||
record_subsubchild.text = "${path.replace(os.sep, '/')}"
|
||||
|
||||
outfile = open(path_to_output, 'w')
|
||||
template = MarkupTemplate(etree.tostring(root, xml_declaration=True,
|
||||
encoding="UTF-8", pretty_print=True))
|
||||
outfile.write(template.generate(data=data, os=os).render('xml'))
|
||||
outfile.close()
|
||||
|
||||
return None
|
||||
|
@ -583,8 +583,120 @@ def command_export(args, dbpath):
|
||||
do_export(get_db(dbpath, opts), ids, dir, opts)
|
||||
return 0
|
||||
|
||||
|
||||
# GR additions
|
||||
|
||||
def catalog_option_parser(args):
|
||||
from calibre.customize.ui import available_catalog_formats, plugin_for_catalog_format
|
||||
from calibre.utils.logging import Log
|
||||
|
||||
def add_plugin_parser_options(fmt, parser, log):
|
||||
|
||||
# Fetch the extension-specific CLI options from the plugin
|
||||
plugin = plugin_for_catalog_format(fmt)
|
||||
for option in plugin.cli_options:
|
||||
parser.add_option(option.option,
|
||||
default=option.default,
|
||||
dest=option.dest,
|
||||
help=option.help)
|
||||
|
||||
return plugin
|
||||
|
||||
def print_help(parser, log):
|
||||
help = parser.format_help().encode(preferred_encoding, 'replace')
|
||||
log(help)
|
||||
|
||||
def validate_command_line(parser, args, log):
|
||||
# calibredb catalog path/to/destination.[epub|csv|xml|...] [options]
|
||||
|
||||
# Validate form
|
||||
if not len(args) or args[0].startswith('-'):
|
||||
print_help(parser, log)
|
||||
log.error("\n\nYou must specify a catalog output file of the form 'path/to/destination.extension'\n"
|
||||
"To review options for an output format, type 'calibredb catalog <.extension> --help'\n"
|
||||
"For example, 'calibredb catalog .xml --help'\n")
|
||||
raise SystemExit(1)
|
||||
|
||||
# Validate plugin exists for specified output format
|
||||
output = os.path.abspath(args[0])
|
||||
file_extension = output[output.rfind('.') + 1:].lower()
|
||||
|
||||
if not file_extension in available_catalog_formats():
|
||||
print_help(parser, log)
|
||||
log.error("No catalog plugin available for extension '%s'.\n" % file_extension +
|
||||
"Catalog plugins available for %s\n" % ', '.join(available_catalog_formats()) )
|
||||
raise SystemExit(1)
|
||||
|
||||
return output, file_extension
|
||||
|
||||
# Entry point
|
||||
log = Log()
|
||||
parser = get_parser(_(
|
||||
'''
|
||||
%prog catalog /path/to/destination.(csv|epub|mobi|xml ...) [options]
|
||||
|
||||
Export a catalog in format specified by path/to/destination extension.
|
||||
Options control how entries are displayed in the generated catalog ouput.
|
||||
'''))
|
||||
|
||||
# Confirm that a plugin handler exists for specified output file extension
|
||||
# Will raise SystemExit(1) if no plugin matching file_extension
|
||||
output, fmt = validate_command_line(parser, args, log)
|
||||
|
||||
# Add options common to all catalog plugins
|
||||
parser.add_option('-s', '--search', default=None, dest='search_text',
|
||||
help=_("Filter the results by the search query. For the format of the search query, please see the search-related documentation in the User Manual.\n"+
|
||||
"Default: no filtering"))
|
||||
parser.add_option('-v','--verbose', default=False, action='store_true',
|
||||
dest='verbose',
|
||||
help=_('Show detailed output information. Useful for debugging'))
|
||||
|
||||
# Add options specific to fmt plugin
|
||||
plugin = add_plugin_parser_options(fmt, parser, log)
|
||||
|
||||
# Merge options from GUI Preferences
|
||||
'''
|
||||
from calibre.library.save_to_disk import config
|
||||
c = config()
|
||||
for pref in ['asciiize', 'update_metadata', 'write_opf', 'save_cover']:
|
||||
opt = c.get_option(pref)
|
||||
switch = '--dont-'+pref.replace('_', '-')
|
||||
parser.add_option(switch, default=True, action='store_false',
|
||||
help=opt.help+' '+_('Specifying this switch will turn '
|
||||
'this behavior off.'), dest=pref)
|
||||
|
||||
for pref in ['timefmt', 'template', 'formats']:
|
||||
opt = c.get_option(pref)
|
||||
switch = '--'+pref
|
||||
parser.add_option(switch, default=opt.default,
|
||||
help=opt.help, dest=pref)
|
||||
|
||||
for pref in ('replace_whitespace', 'to_lowercase'):
|
||||
opt = c.get_option(pref)
|
||||
switch = '--'+pref.replace('_', '-')
|
||||
parser.add_option(switch, default=False, action='store_true',
|
||||
help=opt.help)
|
||||
'''
|
||||
|
||||
return parser, plugin, log
|
||||
|
||||
def command_catalog(args, dbpath):
|
||||
parser, plugin, log = catalog_option_parser(args)
|
||||
opts, args = parser.parse_args(sys.argv[1:])
|
||||
if len(args) < 2:
|
||||
parser.print_help()
|
||||
print
|
||||
print >>sys.stderr, _('Error: You must specify a catalog output file')
|
||||
return 1
|
||||
if opts.verbose:
|
||||
log("library.cli:command_catalog dispatching to plugin %s" % plugin.name)
|
||||
plugin.run(args[1], opts, get_db(dbpath, opts))
|
||||
return 0
|
||||
|
||||
# end of GR additions
|
||||
|
||||
COMMANDS = ('list', 'add', 'remove', 'add_format', 'remove_format',
|
||||
'show_metadata', 'set_metadata', 'export')
|
||||
'show_metadata', 'set_metadata', 'export', 'catalog')
|
||||
|
||||
|
||||
def option_parser():
|
||||
|
@ -270,11 +270,13 @@ Why does |app| show only some of my fonts on OS X?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
There can be several causes for this:
|
||||
|
||||
* **Any windows version**: Try running it as Administrator (Right click on the icon ans select "Run as Administrator")
|
||||
* **Any windows version**: If this happens during an initial run of calibre, try deleting the folder you chose for your ebooks and restarting calibre.
|
||||
* If you get an error about a Python function terminating unexpectedly after upgrading calibre, first uninstall calibre, then delete the folders (if they exists)
|
||||
:file:`C:\\Program Files\\Calibre` and :file:`C:\\Program Files\\Calibre2`. Now re-install and you should be fine.
|
||||
* If you get an error in the welcome wizard on an initial run of calibre, try choosing a folder like :file:`C:\\library` as the calibre library (calibre sometimes
|
||||
has trouble with library locations if the path contains non-English characters, or only numbers, etc.)
|
||||
* Try running it as Administrator (Right click on the icon and select "Run as Administrator")
|
||||
* **Windows Vista**: If the folder :file:`C:\\Users\\Your User Name\\AppData\\Local\\VirtualStore\\Program Files\\calibre` exists, delete it. Uninstall |app|. Reboot. Re-install.
|
||||
* **Any windows version**: Search your computer for a folder named :file:`_ipython`. Delete it and try again.
|
||||
* **Any windows version**: Try disabling any antivirus program you have running and see if that fixes it. Also try diabling any firewall software that prevents connections to the local computer.
|
||||
* **Any windows version**: Try disabling any antivirus program you have running and see if that fixes it. Also try disabling any firewall software that prevents connections to the local computer.
|
||||
|
||||
If it still wont launch, start a command prompt (press the windows key and R; then type :command:`cmd.exe` in the Run dialog that appears). At the command prompt type the following command and press Enter::
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user