Merge lp:~cmulk/cardapio/duckduckgo-plugin into lp:cardapio

Proposed by cmulk
Status: Merged
Merge reported by: Thiago Teixeira
Merged at revision: not available
Proposed branch: lp:~cmulk/cardapio/duckduckgo-plugin
Merge into: lp:cardapio
Diff against target: 253 lines (+249/-0)
1 file modified
cardapio/src/plugins/duckduck.py (+249/-0)
To merge this branch: bzr merge lp:~cmulk/cardapio/duckduckgo-plugin
Reviewer Review Type Date Requested Status
Thiago Teixeira Approve
Review via email: mp+56062@code.launchpad.net

Description of the change

Added DuckDuckGo search plugin

To post a comment you must log in.
Revision history for this message
Thiago Teixeira (tvst) wrote :

Nice! I will review this and add it to the PPA tonight :)

Revision history for this message
Thiago Teixeira (tvst) wrote :

Looks good!

Regarding your comment about the result_limit variable: that variable can't be set in the constructor because sometimes it changes after the plugin has been loaded. For instance, when doing a keyword search, result_limit changes from 4 to 20 (those are default values).

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'cardapio/src/plugins/duckduck.py'
2--- cardapio/src/plugins/duckduck.py 1970-01-01 00:00:00 +0000
3+++ cardapio/src/plugins/duckduck.py 2011-04-03 00:40:56 +0000
4@@ -0,0 +1,249 @@
5+#
6+# Copyright (C) 2010 Cardapio Team (tvst@hotmail.com)
7+#
8+# This program is free software: you can redistribute it and/or modify
9+# it under the terms of the GNU General Public License as published by
10+# the Free Software Foundation, either version 3 of the License, or
11+# (at your option) any later version.
12+#
13+# This program is distributed in the hope that it will be useful,
14+# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+# GNU General Public License for more details.
17+#
18+# You should have received a copy of the GNU General Public License
19+# along with this program. If not, see <http://www.gnu.org/licenses/>.
20+#
21+
22+
23+## Duck Duck Go search plugin by Clifton Mulkey
24+## Adapted from the Google Search plugin
25+class CardapioPlugin(CardapioPluginInterface):
26+
27+ author = _('Cardapio Team')
28+ name = _('DuckDuckGo')
29+ description = _('Perform quick DuckDuckGo searches')
30+
31+ url = ''
32+ help_text = ''
33+ version = '1.0'
34+
35+ plugin_api_version = 1.40
36+
37+ search_delay_type = 'remote'
38+
39+ default_keyword = 'duck'
40+
41+ category_name = _('DuckDuckGo Results')
42+ category_icon = 'system-search'
43+ icon = 'system-search'
44+ category_tooltip = _('Results found with DuckDuckGo')
45+ hide_from_sidebar = True
46+
47+
48+ def __init__(self, cardapio_proxy, category):
49+
50+ self.c = cardapio_proxy
51+
52+ try:
53+ from gio import File, Cancellable
54+ from urllib2 import quote
55+ from simplejson import loads
56+ from locale import getdefaultlocale
57+ from glib import GError
58+
59+ except Exception, exception:
60+ self.c.write_to_log(self, 'Could not import certain modules', is_error = True)
61+ self.c.write_to_log(self, exception, is_error = True)
62+ self.loaded = False
63+ return
64+
65+ self.File = File
66+ self.Cancellable = Cancellable
67+ self.quote = quote
68+ self.loads = loads
69+ self.getdefaultlocale = getdefaultlocale
70+ self.GError = GError
71+
72+ self.query_url = r'http://www.duckduckgo.com/?q={0}&o=json'
73+
74+ self.search_controller = self.Cancellable()
75+
76+ self.action_command = "xdg-open 'http://duckduckgo.com/?q=%s'"
77+ self.action = {
78+ 'name' : _('Show additional results'),
79+ 'tooltip' : _('Show additional search results in your web browser'),
80+ 'icon name' : 'system-search',
81+ 'type' : 'callback',
82+ 'command' : self.more_results_action,
83+ 'context menu' : None,
84+ }
85+
86+
87+
88+ self.loaded = True
89+
90+
91+ def search(self, text, result_limit):
92+
93+ # TODO: I'm sure this is not the best way of doing remote procedure
94+ # calls, but I can't seem to find anything that is this easy to use and
95+ # compatible with gtk. Argh :(
96+
97+ # TODO: we should really check if there's an internet connection before
98+ # proceeding...
99+
100+ self.current_query = text
101+ text = self.quote(text)
102+
103+ # Is there a way to get the result_limit in the init method
104+ # so we don't have to assign it everytime search is called?
105+ self.result_limit = result_limit
106+
107+ query = self.query_url.format(text)
108+
109+ self.stream = self.File(query)
110+
111+ self.search_controller.reset()
112+ self.stream.load_contents_async(self.handle_search_result, cancellable = self.search_controller)
113+
114+
115+ def cancel(self):
116+
117+ if not self.search_controller.is_cancelled():
118+ self.search_controller.cancel()
119+
120+
121+ def handle_search_result(self, gdaemonfile = None, response = None):
122+ # This function parses the results from the query
123+ # The results returned from DDG are a little convoluted
124+ # so we have to check for many different types of results here
125+
126+ result_count = 0;
127+
128+ try:
129+ response = self.stream.load_contents_finish(response)[0]
130+
131+ except self.GError, e:
132+ # no need to worry if there's no response: maybe there's no internet
133+ # connection...
134+ self.c.handle_search_error(self, 'no response')
135+ return
136+
137+ raw_results = self.loads(response)
138+
139+ # print raw_results
140+ parsed_results = []
141+
142+ if 'Error' in raw_results:
143+ self.c.handle_search_error(self, raw_results['Error'])
144+ return
145+
146+ # check for an abstract section
147+ try:
148+ if raw_results['Abstract']:
149+ item = {
150+ 'name' : raw_results['Heading'],
151+ 'tooltip' : '(%s) %s' % (raw_results['AbstractSource'], raw_results['AbstractText']),
152+ 'icon name' : 'text-html',
153+ 'type' : 'xdg',
154+ 'command' : raw_results['AbstractURL'],
155+ 'context menu' : None,
156+ }
157+ parsed_results.append(item)
158+ result_count += 1
159+ except KeyError:
160+ pass
161+
162+ # check for a definition section
163+ try:
164+ if raw_results['Definition']:
165+ item = {
166+ 'name' : '%s (Definition)' % raw_results['Heading'],
167+ 'tooltip' : '(%s) %s' % (raw_results['DefinitionSource'], raw_results['Definition']),
168+ 'icon name' : 'text-html',
169+ 'type' : 'xdg',
170+ 'command' : raw_results['DefinitionURL'],
171+ 'context menu' : None,
172+ }
173+ parsed_results.append(item)
174+ result_count += 1
175+ except KeyError:
176+ pass
177+
178+ # check for a related topics section
179+ try:
180+ if raw_results['RelatedTopics']:
181+ for raw_result in raw_results['RelatedTopics']:
182+ if result_count >= self.result_limit: break
183+
184+ #some related topics have a 'Topics' sub list
185+ try:
186+ for result in raw_result['Topics']:
187+ if result_count >= self.result_limit: break
188+
189+ item = {
190+ 'name' : result['Text'],
191+ 'tooltip' : result['FirstURL'],
192+ 'icon name' : 'text-html',
193+ 'type' : 'xdg',
194+ 'command' : result['FirstURL'],
195+ 'context menu' : None,
196+ }
197+ parsed_results.append(item)
198+ result_count += 1
199+ except KeyError:
200+ #otherwise the RelatedTopic is a single entry
201+ item = {
202+ 'name' : raw_result['Text'],
203+ 'tooltip' : raw_result['FirstURL'],
204+ 'icon name' : 'text-html',
205+ 'type' : 'xdg',
206+ 'command' : raw_result['FirstURL'],
207+ 'context menu' : None,
208+ }
209+ parsed_results.append(item)
210+ result_count += 1
211+ except KeyError:
212+ pass
213+
214+ # check for external results section
215+ try:
216+ if raw_results['Results']:
217+ for raw_result in raw_results['Results']:
218+ if result_count >= self.result_limit: break
219+
220+ item = {
221+ 'name' : raw_result['Text'],
222+ 'tooltip' : raw_result['FirstURL'],
223+ 'icon name' : 'text-html',
224+ 'type' : 'xdg',
225+ 'command' : raw_result['FirstURL'],
226+ 'context menu' : None,
227+ }
228+ parsed_results.append(item)
229+ result_count += 1
230+
231+ except KeyError:
232+ pass
233+
234+
235+
236+ if parsed_results:
237+ parsed_results.append(self.action)
238+
239+ self.c.handle_search_result(self, parsed_results, self.current_query)
240+
241+
242+ def more_results_action(self, text):
243+
244+ text = text.replace("'", r"\'")
245+ text = text.replace('"', r'\"')
246+
247+ try:
248+ subprocess.Popen(self.action_command % text, shell = True)
249+ except OSError, e:
250+ self.c.write_to_log(self, 'Error launching plugin action.', is_error = True)
251+ self.c.write_to_log(self, e, is_error = True)
252+
253+

Subscribers

People subscribed via source and target branches