~ubuntu-branches/debian/sid/calibre/sid

« back to all changes in this revision

Viewing changes to src/calibre/gui2/dialogs/opml.py

  • Committer: Package Import Robot
  • Author(s): Martin Pitt
  • Date: 2014-05-14 18:17:50 UTC
  • mto: This revision was merged to the branch mainline in revision 75.
  • Revision ID: package-import@ubuntu.com-20140514181750-efj1wymey2vb4cao
Tags: upstream-1.36.0+dfsg
ImportĀ upstreamĀ versionĀ 1.36.0+dfsg

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# vim:fileencoding=utf-8
 
3
from __future__ import (unicode_literals, division, absolute_import,
 
4
                        print_function)
 
5
 
 
6
__license__ = 'GPL v3'
 
7
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
 
8
 
 
9
from collections import defaultdict, namedtuple
 
10
from operator import itemgetter
 
11
 
 
12
from PyQt4.Qt import (
 
13
    QDialog, QFormLayout, QHBoxLayout, QLineEdit, QToolButton, QIcon,
 
14
    QDialogButtonBox, Qt, QSpinBox, QCheckBox)
 
15
 
 
16
from lxml import etree
 
17
 
 
18
from calibre.gui2 import choose_files, error_dialog
 
19
from calibre.utils.icu import sort_key
 
20
 
 
21
Group = namedtuple('Group', 'title feeds')
 
22
 
 
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 '''
 
26
    vals = vals or ()
 
27
    lvals = (kmap(x) for x in vals)
 
28
    seen = set()
 
29
    seen_add = seen.add
 
30
    return tuple(x for x, k in zip(vals, lvals) if k not in seen and not seen_add(k))
 
31
 
 
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
 
40
        if preserve_groups:
 
41
            for ancestor in ax(outline):
 
42
                if ancestor.get('type', None) != 'rss':
 
43
                    text = ancestor.get('title') or ancestor.get('text')
 
44
                    if text:
 
45
                        parent = text
 
46
                        break
 
47
        groups[parent].append((title, url))
 
48
 
 
49
    for title in sorted(groups.iterkeys(), key=sort_key):
 
50
        yield Group(title, uniq(groups[title], kmap=itemgetter(1)))
 
51
 
 
52
 
 
53
class ImportOPML(QDialog):
 
54
 
 
55
    def __init__(self, parent=None):
 
56
        QDialog.__init__(self, parent=parent)
 
57
        self.l = l = QFormLayout(self)
 
58
        self.setLayout(l)
 
59
        self.setWindowTitle(_('Import OPML file'))
 
60
        self.setWindowIcon(QIcon(I('opml.png')))
 
61
 
 
62
        self.h = h = QHBoxLayout()
 
63
        self.path = p = QLineEdit(self)
 
64
        p.setMinimumWidth(300)
 
65
        p.setPlaceholderText(_('Path to OPML file'))
 
66
        h.addWidget(p)
 
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)
 
71
        h.addWidget(b)
 
72
        l.addRow(_('&OPML file:'), h)
 
73
        l.labelForField(h).setBuddy(p)
 
74
        b.setFocus(Qt.OtherFocusReason)
 
75
 
 
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)
 
80
 
 
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)
 
86
 
 
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'))
 
90
        g.setChecked(True)
 
91
        l.addRow(g)
 
92
 
 
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'))
 
97
        r.setChecked(True)
 
98
        l.addRow(r)
 
99
 
 
100
        self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
 
101
        bb.accepted.connect(self.accept), bb.rejected.connect(self.reject)
 
102
        l.addRow(bb)
 
103
 
 
104
        self.recipes = ()
 
105
 
 
106
    @property
 
107
    def articles_per_feed(self):
 
108
        return self._articles_per_feed.value()
 
109
 
 
110
    @property
 
111
    def oldest_article(self):
 
112
        return self._oldest_article.value()
 
113
 
 
114
    @property
 
115
    def replace_existing(self):
 
116
        return self._replace_existing.isChecked()
 
117
 
 
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)
 
122
        if opml_files:
 
123
            self.path.setText(opml_files[0])
 
124
 
 
125
    def accept(self):
 
126
        path = unicode(self.path.text())
 
127
        if not path:
 
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:
 
131
            raw = f.read()
 
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)
 
136
 
 
137
        QDialog.accept(self)
 
138
 
 
139
if __name__ == '__main__':
 
140
    import sys
 
141
    for group in import_opml(open(sys.argv[-1], 'rb').read()):
 
142
        print (group.title)
 
143
        for title, url in group.feeds:
 
144
            print ('\t%s - %s' % (title, url))
 
145
        print ()