1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
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.
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.
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
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
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.
29
"""``miro.searchengines`` -- This module holds
30
:class:`SearchEngineInfo` and related helper functions.
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
40
from miro import config
41
from miro import prefs
43
from miro.gtcache import gettext as _
45
class IntentionalCrash(Exception):
48
class SearchEngineInfo:
49
"""Defines a search engine in Miro.
53
Don't instantiate this yourself---search engines are defined by
54
``.xml`` files in the ``resources/searchengines/`` directory.
56
def __init__(self, name, title, url, sort_order=0, filename=None):
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
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.
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)))
82
return "<SearchEngineInfo %s %s>" % (self.name, self.title)
86
def _delete_engines():
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_``.
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))
104
def warn(filename, message):
105
logging.warn("Error parsing searchengine: %s: %s", filename, message)
107
def _load_search_engine(filename):
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:
115
text = child.childNodes[0].data
118
warn(filename, "Duplicated id tag")
121
elif tag == "displayname":
122
if displayname != None:
123
warn(filename, "Duplicated displayname tag")
128
warn(filename, "Duplicated url tag")
133
warn(filename, "Duplicated sort tag")
137
warn(filename, "Unrecognized tag %s" % tag)
141
warn(filename, "Missing id tag")
143
if displayname == None:
144
warn(filename, "Missing displayname tag")
147
warn(filename, "Missing url tag")
152
_engines.append(SearchEngineInfo(id_, displayname, url,
156
except (SystemExit, KeyboardInterrupt):
159
warn(filename, "Exception parsing file")
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"
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),
176
engines.update(_search_for_search_engines(theme_engines_dir))
177
for fn in engines.itervalues():
178
_load_search_engine(fn)
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)))
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(',')
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:
193
engine = get_engine_for_name(name)
195
warn(__file__, 'Invalid search name: %r' % name)
197
new_engines.append(engine)
198
_engines.remove(engine)
200
new_engines.extend(_engines)
201
_engines = new_engines
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.
206
Usually this will return a single URL, but in the case of the "all" engine
207
it will return multiple URLs.
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)
214
There are two "magic" queries:
216
* ``LET'S TEST DTV'S CRASH REPORTER TODAY`` which rases a
217
NameError thus allowing us to test the crash reporter
219
* ``LET'S DEBUT DTV: DUMP DATABASE`` which causes Miro to dump the
220
database to xml and place it in the Miro configuration
223
if query == "LET'S TEST DTV'S CRASH REPORTER TODAY":
224
raise IntentionalCrash("intentional error here")
226
if query == "LET'S DEBUG DTV: DUMP DATABASE":
227
app.db.dumpDatabase()
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']
234
for engine in _engines:
235
if engine.name == engine_name:
236
url = engine.get_request_url(query, filter_adult_contents, limit)
240
def get_search_engines():
241
"""Returns the list of :class:`SearchEngineInfo` instances.
243
return list(_engines)
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``.
249
for mem in get_search_engines():
254
def get_last_engine():
255
"""Checks the preferences and returns the SearchEngine object of
256
that name or ``None``.
258
e = config.get(prefs.LAST_SEARCH_ENGINE)
259
engine = get_engine_for_name(e)
262
return get_search_engines()[0]
264
def set_last_engine(engine):
265
"""Takes a :class:`SearchEngineInfo` or search engine id and
266
persists it to preferences.
268
if not isinstance(engine, basestring):
271
if not get_engine_for_name(engine):
272
engine = str(get_search_engines()[0].name)
273
config.set(prefs.LAST_SEARCH_ENGINE, engine)
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' %
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