~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to lib/searchengines.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Miro - an RSS based video player application
 
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 
17
#
 
18
# In addition, as a special exception, the copyright holders give
 
19
# permission to link the code of portions of this program with the OpenSSL
 
20
# library.
 
21
#
 
22
# You must obey the GNU General Public License in all respects for all of
 
23
# the code used other than OpenSSL. If you modify file(s) with this
 
24
# exception, you may extend this exception to your version of the file(s),
 
25
# but you are not obligated to do so. If you do not wish to do so, delete
 
26
# this exception statement from your version. If you delete this exception
 
27
# statement from all source files in the program, then also delete it here.
 
28
 
 
29
"""``miro.searchengines`` -- This module holds
 
30
:class:`SearchEngineInfo` and related helper functions.
 
31
"""
 
32
 
 
33
from miro.util import check_u, returns_unicode
 
34
from miro.xhtmltools import urlencode
 
35
from xml.dom.minidom import parse
 
36
from miro.plat import resources
 
37
from miro.plat.utils import unicode_to_filename
 
38
import os
 
39
from miro import app
 
40
from miro import config
 
41
from miro import prefs
 
42
import logging
 
43
from miro.gtcache import gettext as _
 
44
 
 
45
class IntentionalCrash(Exception):
 
46
    pass
 
47
 
 
48
class SearchEngineInfo:
 
49
    """Defines a search engine in Miro.
 
50
 
 
51
    .. note::
 
52
 
 
53
       Don't instantiate this yourself---search engines are defined by
 
54
       ``.xml`` files in the ``resources/searchengines/`` directory.
 
55
    """
 
56
    def __init__(self, name, title, url, sort_order=0, filename=None):
 
57
        check_u(name)
 
58
        check_u(title)
 
59
        check_u(url)
 
60
        self.name = name
 
61
        self.title = title
 
62
        self.url = url
 
63
        self.sort_order = sort_order
 
64
        if filename is not None:
 
65
            self.filename = os.path.normcase(filename)
 
66
            # used for changing icon location on themed searches
 
67
        else:
 
68
            self.filename = None
 
69
 
 
70
    def get_request_url(self, query, filterAdultContents, limit):
 
71
        """Returns the request url expanding the query, filter adult content,
 
72
        and results limit place holders.
 
73
        """
 
74
        request_url = self.url.replace(u"%s", urlencode(query))
 
75
        request_url = request_url.replace(u"%a", 
 
76
                                          unicode(int(not filterAdultContents)))
 
77
        request_url = request_url.replace(u"%l", unicode(int(limit)))
 
78
 
 
79
        return request_url
 
80
 
 
81
    def __repr__(self):
 
82
        return "<SearchEngineInfo %s %s>" % (self.name, self.title)
 
83
 
 
84
_engines = []
 
85
 
 
86
def _delete_engines():
 
87
    global _engines
 
88
    _engines = []
 
89
 
 
90
def _search_for_search_engines(dir_):
 
91
    """Returns a dict of search engine -> search engine xml file for
 
92
    all search engines in the specified directory ``dir_``.
 
93
    """
 
94
    engines = {}
 
95
    try:
 
96
        for f in os.listdir(dir_):
 
97
            if f.endswith(".xml"):
 
98
                engines[os.path.normcase(f)] = os.path.normcase(
 
99
                    os.path.join(dir_, f))
 
100
    except OSError:
 
101
        pass
 
102
    return engines
 
103
 
 
104
def warn(filename, message):
 
105
    logging.warn("Error parsing searchengine: %s: %s", filename, message)
 
106
 
 
107
def _load_search_engine(filename):
 
108
    try:
 
109
        dom = parse(filename)
 
110
        id_ = displayname = url = sort = None
 
111
        root = dom.documentElement
 
112
        for child in root.childNodes:
 
113
            if child.nodeType == child.ELEMENT_NODE:
 
114
                tag = child.tagName
 
115
                text = child.childNodes[0].data
 
116
                if tag == "id":
 
117
                    if id_ != None:
 
118
                        warn(filename, "Duplicated id tag")
 
119
                        return
 
120
                    id_ = text
 
121
                elif tag == "displayname":
 
122
                    if displayname != None:
 
123
                        warn(filename, "Duplicated displayname tag")
 
124
                        return
 
125
                    displayname = text
 
126
                elif tag == "url":
 
127
                    if url != None:
 
128
                        warn(filename, "Duplicated url tag")
 
129
                        return
 
130
                    url = text
 
131
                elif tag == "sort":
 
132
                    if sort != None:
 
133
                        warn(filename, "Duplicated sort tag")
 
134
                        return
 
135
                    sort = float(text)
 
136
                else:
 
137
                    warn(filename, "Unrecognized tag %s" % tag)
 
138
                    return
 
139
        dom.unlink()
 
140
        if id_ == None:
 
141
            warn(filename, "Missing id tag")
 
142
            return
 
143
        if displayname == None:
 
144
            warn(filename, "Missing displayname tag")
 
145
            return
 
146
        if url == None:
 
147
            warn(filename, "Missing url tag")
 
148
            return
 
149
        if sort == None:
 
150
            sort = 0
 
151
 
 
152
        _engines.append(SearchEngineInfo(id_, displayname, url,
 
153
                                         sort_order=sort,
 
154
                                         filename=filename))
 
155
 
 
156
    except (SystemExit, KeyboardInterrupt):
 
157
        raise
 
158
    except:
 
159
        warn(filename, "Exception parsing file")
 
160
 
 
161
def create_engines():
 
162
    """Creates all the search engines specified in the
 
163
    ``resources/searchengines/`` directory and the theme searchengines
 
164
    directory.  After doing that, it adds an additional "Search All"
 
165
    engine.
 
166
    """
 
