mirror of
https://github.com/xiaoyifang/goldendict-ng.git
synced 2024-11-24 00:14:06 +00:00
04bdf3aa36
This is just a refinement on how the word search is done in the dictionaries that don't actually ability to search words in. Previously they emitted dummy italicized suggestions, which were getting in the way. Now they don't emit anything, but mark the search as uncertain. Any uncertain searches don't mark the word input with the different color to indicate the search has failed.
414 lines
12 KiB
C++
414 lines
12 KiB
C++
/* This file is (c) 2008-2010 Konstantin Isakov <ikm@users.berlios.de>
|
|
* Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */
|
|
|
|
#include "forvo.hh"
|
|
#include "wstring_qt.hh"
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkReply>
|
|
#include <QUrl>
|
|
#include <QtXml>
|
|
#include <list>
|
|
#include "audiolink.hh"
|
|
#include "htmlescape.hh"
|
|
#include "country.hh"
|
|
#include "language.hh"
|
|
#include "langcoder.hh"
|
|
|
|
namespace Forvo {
|
|
|
|
using namespace Dictionary;
|
|
|
|
namespace {
|
|
|
|
class ForvoDictionary: public Dictionary::Class
|
|
{
|
|
string name;
|
|
QString apiKey, languageCode;
|
|
QNetworkAccessManager & netMgr;
|
|
|
|
public:
|
|
|
|
ForvoDictionary( string const & id, string const & name_,
|
|
QString const & apiKey_,
|
|
QString const & languageCode_,
|
|
QNetworkAccessManager & netMgr_ ):
|
|
Dictionary::Class( id, vector< string >() ),
|
|
name( name_ ),
|
|
apiKey( apiKey_ ),
|
|
languageCode( languageCode_ ),
|
|
netMgr( netMgr_ )
|
|
{
|
|
}
|
|
|
|
virtual string getName() throw()
|
|
{ return name; }
|
|
|
|
virtual map< Property, string > getProperties() throw()
|
|
{ return map< Property, string >(); }
|
|
|
|
virtual unsigned long getArticleCount() throw()
|
|
{ return 0; }
|
|
|
|
virtual unsigned long getWordCount() throw()
|
|
{ return 0; }
|
|
|
|
virtual QIcon getIcon() throw();
|
|
|
|
virtual sptr< WordSearchRequest > prefixMatch( wstring const & /*word*/,
|
|
unsigned long /*maxResults*/ ) throw( std::exception )
|
|
{
|
|
sptr< WordSearchRequestInstant > sr = new WordSearchRequestInstant;
|
|
|
|
sr->setUncertain( true );
|
|
|
|
return sr;
|
|
}
|
|
|
|
virtual sptr< DataRequest > getArticle( wstring const &, vector< wstring > const & alts,
|
|
wstring const & )
|
|
throw( std::exception );
|
|
};
|
|
|
|
class ForvoArticleRequest: public ForvoDataRequestSlots
|
|
{
|
|
typedef std::list< std::pair< sptr< QNetworkReply >, bool > > NetReplies;
|
|
NetReplies netReplies;
|
|
QString word, apiKey, languageCode;
|
|
string dictionaryId;
|
|
|
|
public:
|
|
|
|
ForvoArticleRequest( wstring const & word, vector< wstring > const & alts,
|
|
QString const & apiKey_,
|
|
QString const & languageCode_,
|
|
string const & dictionaryId_,
|
|
QNetworkAccessManager & mgr );
|
|
|
|
virtual void cancel();
|
|
|
|
private:
|
|
|
|
void addQuery( QNetworkAccessManager & mgr, wstring const & word );
|
|
|
|
virtual void requestFinished( QNetworkReply * );
|
|
};
|
|
|
|
void ForvoArticleRequest::cancel()
|
|
{
|
|
finish();
|
|
}
|
|
|
|
ForvoArticleRequest::ForvoArticleRequest( wstring const & str,
|
|
vector< wstring > const & /*alts*/,
|
|
QString const & apiKey_,
|
|
QString const & languageCode_,
|
|
string const & dictionaryId_,
|
|
QNetworkAccessManager & mgr ):
|
|
word( gd::toQString( str ) ), apiKey( apiKey_ ),
|
|
languageCode( languageCode_ ), dictionaryId( dictionaryId_ )
|
|
{
|
|
connect( &mgr, SIGNAL( finished( QNetworkReply * ) ),
|
|
this, SLOT( requestFinished( QNetworkReply * ) ),
|
|
Qt::QueuedConnection );
|
|
|
|
addQuery( mgr, str );
|
|
|
|
// Don't do alts for now -- the api only allows 1000 requests a day per key
|
|
#if 0
|
|
for( unsigned x = 0; x < alts.size(); ++x )
|
|
addQuery( mgr, alts[ x ] );
|
|
#endif
|
|
}
|
|
|
|
void ForvoArticleRequest::addQuery( QNetworkAccessManager & mgr,
|
|
wstring const & str )
|
|
{
|
|
printf( "Requesting article %ls\n", str.c_str() );
|
|
|
|
if ( apiKey.simplified().isEmpty() )
|
|
{
|
|
// Use the default api key. That's the key I have just registered myself.
|
|
// It has a limit of 1000 requests a day, and may also get banned in the
|
|
// future. Can't do much about it. Get your own key, it is simple.
|
|
apiKey = "5efa5d045a16d10ad9c4705bd5d8e56a";
|
|
}
|
|
|
|
QUrl reqUrl = QUrl::fromEncoded(
|
|
QString( "http://apifree.forvo.com/key/%1/format/xml/action/word-pronunciations/word/%2/language/%3" )
|
|
.arg( apiKey ).arg( QString::fromAscii( QUrl::toPercentEncoding( gd::toQString( str ) ) ) ).arg( languageCode ).toUtf8() );
|
|
|
|
printf( "req: %s\n", reqUrl.toEncoded().data() );
|
|
|
|
sptr< QNetworkReply > netReply = mgr.get( QNetworkRequest( reqUrl ) );
|
|
|
|
netReplies.push_back( std::make_pair( netReply, false ) );
|
|
}
|
|
|
|
void ForvoArticleRequest::requestFinished( QNetworkReply * r )
|
|
{
|
|
printf( "Finished.\n" );
|
|
|
|
if ( isFinished() ) // Was cancelled
|
|
return;
|
|
|
|
// Find this reply
|
|
|
|
bool found = false;
|
|
|
|
for( NetReplies::iterator i = netReplies.begin(); i != netReplies.end(); ++i )
|
|
{
|
|
if ( i->first.get() == r )
|
|
{
|
|
i->second = true; // Mark as finished
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !found )
|
|
{
|
|
// Well, that's not our reply, don't do anything
|
|
return;
|
|
}
|
|
|
|
bool updated = false;
|
|
|
|
for( ; netReplies.size() && netReplies.front().second; netReplies.pop_front() )
|
|
{
|
|
sptr< QNetworkReply > netReply = netReplies.front().first;
|
|
|
|
if ( netReply->error() == QNetworkReply::NoError )
|
|
{
|
|
QDomDocument dd;
|
|
|
|
QString errorStr;
|
|
int errorLine, errorColumn;
|
|
|
|
if ( !dd.setContent( netReply.get(), false, &errorStr, &errorLine, &errorColumn ) )
|
|
{
|
|
setErrorString( QString( tr( "XML parse error: %1 at %2,%3" ).
|
|
arg( errorStr ).arg( errorLine ).arg( errorColumn ) ) );
|
|
}
|
|
else
|
|
{
|
|
printf( "%s\n", dd.toByteArray().data() );
|
|
|
|
QDomNode items = dd.namedItem( "items" );
|
|
|
|
if ( !items.isNull() )
|
|
{
|
|
QDomNodeList nl = items.toElement().elementsByTagName( "item" );
|
|
|
|
if ( nl.count() )
|
|
{
|
|
string articleBody;
|
|
|
|
articleBody += "<div class='forvo_headword'>";
|
|
articleBody += Html::escape( word.toUtf8().data() );
|
|
articleBody += "</div>";
|
|
|
|
articleBody += "<table class=\"forvo_play\">";
|
|
|
|
for( unsigned x = 0; x < nl.length(); ++x )
|
|
{
|
|
QDomElement item = nl.item( x ).toElement();
|
|
|
|
QDomNode mp3 = item.namedItem( "pathmp3" );
|
|
|
|
if ( !mp3.isNull() )
|
|
{
|
|
articleBody += "<tr>";
|
|
|
|
QUrl url( mp3.toElement().text() );
|
|
|
|
string ref = string( "\"" ) + url.toEncoded().data() + "\"";
|
|
|
|
articleBody += addAudioLink( ref, dictionaryId ).c_str();
|
|
|
|
bool isMale = ( item.namedItem( "sex" ).toElement().text().toLower() != "f" );
|
|
|
|
QString user = item.namedItem( "username" ).toElement().text();
|
|
QString country = item.namedItem( "country" ).toElement().text();
|
|
|
|
string userProfile = string( "http://www.forvo.com/user/" ) +
|
|
QUrl::toPercentEncoding( user ).data() + "/";
|
|
|
|
int totalVotes = item.namedItem( "num_votes" ).toElement().text().toInt();
|
|
int positiveVotes = item.namedItem( "num_positive_votes" ).toElement().text().toInt();
|
|
int negativeVotes = totalVotes - positiveVotes;
|
|
|
|
string votes;
|
|
|
|
if ( positiveVotes || negativeVotes )
|
|
{
|
|
votes += " ";
|
|
|
|
if ( positiveVotes )
|
|
{
|
|
votes += "<span class='forvo_positive_votes'>+";
|
|
votes += QByteArray::number( positiveVotes ).data();
|
|
votes += "</span>";
|
|
}
|
|
|
|
if ( negativeVotes )
|
|
{
|
|
if ( positiveVotes )
|
|
votes += " ";
|
|
|
|
votes += "<span class='forvo_negative_votes'>-";
|
|
votes += QByteArray::number( negativeVotes ).data();
|
|
votes += "</span>";
|
|
}
|
|
}
|
|
|
|
string addTime =
|
|
tr( "Added %1" ).arg( item.namedItem( "addtime" ).toElement().text() ).toUtf8().data();
|
|
|
|
articleBody += "<td><a href=" + ref + " title=\"" + Html::escape( addTime ) + "\"><img src=\"qrcx://localhost/icons/playsound.png\" border=\"0\" alt=\"Play\"/></a></td>";
|
|
articleBody += string( "<td>" ) + tr( "by" ).toUtf8().data() + " <a class='forvo_user' href='"
|
|
+ userProfile + "'>"
|
|
+ Html::escape( user.toUtf8().data() )
|
|
+ "</a> <span class='forvo_location'>("
|
|
+ ( isMale ? tr( "Male" ) : tr( "Female" ) ).toUtf8().data()
|
|
+ " "
|
|
+ tr( "from" ).toUtf8().data()
|
|
+ " "
|
|
+ "<img src='qrcx://localhost/flags/" + Country::englishNametoIso2( country ).toUtf8().data()
|
|
+ ".png'/> "
|
|
+ Html::escape( country.toUtf8().data() )
|
|
+ ")</span>"
|
|
+ votes
|
|
+ "</td>";
|
|
articleBody += "</tr>";
|
|
}
|
|
}
|
|
|
|
articleBody += "</table>";
|
|
|
|
Mutex::Lock _( dataMutex );
|
|
|
|
size_t prevSize = data.size();
|
|
|
|
data.resize( prevSize + articleBody.size() );
|
|
|
|
memcpy( &data.front() + prevSize, articleBody.data(), articleBody.size() );
|
|
|
|
hasAnyData = true;
|
|
|
|
updated = true;
|
|
}
|
|
}
|
|
|
|
QDomNode errors = dd.namedItem( "errors" );
|
|
|
|
if ( !errors.isNull() )
|
|
setErrorString( errors.namedItem( "error" ).toElement().text() );
|
|
}
|
|
printf( "done.\n" );
|
|
}
|
|
else
|
|
setErrorString( netReply->errorString() );
|
|
}
|
|
|
|
if ( netReplies.empty() )
|
|
finish();
|
|
else
|
|
if ( updated )
|
|
update();
|
|
}
|
|
|
|
sptr< DataRequest > ForvoDictionary::getArticle( wstring const & word,
|
|
vector< wstring > const & alts,
|
|
wstring const & )
|
|
throw( std::exception )
|
|
{
|
|
if ( word.size() > 80 )
|
|
{
|
|
// Don't make excessively large queries -- they're fruitless anyway
|
|
|
|
return new DataRequestInstant( false );
|
|
}
|
|
else
|
|
return new ForvoArticleRequest( word, alts, apiKey, languageCode, getId(),
|
|
netMgr );
|
|
}
|
|
|
|
QIcon ForvoDictionary::getIcon() throw()
|
|
{
|
|
// Experimental code to generate icon -- but the flags clutter the interface too
|
|
// much and we're better with a single icon.
|
|
#if 0
|
|
if ( languageCode.size() == 2 )
|
|
{
|
|
QString countryCode = Language::countryCodeForId( LangCoder::code2toInt( languageCode.toAscii().data() ) );
|
|
|
|
if ( countryCode.size() )
|
|
{
|
|
QImage flag( QString( ":/flags/%1.png" ).arg( countryCode.toLower() ) );
|
|
|
|
if ( !flag.isNull() )
|
|
{
|
|
QImage img( ":/icons/forvo_icon_base.png" );
|
|
|
|
{
|
|
QPainter painter( &img );
|
|
painter.drawImage( QPoint( 5, 7 ), flag );
|
|
}
|
|
|
|
return QIcon( QPixmap::fromImage( img ) );
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
return QIcon( ":/icons/forvo.png" );
|
|
}
|
|
|
|
}
|
|
|
|
vector< sptr< Dictionary::Class > > makeDictionaries(
|
|
Dictionary::Initializing &,
|
|
Config::Forvo const & forvo,
|
|
QNetworkAccessManager & mgr )
|
|
throw( std::exception )
|
|
{
|
|
vector< sptr< Dictionary::Class > > result;
|
|
|
|
if ( forvo.enable )
|
|
{
|
|
QStringList codes = forvo.languageCodes.split( ',', QString::SkipEmptyParts );
|
|
|
|
QSet< QString > usedCodes;
|
|
|
|
for( int x = 0; x < codes.size(); ++x )
|
|
{
|
|
QString code = codes[ x ].simplified();
|
|
|
|
if ( code.size() && !usedCodes.contains( code ) )
|
|
{
|
|
// Generate id
|
|
|
|
QCryptographicHash hash( QCryptographicHash::Md5 );
|
|
|
|
hash.addData( "Forvo source version 1.0" );
|
|
hash.addData( code.toUtf8() );
|
|
|
|
QString displayedCode( code.toLower() );
|
|
|
|
if ( displayedCode.size() )
|
|
displayedCode[ 0 ] = displayedCode[ 0 ].toUpper();
|
|
|
|
result.push_back(
|
|
new ForvoDictionary( hash.result().toHex().data(),
|
|
QString( "Forvo (%1)" ).arg( displayedCode ).toUtf8().data(),
|
|
forvo.apiKey, code, mgr ) );
|
|
|
|
usedCodes.insert( code );
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
}
|