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