2
# vim:fileencoding=utf-8
3
from __future__ import (unicode_literals, division, absolute_import,
7
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
9
from collections import defaultdict, namedtuple
10
from operator import itemgetter
12
from PyQt4.Qt import (
13
QDialog, QFormLayout, QHBoxLayout, QLineEdit, QToolButton, QIcon,
14
QDialogButtonBox, Qt, QSpinBox, QCheckBox)
16
from lxml import etree
18
from calibre.gui2 import choose_files, error_dialog
19
from calibre.utils.icu import sort_key
21
Group = namedtuple('Group', 'title feeds')
23
def uniq(vals, kmap=lambda x:x):
24
''' Remove all duplicates from vals, while preserving order. kmap must be a
25
callable that returns a hashable value for every item in vals '''
27
lvals = (kmap(x) for x in vals)
30
return tuple(x for x, k in zip(vals, lvals) if k not in seen and not seen_add(k))
32
def import_opml(raw, preserve_groups=True):
33
root = etree.fromstring(raw)
34
groups = defaultdict(list)
35
ax = etree.XPath('ancestor::outline[@title or @text]')
36
for outline in root.xpath('//outline[@type="rss" and @xmlUrl]'):
37
url = outline.get('xmlUrl')
38
parent = outline.get('title', '') or url
39
title = parent if ('title' in outline.attrib and parent) else None
41
for ancestor in ax(outline):
42
if ancestor.get('type', None) != 'rss':
43
text = ancestor.get('title') or ancestor.get('text')
47
groups[parent].append((title, url))
49
for title in sorted(groups.iterkeys(), key=sort_key):
50
yield Group(title, uniq(groups[title], kmap=itemgetter(1)))
53
class ImportOPML(QDialog):
55
def __init__(self, parent=None):
56
QDialog.__init__(self, parent=parent)
57
self.l = l = QFormLayout(self)
59
self.setWindowTitle(_('Import OPML file'))
60
self.setWindowIcon(QIcon(I('opml.png')))
62
self.h = h = QHBoxLayout()
63
self.path = p = QLineEdit(self)
64
p.setMinimumWidth(300)
65
p.setPlaceholderText(_('Path to OPML file'))
67
self.cfb = b = QToolButton(self)
68
b.setIcon(QIcon(I('document_open.png')))
69
b.setToolTip(_('Browse for OPML file'))
70
b.clicked.connect(self.choose_file)
72
l.addRow(_('&OPML file:'), h)
73
l.labelForField(h).setBuddy(p)
74
b.setFocus(Qt.OtherFocusReason)
76
self._articles_per_feed = a = QSpinBox(self)
77
a.setMinimum(1), a.setMaximum(1000), a.setValue(100)
78
a.setToolTip(_('Maximum number of articles to download per RSS feed'))
79
l.addRow(_('&Maximum articles per feed:'), a)
81
self._oldest_article = o = QSpinBox(self)
82
o.setMinimum(1), o.setMaximum(3650), o.setValue(7)
83
o.setSuffix(_(' days'))
84
o.setToolTip(_('Articles in the RSS feeds older than this will be ignored'))
85
l.addRow(_('&Oldest article:'), o)
87
self.preserve_groups = g = QCheckBox(_('Preserve groups in the OPML file'))
88
g.setToolTip('<p>' + _(
89
'If enabled, every group of feeds in the OPML file will be converted into a single recipe. Otherwise every feed becomes its own recipe'))
93
self._replace_existing = r = QCheckBox(_('Replace existing recipes'))
94
r.setToolTip('<p>' + _(
95
'If enabled, any existing recipes with the same titles as entries in the OPML file will be replaced.'
96
' Otherwise, new entries with modified titles will be created'))
100
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
101
bb.accepted.connect(self.accept), bb.rejected.connect(self.reject)
107
def articles_per_feed(self):
108
return self._articles_per_feed.value()
111
def oldest_article(self):
112
return self._oldest_article.value()
115
def replace_existing(self):
116
return self._replace_existing.isChecked()
118
def choose_file(self):
119
opml_files = choose_files(
120
self, 'opml-select-dialog', _('Select OPML file'), filters=[(_('OPML files'), ['opml'])],
121
all_files=False, select_only_single_file=True)
123
self.path.setText(opml_files[0])
126
path = unicode(self.path.text())
128
return error_dialog(self, _('Path not specified'), _(
129
'You must specify the path to the OPML file to import'), show=True)
130
with open(path, 'rb') as f:
132
self.recipes = tuple(import_opml(raw, self.preserve_groups.isChecked()))
133
if len(self.recipes) == 0:
134
return error_dialog(self, _('No feeds found'), _(
135
'No importable RSS feeds found in the OPML file'), show=True)
139
if __name__ == '__main__':
141
for group in import_opml(open(sys.argv[-1], 'rb').read()):
143
for title, url in group.feeds:
144
print ('\t%s - %s' % (title, url))