on master help sign in
Merge pull request #346 from Cqoicebordel/youtube

Adds engines : Youtube with or without API and multiple Qwant

commit8ce604385901ddfe15ef030188970cbb9b807c6a authorAdam Tauber <asciimoo@gmail.com> dateTue 2 Jun 2015, 17:26 (11 years ago) parent9d10277e0774c8
8 files changed+813−1
searx/engines/qwant.py+98 searx/engines/youtube_api.py+83 searx/engines/youtube_noapi.py+72 searx/settings.yml+26−1 searx/tests/engines/test_qwant.py+317 searx/tests/engines/test_youtube_api.py+111 searx/tests/engines/test_youtube_noapi.py+103 searx/tests/test_engines.py+3
searx/engines/qwant.pynew file+98
@@ -0,0 +1,98 @@
1+"""
2+ Qwant (Web, Images, News, Social)
3+
4+ @website https://qwant.com/
5+ @provide-api not officially (https://api.qwant.com/api/search/)
6+
7+ @using-api yes
8+ @results JSON
9+ @stable yes
10+ @parse url, title, content
11+"""
12+
13+from urllib import urlencode
14+from json import loads
15+from datetime import datetime
16+
17+# engine dependent config
18+categories = None
19+paging = True
20+language_support = True
21+
22+category_to_keyword = {'general': 'web',
23+ 'images': 'images',
24+ 'news': 'news',
25+ 'social media': 'social'}
26+
27+# search-url
28+url = 'https://api.qwant.com/api/search/{keyword}?count=10&offset={offset}&f=&{query}'
29+
30+
31+# do search-request
32+def request(query, params):
33+ offset = (params['pageno'] - 1) * 10
34+
35+ if categories[0] and categories[0] in category_to_keyword:
36+
37+ params['url'] = url.format(keyword=category_to_keyword[categories[0]],
38+ query=urlencode({'q': query}),
39+ offset=offset)
40+ else:
41+ params['url'] = url.format(keyword='web',
42+ query=urlencode({'q': query}),
43+ offset=offset)
44+
45+ # add language tag if specified
46+ if params['language'] != 'all':
47+ params['url'] += '&locale=' + params['language'].lower()
48+
49+ return params
50+
51+
52+# get response from search-request
53+def response(resp):
54+ results = []
55+
56+ search_results = loads(resp.text)
57+
58+ # return empty array if there are no results
59+ if 'data' not in search_results:
60+ return []
61+
62+ data = search_results.get('data', {})
63+
64+ res = data.get('result', {})
65+
66+ # parse results
67+ for result in res.get('items', {}):
68+
69+ title = result['title']
70+ res_url = result['url']
71+ content = result['desc']
72+
73+ if category_to_keyword.get(categories[0], '') == 'web':
74+ results.append({'title': title,
75+ 'content': content,
76+ 'url': res_url})
77+
78+ elif category_to_keyword.get(categories[0], '') == 'images':
79+ thumbnail_src = result['thumbnail']
80+ img_src = result['media']
81+ results.append({'template': 'images.html',
82+ 'url': res_url,
83+ 'title': title,
84+ 'content': '',
85+ 'thumbnail_src': thumbnail_src,
86+ 'img_src': img_src})
87+
88+ elif (category_to_keyword.get(categories[0], '') == 'news' or
89+ category_to_keyword.get(categories[0], '') == 'social'):
90+ published_date = datetime.fromtimestamp(result['date'], None)
91+
92+ results.append({'url': res_url,
93+ 'title': title,
94+ 'publishedDate': published_date,
95+ 'content': content})
96+
97+ # return results
98+ return results
searx/engines/youtube_api.pynew file+83
@@ -0,0 +1,83 @@
1+# Youtube (Videos)
2+#
3+# @website https://www.youtube.com/
4+# @provide-api yes (https://developers.google.com/apis-explorer/#p/youtube/v3/youtube.search.list)
5+#
6+# @using-api yes
7+# @results JSON
8+# @stable yes
9+# @parse url, title, content, publishedDate, thumbnail, embedded
10+
11+from json import loads
12+from urllib import urlencode
13+from dateutil import parser
14+
15+# engine dependent config
16+categories = ['videos', 'music']
17+paging = False
18+language_support = True
19+api_key = None
20+
21+# search-url
22+base_url = 'https://www.googleapis.com/youtube/v3/search'
23+search_url = base_url + '?part=snippet&{query}&maxResults=20&key={api_key}'
24+
25+embedded_url = '<iframe width="540" height="304" ' +\
26+ 'data-src="//www.youtube-nocookie.com/embed/{videoid}" ' +\
27+ 'frameborder="0" allowfullscreen></iframe>'
28+
29+base_youtube_url = 'https://www.youtube.com/watch?v='
30+
31+
32+# do search-request
33+def request(query, params):
34+ params['url'] = search_url.format(query=urlencode({'q': query}),
35+ api_key=api_key)
36+
37+ # add language tag if specified
38+ if params['language'] != 'all':
39+ params['url'] += '&relevanceLanguage=' + params['language'].split('_')[0]
40+
41+ return params
42+
43+
44+# get response from search-request
45+def response(resp):
46+ results = []
47+
48+ search_results = loads(resp.text)
49+
50+ # return empty array if there are no results
51+ if 'items' not in search_results:
52+ return []
53+
54+ # parse results
55+ for result in search_results['items']:
56+ videoid = result['id']['videoId']
57+
58+ title = result['snippet']['title']
59+ content = ''
60+ thumbnail = ''
61+
62+ pubdate = result['snippet']['publishedAt']
63+ publishedDate = parser.parse(pubdate)
64+
65+ thumbnail = result['snippet']['thumbnails']['high']['url']
66+
67+ content = result['snippet']['description']
68+
69+ url = base_youtube_url + videoid
70+
71+ embedded = embedded_url.format(videoid=videoid)
72+
73+ # append result
74+ results.append({'url': url,
75+ 'title': title,
76+ 'content': content,
77+ 'template': 'videos.html',
78+ 'publishedDate': publishedDate,
79+ 'embedded': embedded,
80+ 'thumbnail': thumbnail})
81+
82+ # return results
83+ return results
searx/engines/youtube_noapi.pynew file+72
@@ -0,0 +1,72 @@
1+# Youtube (Videos)
2+#
3+# @website https://www.youtube.com/
4+# @provide-api yes (https://developers.google.com/apis-explorer/#p/youtube/v3/youtube.search.list)
5+#
6+# @using-api no
7+# @results HTML
8+# @stable no
9+# @parse url, title, content, publishedDate, thumbnail, embedded
10+
11+from urllib import quote_plus
12+from lxml import html
13+from searx.engines.xpath import extract_text
14+
15+# engine dependent config
16+categories = ['videos', 'music']
17+paging = True
18+language_support = False
19+
20+# search-url
21+base_url = 'https://www.youtube.com/results'
22+search_url = base_url + '?search_query={query}&page={page}'
23+
24+embedded_url = '<iframe width="540" height="304" ' +\
25+ 'data-src="//www.youtube-nocookie.com/embed/{videoid}" ' +\
26+ 'frameborder="0" allowfullscreen></iframe>'
27+
28+base_youtube_url = 'https://www.youtube.com/watch?v='
29+
30+# specific xpath variables
31+results_xpath = "//ol/li/div[contains(@class, 'yt-lockup yt-lockup-tile yt-lockup-video vve-check')]"
32+url_xpath = './/h3/a/@href'
33+title_xpath = './/div[@class="yt-lockup-content"]/h3/a'
34+content_xpath = './/div[@class="yt-lockup-content"]/div[@class="yt-lockup-description yt-ui-ellipsis yt-ui-ellipsis-2"]'
35+
36+
37+# do search-request
38+def request(query, params):
39+ params['url'] = search_url.format(query=quote_plus(query),
40+ page=params['pageno'])
41+
42+ return params
43+
44+
45+# get response from search-request
46+def response(resp):
47+ results = []
48+
49+ dom = html.fromstring(resp.text)
50+
51+ # parse results
52+ for result in dom.xpath(results_xpath):
53+ videoid = result.xpath('@data-context-item-id')[0]
54+
55+ url = base_youtube_url + videoid
56+ thumbnail = 'https://i.ytimg.com/vi/' + videoid + '/hqdefault.jpg'
57+
58+ title = extract_text(result.xpath(title_xpath)[0])
59+ content = extract_text(result.xpath(content_xpath)[0])
60+
61+ embedded = embedded_url.format(videoid=videoid)
62+
63+ # append result
64+ results.append({'url': url,
65+ 'title': title,
66+ 'content': content,
67+ 'template': 'videos.html',
68+ 'embedded': embedded,
69+ 'thumbnail': thumbnail})
70+
71+ # return results
72+ return results
searx/settings.yml+26−1
@@ -168,6 +168,26 @@ engines:
168168 engine : piratebay
169169 shortcut : tpb
170170
171+ - name : qwant
172+ engine : qwant
173+ shortcut : qw
174+ categories : general
175+
176+ - name : qwant images
177+ engine : qwant
178+ shortcut : qwi
179+ categories : images
180+
181+ - name : qwant news
182+ engine : qwant
183+ shortcut : qwn
184+ categories : news
185+
186+ - name : qwant social
187+ engine : qwant
188+ shortcut : qws
189+ categories : social media
190+
171191 - name : kickass
172192 engine : kickass
173193 shortcut : ka
@@ -246,8 +266,13 @@ engines:
246266 shortcut : yhn
247267
248268 - name : youtube
249− engine : youtube
250269 shortcut : yt
270+ # You can use the engine using the official stable API, but you need an API key
271+ # See : https://console.developers.google.com/project
272+ # engine : youtube_api
273+ # api_key: 'apikey' # required!
274+ # Or you can use the html non-stable engine, activated by default
275+ engine : youtube_noapi
251276
252277 - name : dailymotion
253278 engine : dailymotion
searx/tests/engines/test_qwant.pynew file+317
@@ -0,0 +1,317 @@
1+from collections import defaultdict
2+import mock
3+from searx.engines import qwant
4+from searx.testing import SearxTestCase
5+
6+
7+class TestQwantEngine(SearxTestCase):
8+
9+ def test_request(self):
10+ query = 'test_query'
11+ dicto = defaultdict(dict)
12+ dicto['pageno'] = 0
13+ dicto['language'] = 'fr_FR'
14+ qwant.categories = ['']
15+ params = qwant.request(query, dicto)
16+ self.assertIn('url', params)
17+ self.assertIn(query, params['url'])
18+ self.assertIn('web', params['url'])
19+ self.assertIn('qwant.com', params['url'])
20+ self.assertIn('fr_fr', params['url'])
21+
22+ dicto['language'] = 'all'
23+ qwant.categories = ['news']
24+ params = qwant.request(query, dicto)
25+ self.assertFalse('fr' in params['url'])
26+ self.assertIn('news', params['url'])
27+
28+ def test_response(self):
29+ self.assertRaises(AttributeError, qwant.response, None)
30+ self.assertRaises(AttributeError, qwant.response, [])
31+ self.assertRaises(AttributeError, qwant.response, '')
32+ self.assertRaises(AttributeError, qwant.response, '[]')
33+
34+ response = mock.Mock(text='{}')
35+ self.assertEqual(qwant.response(response), [])
36+
37+ response = mock.Mock(text='{"data": {}}')
38+ self.assertEqual(qwant.response(response), [])
39+
40+ json = """
41+ {
42+ "status": "success",
43+ "data": {
44+ "query": {
45+ "locale": "en_us",
46+ "query": "Test",
47+ "offset": 10
48+ },
49+ "result": {
50+ "items": [
51+ {
52+ "title": "Title",
53+ "score": 9999,
54+ "url": "http://www.url.xyz",
55+ "source": "...",
56+ "desc": "Description",
57+ "date": "",
58+ "_id": "db0aadd62c2a8565567ffc382f5c61fa",
59+ "favicon": "https://s.qwant.com/fav.ico"
60+ }
61+ ],
62+ "filters": []
63+ },
64+ "cache": {
65+ "key": "e66aa864c00147a0e3a16ff7a5efafde",
66+ "created": 1433092754,
67+ "expiration": 259200,
68+ "status": "miss",
69+ "age": 0
70+ }
71+ }
72+ }
73+ """
74+ response = mock.Mock(text=json)
75+ qwant.categories = ['general']
76+ results = qwant.response(response)
77+ self.assertEqual(type(results), list)
78+ self.assertEqual(len(results), 1)
79+ self.assertEqual(results[0]['title'], 'Title')
80+ self.assertEqual(results[0]['url'], 'http://www.url.xyz')
81+ self.assertEqual(results[0]['content'], 'Description')
82+
83+ json = """
84+ {
85+ "status": "success",
86+ "data": {
87+ "query": {
88+ "locale": "en_us",
89+ "query": "Test",
90+ "offset": 10
91+ },
92+ "result": {
93+ "items": [
94+ {
95+ "title": "Title",
96+ "score": 9999,
97+ "url": "http://www.url.xyz",
98+ "source": "...",
99+ "media": "http://image.jpg",
100+ "desc": "",
101+ "thumbnail": "http://thumbnail.jpg",
102+ "date": "",
103+ "_id": "db0aadd62c2a8565567ffc382f5c61fa",
104+ "favicon": "https://s.qwant.com/fav.ico"
105+ }
106+ ],
107+ "filters": []
108+ },
109+ "cache": {
110+ "key": "e66aa864c00147a0e3a16ff7a5efafde",
111+ "created": 1433092754,
112+ "expiration": 259200,
113+ "status": "miss",
114+ "age": 0
115+ }
116+ }
117+ }
118+ """
119+ response = mock.Mock(text=json)
120+ qwant.categories = ['images']
121+ results = qwant.response(response)
122+ self.assertEqual(type(results), list)
123+ self.assertEqual(len(results), 1)
124+ self.assertEqual(results[0]['title'], 'Title')
125+ self.assertEqual(results[0]['url'], 'http://www.url.xyz')
126+ self.assertEqual(results[0]['content'], '')
127+ self.assertEqual(results[0]['thumbnail_src'], 'http://thumbnail.jpg')
128+ self.assertEqual(results[0]['img_src'], 'http://image.jpg')
129+
130+ json = """
131+ {
132+ "status": "success",
133+ "data": {
134+ "query": {
135+ "locale": "en_us",
136+ "query": "Test",
137+ "offset": 10
138+ },
139+ "result": {
140+ "items": [
141+ {
142+ "title": "Title",
143+ "score": 9999,
144+ "url": "http://www.url.xyz",
145+ "source": "...",
146+ "desc": "Description",
147+ "date": 1433260920,
148+ "_id": "db0aadd62c2a8565567ffc382f5c61fa",
149+ "favicon": "https://s.qwant.com/fav.ico"
150+ }
151+ ],
152+ "filters": []
153+ },
154+ "cache": {
155+ "key": "e66aa864c00147a0e3a16ff7a5efafde",
156+ "created": 1433092754,
157+ "expiration": 259200,
158+ "status": "miss",
159+ "age": 0
160+ }
161+ }
162+ }
163+ """
164+ response = mock.Mock(text=json)
165+ qwant.categories = ['news']
166+ results = qwant.response(response)
167+ self.assertEqual(type(results), list)
168+ self.assertEqual(len(results), 1)
169+ self.assertEqual(results[0]['title'], 'Title')
170+ self.assertEqual(results[0]['url'], 'http://www.url.xyz')
171+ self.assertEqual(results[0]['content'], 'Description')
172+ self.assertIn('publishedDate', results[0])
173+
174+ json = """
175+ {
176+ "status": "success",
177+ "data": {
178+ "query": {
179+ "locale": "en_us",
180+ "query": "Test",
181+ "offset": 10
182+ },
183+ "result": {
184+ "items": [
185+ {
186+ "title": "Title",
187+ "score": 9999,
188+ "url": "http://www.url.xyz",
189+ "source": "...",
190+ "desc": "Description",
191+ "date": 1433260920,
192+ "_id": "db0aadd62c2a8565567ffc382f5c61fa",
193+ "favicon": "https://s.qwant.com/fav.ico"
194+ }
195+ ],
196+ "filters": []
197+ },
198+ "cache": {
199+ "key": "e66aa864c00147a0e3a16ff7a5efafde",
200+ "created": 1433092754,
201+ "expiration": 259200,
202+ "status": "miss",
203+ "age": 0
204+ }
205+ }
206+ }
207+ """
208+ response = mock.Mock(text=json)
209+ qwant.categories = ['social media']
210+ results = qwant.response(response)
211+ self.assertEqual(type(results), list)
212+ self.assertEqual(len(results), 1)
213+ self.assertEqual(results[0]['title'], 'Title')
214+ self.assertEqual(results[0]['url'], 'http://www.url.xyz')
215+ self.assertEqual(results[0]['content'], 'Description')
216+ self.assertIn('publishedDate', results[0])
217+
218+ json = """
219+ {
220+ "status": "success",
221+ "data": {
222+ "query": {
223+ "locale": "en_us",
224+ "query": "Test",
225+ "offset": 10
226+ },
227+ "result": {
228+ "items": [
229+ {
230+ "title": "Title",
231+ "score": 9999,
232+ "url": "http://www.url.xyz",
233+ "source": "...",
234+ "desc": "Description",
235+ "date": 1433260920,
236+ "_id": "db0aadd62c2a8565567ffc382f5c61fa",
237+ "favicon": "https://s.qwant.com/fav.ico"
238+ }
239+ ],
240+ "filters": []
241+ },
242+ "cache": {
243+ "key": "e66aa864c00147a0e3a16ff7a5efafde",
244+ "created": 1433092754,
245+ "expiration": 259200,
246+ "status": "miss",
247+ "age": 0
248+ }
249+ }
250+ }
251+ """
252+ response = mock.Mock(text=json)
253+ qwant.categories = ['']
254+ results = qwant.response(response)
255+ self.assertEqual(type(results), list)
256+ self.assertEqual(len(results), 0)
257+
258+ json = """
259+ {
260+ "status": "success",
261+ "data": {
262+ "query": {
263+ "locale": "en_us",
264+ "query": "Test",
265+ "offset": 10
266+ },
267+ "result": {
268+ "filters": []
269+ },
270+ "cache": {
271+ "key": "e66aa864c00147a0e3a16ff7a5efafde",
272+ "created": 1433092754,
273+ "expiration": 259200,
274+ "status": "miss",
275+ "age": 0
276+ }
277+ }
278+ }
279+ """
280+ response = mock.Mock(text=json)
281+ results = qwant.response(response)
282+ self.assertEqual(type(results), list)
283+ self.assertEqual(len(results), 0)
284+
285+ json = """
286+ {
287+ "status": "success",
288+ "data": {
289+ "query": {
290+ "locale": "en_us",
291+ "query": "Test",
292+ "offset": 10
293+ },
294+ "cache": {
295+ "key": "e66aa864c00147a0e3a16ff7a5efafde",
296+ "created": 1433092754,
297+ "expiration": 259200,
298+ "status": "miss",
299+ "age": 0
300+ }
301+ }
302+ }
303+ """
304+ response = mock.Mock(text=json)
305+ results = qwant.response(response)
306+ self.assertEqual(type(results), list)
307+ self.assertEqual(len(results), 0)
308+
309+ json = """
310+ {
311+ "status": "success"
312+ }
313+ """
314+ response = mock.Mock(text=json)
315+ results = qwant.response(response)
316+ self.assertEqual(type(results), list)
317+ self.assertEqual(len(results), 0)
searx/tests/engines/test_youtube_api.pynew file+111
@@ -0,0 +1,111 @@
1+from collections import defaultdict
2+import mock
3+from searx.engines import youtube_api
4+from searx.testing import SearxTestCase
5+
6+
7+class TestYoutubeAPIEngine(SearxTestCase):
8+
9+ def test_request(self):
10+ query = 'test_query'
11+ dicto = defaultdict(dict)
12+ dicto['pageno'] = 0
13+ dicto['language'] = 'fr_FR'
14+ params = youtube_api.request(query, dicto)
15+ self.assertTrue('url' in params)
16+ self.assertTrue(query in params['url'])
17+ self.assertIn('googleapis.com', params['url'])
18+ self.assertIn('youtube', params['url'])
19+ self.assertIn('fr', params['url'])
20+
21+ dicto['language'] = 'all'
22+ params = youtube_api.request(query, dicto)
23+ self.assertFalse('fr' in params['url'])
24+
25+ def test_response(self):
26+ self.assertRaises(AttributeError, youtube_api.response, None)
27+ self.assertRaises(AttributeError, youtube_api.response, [])
28+ self.assertRaises(AttributeError, youtube_api.response, '')
29+ self.assertRaises(AttributeError, youtube_api.response, '[]')
30+
31+ response = mock.Mock(text='{}')
32+ self.assertEqual(youtube_api.response(response), [])
33+
34+ response = mock.Mock(text='{"data": []}')
35+ self.assertEqual(youtube_api.response(response), [])
36+
37+ json = """
38+ {
39+ "kind": "youtube#searchListResponse",
40+ "etag": "xmg9xJZuZD438sF4hb-VcBBREXc/YJQDcTBCDcaBvl-sRZJoXdvy1ME",
41+ "nextPageToken": "CAUQAA",
42+ "pageInfo": {
43+ "totalResults": 1000000,
44+ "resultsPerPage": 20
45+ },
46+ "items": [
47+ {
48+ "kind": "youtube#searchResult",
49+ "etag": "xmg9xJZuZD438sF4hb-VcBBREXc/IbLO64BMhbHIgWLwLw7MDYe7Hs4",
50+ "id": {
51+ "kind": "youtube#video",
52+ "videoId": "DIVZCPfAOeM"
53+ },
54+ "snippet": {
55+ "publishedAt": "2015-05-29T22:41:04.000Z",
56+ "channelId": "UCNodmx1ERIjKqvcJLtdzH5Q",
57+ "title": "Title",
58+ "description": "Description",
59+ "thumbnails": {
60+ "default": {
61+ "url": "https://i.ytimg.com/vi/DIVZCPfAOeM/default.jpg"
62+ },
63+ "medium": {
64+ "url": "https://i.ytimg.com/vi/DIVZCPfAOeM/mqdefault.jpg"
65+ },
66+ "high": {
67+ "url": "https://i.ytimg.com/vi/DIVZCPfAOeM/hqdefault.jpg"
68+ }
69+ },
70+ "channelTitle": "MinecraftUniverse",
71+ "liveBroadcastContent": "none"
72+ }
73+ }
74+ ]
75+ }
76+ """
77+ response = mock.Mock(text=json)
78+ results = youtube_api.response(response)
79+ self.assertEqual(type(results), list)
80+ self.assertEqual(len(results), 1)
81+ self.assertEqual(results[0]['title'], 'Title')
82+ self.assertEqual(results[0]['url'], 'https://www.youtube.com/watch?v=DIVZCPfAOeM')
83+ self.assertEqual(results[0]['content'], 'Description')
84+ self.assertEqual(results[0]['thumbnail'], 'https://i.ytimg.com/vi/DIVZCPfAOeM/hqdefault.jpg')
85+ self.assertTrue('DIVZCPfAOeM' in results[0]['embedded'])
86+
87+ json = """
88+ {
89+ "kind": "youtube#searchListResponse",
90+ "etag": "xmg9xJZuZD438sF4hb-VcBBREXc/YJQDcTBCDcaBvl-sRZJoXdvy1ME",
91+ "nextPageToken": "CAUQAA",
92+ "pageInfo": {
93+ "totalResults": 1000000,
94+ "resultsPerPage": 20
95+ }
96+ }
97+ """
98+ response = mock.Mock(text=json)
99+ results = youtube_api.response(response)
100+ self.assertEqual(type(results), list)
101+ self.assertEqual(len(results), 0)
102+
103+ json = """
104+ {"toto":{"entry":[]
105+ }
106+ }
107+ """
108+ response = mock.Mock(text=json)
109+ results = youtube_api.response(response)
110+ self.assertEqual(type(results), list)
111+ self.assertEqual(len(results), 0)
searx/tests/engines/test_youtube_noapi.pynew file+103
@@ -0,0 +1,103 @@
1+# -*- coding: utf-8 -*-
2+from collections import defaultdict
3+import mock
4+from searx.engines import youtube_noapi
5+from searx.testing import SearxTestCase
6+
7+
8+class TestYoutubeNoAPIEngine(SearxTestCase):
9+
10+ def test_request(self):
11+ query = 'test_query'
12+ dicto = defaultdict(dict)
13+ dicto['pageno'] = 0
14+ params = youtube_noapi.request(query, dicto)
15+ self.assertIn('url', params)
16+ self.assertIn(query, params['url'])
17+ self.assertIn('youtube.com', params['url'])
18+
19+ def test_response(self):
20+ self.assertRaises(AttributeError, youtube_noapi.response, None)
21+ self.assertRaises(AttributeError, youtube_noapi.response, [])
22+ self.assertRaises(AttributeError, youtube_noapi.response, '')
23+ self.assertRaises(AttributeError, youtube_noapi.response, '[]')
24+
25+ response = mock.Mock(text='<html></html>')
26+ self.assertEqual(youtube_noapi.response(response), [])
27+
28+ html = """
29+ <ol id="item-section-063864" class="item-section">
30+ <li>
31+ <div class="yt-lockup yt-lockup-tile yt-lockup-video vve-check clearfix yt-uix-tile"
32+ data-context-item-id="DIVZCPfAOeM"
33+ data-visibility-tracking="CBgQ3DAYACITCPGXnYau6sUCFZEIHAod-VQASCj0JECx_-GK5uqMpcIB">
34+ <div class="yt-lockup-dismissable"><div class="yt-lockup-thumbnail contains-addto">
35+ <a aria-hidden="true" href="/watch?v=DIVZCPfAOeM" class=" yt-uix-sessionlink pf-link"
36+ data-sessionlink="itct=CBgQ3DAYACITCPGXnYau6sUCFZEIHAod-VQASCj0JFIEdGVzdA">
37+ <div class="yt-thumb video-thumb"><img src="//i.ytimg.com/vi/DIVZCPfAOeM/mqdefault.jpg"
38+ width="196" height="110"/></div><span class="video-time" aria-hidden="true">11:35</span></a>
39+ <span class="thumb-menu dark-overflow-action-menu video-actions">
40+ </span>
41+ </div>
42+ <div class="yt-lockup-content">
43+ <h3 class="yt-lockup-title">
44+ <a href="/watch?v=DIVZCPfAOeM"
45+ class="yt-uix-tile-link yt-ui-ellipsis yt-ui-ellipsis-2 yt-uix-sessionlink spf-link"
46+ data-sessionlink="itct=CBgQ3DAYACITCPGXnYau6sUCFZEIHAod-VQASCj0JFIEdGVzdA"
47+ title="Top Speed Test Kawasaki Ninja H2 (Thailand) By. MEHAY SUPERBIKE"
48+ aria-describedby="description-id-259079" rel="spf-prefetch" dir="ltr">
49+ Title
50+ </a>
51+ <span class="accessible-description" id="description-id-259079"> - Durée : 11:35.</span>
52+ </h3>
53+ <div class="yt-lockup-byline">de
54+ <a href="/user/mheejapan" class=" yt-uix-sessionlink spf-link g-hovercard"
55+ data-sessionlink="itct=CBgQ3DAYACITCPGXnYau6sUCFZEIHAod-VQASCj0JA" data-ytid="UCzEesu54Hjs0uRKmpy66qeA"
56+ data-name="">MEHAY SUPERBIKE</a></div><div class="yt-lockup-meta">
57+ <ul class="yt-lockup-meta-info">
58+ <li>il y a 20 heures</li>
59+ <li>8 424 vues</li>
60+ </ul>
61+ </div>
62+ <div class="yt-lockup-description yt-ui-ellipsis yt-ui-ellipsis-2" dir="ltr">
63+ Description
64+ </div>
65+ <div class="yt-lockup-badges">
66+ <ul class="yt-badge-list ">
67+ <li class="yt-badge-item" >
68+ <span class="yt-badge">Nouveauté</span>
69+ </li>
70+ <li class="yt-badge-item" ><span class="yt-badge " >HD</span></li>
71+ </ul>
72+ </div>
73+ <div class="yt-lockup-action-menu yt-uix-menu-container">
74+ <div class="yt-uix-menu yt-uix-videoactionmenu hide-until-delayloaded"
75+ data-video-id="DIVZCPfAOeM" data-menu-content-id="yt-uix-videoactionmenu-menu">
76+ </div>
77+ </div>
78+ </div>
79+ </div>
80+ </div>
81+ </li>
82+ </ol>
83+ """
84+ response = mock.Mock(text=html)
85+ results = youtube_noapi.response(response)
86+ self.assertEqual(type(results), list)
87+ self.assertEqual(len(results), 1)
88+ self.assertEqual(results[0]['title'], 'Title')
89+ self.assertEqual(results[0]['url'], 'https://www.youtube.com/watch?v=DIVZCPfAOeM')
90+ self.assertEqual(results[0]['content'], 'Description')
91+ self.assertEqual(results[0]['thumbnail'], 'https://i.ytimg.com/vi/DIVZCPfAOeM/hqdefault.jpg')
92+ self.assertTrue('DIVZCPfAOeM' in results[0]['embedded'])
93+
94+ html = """
95+ <ol id="item-section-063864" class="item-section">
96+ <li>
97+ </li>
98+ </ol>
99+ """
100+ response = mock.Mock(text=html)
101+ results = youtube_noapi.response(response)
102+ self.assertEqual(type(results), list)
103+ self.assertEqual(len(results), 0)
searx/tests/test_engines.py+3
@@ -25,6 +25,7 @@ from searx.tests.engines.test_mixcloud import * # noqa
2525from searx.tests.engines.test_openstreetmap import * # noqa
2626from searx.tests.engines.test_photon import * # noqa
2727from searx.tests.engines.test_piratebay import * # noqa
28+from searx.tests.engines.test_qwant import * # noqa
2829from searx.tests.engines.test_searchcode_code import * # noqa
2930from searx.tests.engines.test_searchcode_doc import * # noqa
3031from searx.tests.engines.test_soundcloud import * # noqa
@@ -40,4 +41,6 @@ from searx.tests.engines.test_www500px import * # noqa
4041from searx.tests.engines.test_yacy import * # noqa
4142from searx.tests.engines.test_yahoo import * # noqa
4243from searx.tests.engines.test_youtube import * # noqa
44+from searx.tests.engines.test_youtube_api import * # noqa
45+from searx.tests.engines.test_youtube_noapi import * # noqa
4346from searx.tests.engines.test_yahoo_news import * # noqa