167
    global _engines
 
168
    _delete_engines()
 
169
    engines = _search_for_search_engines(resources.path("searchengines"))
 
170
    engines_dir = os.path.join(
 
171
        config.get(prefs.SUPPORT_DIRECTORY), "searchengines")
 
172
    engines.update(_search_for_search_engines(engines_dir))
 
173
    if config.get(prefs.THEME_NAME):
 
174
        theme_engines_dir = resources.theme_path(config.get(prefs.THEME_NAME),
 
175
                                                 'searchengines')
 
176
        engines.update(_search_for_search_engines(theme_engines_dir))
 
177
    for fn in engines.itervalues():
 
178
        _load_search_engine(fn)
 
179
 
 
180
    _engines.append(SearchEngineInfo(u"all", _("Search All"), u"", -1))
 
181
    _engines.sort(lambda a, b: cmp((a.sort_order, a.name, a.title), 
 
182
                                   (b.sort_order, b.name, b.title)))
 
183
 
 
184
    # SEARCH_ORDERING is a comma-separated list of search engine names to
 
185
    # include.  An * as the last engine includes the rest of the engines.
 
186
    if config.get(prefs.SEARCH_ORDERING):
 
187
        search_names = config.get(prefs.SEARCH_ORDERING).split(',')
 
188
        new_engines = []
 
189
        if '*' in search_names and '*' in search_names[:-1]:
 
190
            raise RuntimeError('wildcard search ordering must be at the end')
 
191
        for name in search_names:
 
192
            if name != '*':
 
193
                engine = get_engine_for_name(name)
 
194
                if not engine:
 
195
                    warn(__file__, 'Invalid search name: %r' % name)
 
196
                else:
 
197
                    new_engines.append(engine)
 
198
                    _engines.remove(engine)
 
199
            else:
 
200
                new_engines.extend(_engines)
 
201
        _engines = new_engines
 
202
 
 
203
def get_request_urls(engine_name, query, filter_adult_contents=True, limit=50):
 
204
    """Get a list of RSS feed URLs for a search.
 
205
 
 
206
    Usually this will return a single URL, but in the case of the "all" engine
 
207
    it will return multiple URLs.
 
208
 
 
209
    :param engine_name: which engine to use, or "all" for all engines
 
210
    :param query: search query
 
211
    :param filter_adult_contents: Should we include "adult" results
 
212
    :param limit: Limit the results to this number (per engine returned)
 
213
 
 
214
    There are two "magic" queries:
 
215
 
 
216
    * ``LET'S TEST DTV'S CRASH REPORTER TODAY`` which rases a
 
217
      NameError thus allowing us to test the crash reporter
 
218
 
 
219
    * ``LET'S DEBUT DTV: DUMP DATABASE`` which causes Miro to dump the
 
220
       database to xml and place it in the Miro configuration
 
221
       directory
 
222
    """
 
223
    if query == "LET'S TEST DTV'S CRASH REPORTER TODAY":
 
224
        raise IntentionalCrash("intentional error here")
 
225
 
 
226
    if query == "LET'S DEBUG DTV: DUMP DATABASE":
 
227
        app.db.dumpDatabase()
 
228
        return u""
 
229
 
 
230
    if engine_name == u'all':
 
231
        return [engine.get_request_url(query, filter_adult_contents, limit) \
 
232
                for engine in _engines if engine.name != u'all']
 
233
 
 
234
    for engine in _engines:
 
235
        if engine.name == engine_name:
 
236
            url = engine.get_request_url(query, filter_adult_contents, limit)
 
237
            return [url]
 
238
    return []
 
239
 
 
240
def get_search_engines():
 
241
    """Returns the list of :class:`SearchEngineInfo` instances.
 
242
    """
 
243
    return list(_engines)
 
244
 
 
245
def get_engine_for_name(name):
 
246
    """Returns the :class:`SearchEngineInfo` instance for the given
 
247
    id.  If no such search engine exists, returns ``None``.
 
248
    """
 
249
    for mem in get_search_engines():
 
250
        if mem.name == name:
 
251
            return mem
 
252
    return None
 
253
 
 
254
def get_last_engine():
 
255
    """Checks the preferences and returns the SearchEngine object of
 
256
    that name or ``None``.
 
257
    """
 
258
    e = config.get(prefs.LAST_SEARCH_ENGINE)
 
259
    engine = get_engine_for_name(e)
 
260
    if engine:
 
261
        return engine
 
262
    return get_search_engines()[0]
 
263
 
 
264
def set_last_engine(engine):
 
265
    """Takes a :class:`SearchEngineInfo` or search engine id and
 
266
    persists it to preferences.
 
267
    """
 
268
    if not isinstance(engine, basestring):
 
269
        engine = engine.name
 
270
    engine = str(engine)
 
271
    if not get_engine_for_name(engine):
 
272
        engine = str(get_search_engines()[0].name)
 
273
    config.set(prefs.LAST_SEARCH_ENGINE, engine)
 
274
 
 
275
def icon_path_for_engine(engine):
 
276
    engine_name = unicode_to_filename(engine.name)
 
277
    icon_path = resources.path('images/search_icon_%s.png' % engine_name)
 
278
    if config.get(prefs.THEME_NAME):
 
279
        logging.debug('engine %s filename: %s' % (engine.name, engine.filename))
 
280
        test_icon_path = resources.theme_path(config.get(prefs.THEME_NAME),
 
281
                                              'images/search_icon_%s.png' %
 
282
                                              engine_name)
 
283
        if os.path.exists(test_icon_path):
 
284
            # this search engine came from a theme; look up the icon in the
 
285
            # theme directory instead
 
286
            icon_path = test_icon_path
 
287
    return icon_path