1 /* This file is (c) 2008-2012 Konstantin Isakov <ikm@goldendict.org>
2 * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */
3
4 #include "forvo.hh"
5 #include "wstring_qt.hh"
6 #include <QNetworkAccessManager>
7 #include <QNetworkReply>
8 #include <QtXml>
9 #include <list>
10 #include "audiolink.hh"
11 #include "htmlescape.hh"
12 #include "country.hh"
13 #include "language.hh"
14 #include "langcoder.hh"
15 #include "utf8.hh"
16 #include "gddebug.hh"
17 #include "qt4x5.hh"
18
19 namespace Forvo {
20
21 using namespace Dictionary;
22
23 namespace {
24
25 class ForvoDictionary: public Dictionary::Class
26 {
27 string name;
28 QString apiKey, languageCode;
29 QNetworkAccessManager & netMgr;
30
31 public:
32
ForvoDictionary(string const & id,string const & name_,QString const & apiKey_,QString const & languageCode_,QNetworkAccessManager & netMgr_)33 ForvoDictionary( string const & id, string const & name_,
34 QString const & apiKey_,
35 QString const & languageCode_,
36 QNetworkAccessManager & netMgr_ ):
37 Dictionary::Class( id, vector< string >() ),
38 name( name_ ),
39 apiKey( apiKey_ ),
40 languageCode( languageCode_ ),
41 netMgr( netMgr_ )
42 {
43 }
44
getName()45 virtual string getName() throw()
46 { return name; }
47
getProperties()48 virtual map< Property, string > getProperties() throw()
49 { return map< Property, string >(); }
50
getArticleCount()51 virtual unsigned long getArticleCount() throw()
52 { return 0; }
53
getWordCount()54 virtual unsigned long getWordCount() throw()
55 { return 0; }
56
prefixMatch(wstring const &,unsigned long)57 virtual sptr< WordSearchRequest > prefixMatch( wstring const & /*word*/,
58 unsigned long /*maxResults*/ ) THROW_SPEC( std::exception )
59 {
60 sptr< WordSearchRequestInstant > sr = new WordSearchRequestInstant;
61
62 sr->setUncertain( true );
63
64 return sr;
65 }
66
67 virtual sptr< DataRequest > getArticle( wstring const &, vector< wstring > const & alts,
68 wstring const &, bool )
69 THROW_SPEC( std::exception );
70
71 protected:
72
73 virtual void loadIcon() throw();
74
75 };
76
getArticle(wstring const & word,vector<wstring> const & alts,wstring const &,bool)77 sptr< DataRequest > ForvoDictionary::getArticle( wstring const & word,
78 vector< wstring > const & alts,
79 wstring const &, bool )
80 THROW_SPEC( std::exception )
81 {
82 if ( word.size() > 80 )
83 {
84 // Don't make excessively large queries -- they're fruitless anyway
85
86 return new DataRequestInstant( false );
87 }
88 else
89 return new ForvoArticleRequest( word, alts, apiKey, languageCode, getId(),
90 netMgr );
91 }
92
loadIcon()93 void ForvoDictionary::loadIcon() throw()
94 {
95 if ( dictionaryIconLoaded )
96 return;
97
98 // Experimental code to generate icon -- but the flags clutter the interface too
99 // much and we're better with a single icon.
100 #if 0
101 if ( languageCode.size() == 2 )
102 {
103 QString countryCode = Language::countryCodeForId( LangCoder::code2toInt( languageCode.toLatin1().data() ) );
104
105 if ( countryCode.size() )
106 {
107 QImage flag( QString( ":/flags/%1.png" ).arg( countryCode.toLower() ) );
108
109 if ( !flag.isNull() )
110 {
111 QImage img( ":/icons/forvo_icon_base.png" );
112
113 {
114 QPainter painter( &img );
115 painter.drawImage( QPoint( 5, 7 ), flag );
116 }
117
118 return QIcon( QPixmap::fromImage( img ) );
119 }
120 }
121 }
122 #endif
123 dictionaryIcon = dictionaryNativeIcon = QIcon( ":/icons/forvo.png" );
124 dictionaryIconLoaded = true;
125 }
126
127 }
128
cancel()129 void ForvoArticleRequest::cancel()
130 {
131 finish();
132 }
133
ForvoArticleRequest(wstring const & str,vector<wstring> const & alts,QString const & apiKey_,QString const & languageCode_,string const & dictionaryId_,QNetworkAccessManager & mgr)134 ForvoArticleRequest::ForvoArticleRequest( wstring const & str,
135 vector< wstring > const & alts,
136 QString const & apiKey_,
137 QString const & languageCode_,
138 string const & dictionaryId_,
139 QNetworkAccessManager & mgr ):
140 apiKey( apiKey_ ), languageCode( languageCode_ ),
141 dictionaryId( dictionaryId_ )
142 {
143 connect( &mgr, SIGNAL( finished( QNetworkReply * ) ),
144 this, SLOT( requestFinished( QNetworkReply * ) ),
145 Qt::QueuedConnection );
146
147 addQuery( mgr, str );
148
149 for( unsigned x = 0; x < alts.size(); ++x )
150 addQuery( mgr, alts[ x ] );
151 }
152
addQuery(QNetworkAccessManager & mgr,wstring const & str)153 void ForvoArticleRequest::addQuery( QNetworkAccessManager & mgr,
154 wstring const & str )
155 {
156 gdDebug( "Forvo: requesting article %s\n", gd::toQString( str ).toUtf8().data() );
157
158 QString key;
159
160 if ( apiKey.simplified().isEmpty() )
161 {
162 // Use the default api key. That's the key I have just registered myself.
163 // It has a limit of 1000 requests a day, and may also get banned in the
164 // future. Can't do much about it. Get your own key, it is simple.
165 key = "5efa5d045a16d10ad9c4705bd5d8e56a";
166 }
167 else
168 key = apiKey;
169
170 QUrl reqUrl = QUrl::fromEncoded(
171 QString( "https://apifree.forvo.com"
172 "/key/" + key +
173 "/action/word-pronunciations"
174 "/format/xml"
175 "/word/" + QLatin1String( QUrl::toPercentEncoding( gd::toQString( str ) ) ) +
176 "/language/" + languageCode +
177 "/order/rate-desc"
178 ).toUtf8() );
179
180 // DPRINTF( "req: %s\n", reqUrl.toEncoded().data() );
181
182 sptr< QNetworkReply > netReply = mgr.get( QNetworkRequest( reqUrl ) );
183
184 netReplies.push_back( NetReply( netReply, Utf8::encode( str ) ) );
185 }
186
requestFinished(QNetworkReply * r)187 void ForvoArticleRequest::requestFinished( QNetworkReply * r )
188 {
189 GD_DPRINTF( "Finished.\n" );
190
191 if ( isFinished() ) // Was cancelled
192 return;
193
194 // Find this reply
195
196 bool found = false;
197
198 for( NetReplies::iterator i = netReplies.begin(); i != netReplies.end(); ++i )
199 {
200 if ( i->reply.get() == r )
201 {
202 i->finished = true; // Mark as finished
203 found = true;
204 break;
205 }
206 }
207
208 if ( !found )
209 {
210 // Well, that's not our reply, don't do anything
211 return;
212 }
213
214 bool updated = false;
215
216 for( ; netReplies.size() && netReplies.front().finished; netReplies.pop_front() )
217 {
218 sptr< QNetworkReply > netReply = netReplies.front().reply;
219
220 if ( netReply->error() == QNetworkReply::NoError )
221 {
222 QDomDocument dd;
223
224 QString errorStr;
225 int errorLine, errorColumn;
226
227 if ( !dd.setContent( netReply.get(), false, &errorStr, &errorLine, &errorColumn ) )
228 {
229 setErrorString( QString( tr( "XML parse error: %1 at %2,%3" ).
230 arg( errorStr ).arg( errorLine ).arg( errorColumn ) ) );
231 }
232 else
233 {
234 // DPRINTF( "%s\n", dd.toByteArray().data() );
235
236 QDomNode items = dd.namedItem( "items" );
237
238 if ( !items.isNull() )
239 {
240 QDomNodeList nl = items.toElement().elementsByTagName( "item" );
241
242 if ( nl.count() )
243 {
244 string articleBody;
245
246 articleBody += "<div class='forvo_headword'>";
247 articleBody += Html::escape( netReplies.front().word );
248 articleBody += "</div>";
249
250 articleBody += "<table class=\"forvo_play\">";
251
252 for( Qt4x5::Dom::size_type x = 0; x < nl.length(); ++x )
253 {
254 QDomElement item = nl.item( x ).toElement();
255
256 QDomNode mp3 = item.namedItem( "pathmp3" );
257
258 if ( !mp3.isNull() )
259 {
260 articleBody += "<tr>";
261
262 QUrl url( mp3.toElement().text() );
263
264 string ref = string( "\"" ) + url.toEncoded().data() + "\"";
265
266 articleBody += addAudioLink( ref, dictionaryId ).c_str();
267
268 bool isMale = ( item.namedItem( "sex" ).toElement().text().toLower() != "f" );
269
270 QString user = item.namedItem( "username" ).toElement().text();
271 QString country = item.namedItem( "country" ).toElement().text();
272
273 string userProfile = string( "http://www.forvo.com/user/" ) +
274 QUrl::toPercentEncoding( user ).data() + "/";
275
276 int totalVotes = item.namedItem( "num_votes" ).toElement().text().toInt();
277 int positiveVotes = item.namedItem( "num_positive_votes" ).toElement().text().toInt();
278 int negativeVotes = totalVotes - positiveVotes;
279
280 string votes;
281
282 if ( positiveVotes || negativeVotes )
283 {
284 votes += " ";
285
286 if ( positiveVotes )
287 {
288 votes += "<span class='forvo_positive_votes'>+";
289 votes += QByteArray::number( positiveVotes ).data();
290 votes += "</span>";
291 }
292
293 if ( negativeVotes )
294 {
295 if ( positiveVotes )
296 votes += " ";
297
298 votes += "<span class='forvo_negative_votes'>-";
299 votes += QByteArray::number( negativeVotes ).data();
300 votes += "</span>";
301 }
302 }
303
304 string addTime =
305 tr( "Added %1" ).arg( item.namedItem( "addtime" ).toElement().text() ).toUtf8().data();
306
307 articleBody += "<td><a href=" + ref + " title=\"" + Html::escape( addTime ) + "\"><img src=\"qrcx://localhost/icons/playsound.png\" border=\"0\" alt=\"Play\"/></a></td>";
308 articleBody += string( "<td>" ) + tr( "by" ).toUtf8().data() + " <a class='forvo_user' href='"
309 + userProfile + "'>"
310 + Html::escape( user.toUtf8().data() )
311 + "</a> <span class='forvo_location'>("
312 + ( isMale ? tr( "Male" ) : tr( "Female" ) ).toUtf8().data()
313 + " "
314 + tr( "from" ).toUtf8().data()
315 + " "
316 + "<img src='qrcx://localhost/flags/" + Country::englishNametoIso2( country ).toUtf8().data()
317 + ".png'/> "
318 + Html::escape( country.toUtf8().data() )
319 + ")</span>"
320 + votes
321 + "</td>";
322 articleBody += "</tr>";
323 }
324 }
325
326 articleBody += "</table>";
327
328 Mutex::Lock _( dataMutex );
329
330 size_t prevSize = data.size();
331
332 data.resize( prevSize + articleBody.size() );
333
334 memcpy( &data.front() + prevSize, articleBody.data(), articleBody.size() );
335
336 hasAnyData = true;
337
338 updated = true;
339 }
340 }
341
342 QDomNode errors = dd.namedItem( "errors" );
343
344 if ( !errors.isNull() )
345 {
346 QString text( errors.namedItem( "error" ).toElement().text() );
347
348 if ( text == "Limit/day reached." && apiKey.simplified().isEmpty() )
349 {
350 // Give a hint that the user should apply for his own key.
351
352 text += "\n" + tr( "Go to Edit|Dictionaries|Sources|Forvo and apply for our own API key to make this error disappear." );
353 }
354
355 setErrorString( text );
356 }
357 }
358 GD_DPRINTF( "done.\n" );
359 }
360 else
361 setErrorString( netReply->errorString() );
362 }
363
364 if ( netReplies.empty() )
365 finish();
366 else
367 if ( updated )
368 update();
369 }
370
makeDictionaries(Dictionary::Initializing &,Config::Forvo const & forvo,QNetworkAccessManager & mgr)371 vector< sptr< Dictionary::Class > > makeDictionaries(
372 Dictionary::Initializing &,
373 Config::Forvo const & forvo,
374 QNetworkAccessManager & mgr )
375 THROW_SPEC( std::exception )
376 {
377 vector< sptr< Dictionary::Class > > result;
378
379 if ( forvo.enable )
380 {
381 QStringList codes = forvo.languageCodes.split( ',', QString::SkipEmptyParts );
382
383 QSet< QString > usedCodes;
384
385 for( int x = 0; x < codes.size(); ++x )
386 {
387 QString code = codes[ x ].simplified();
388
389 if ( code.size() && !usedCodes.contains( code ) )
390 {
391 // Generate id
392
393 QCryptographicHash hash( QCryptographicHash::Md5 );
394
395 hash.addData( "Forvo source version 1.0" );
396 hash.addData( code.toUtf8() );
397
398 QString displayedCode( code.toLower() );
399
400 if ( displayedCode.size() )
401 displayedCode[ 0 ] = displayedCode[ 0 ].toUpper();
402
403 result.push_back(
404 new ForvoDictionary( hash.result().toHex().data(),
405 QString( "Forvo (%1)" ).arg( displayedCode ).toUtf8().data(),
406 forvo.apiKey, code, mgr ) );
407
408 usedCodes.insert( code );
409 }
410 }
411 }
412
413 return result;
414 }
415
416 }
417