kde-extraapps/juk/ktrm.cpp
2014-11-28 15:08:46 +00:00

771 lines
20 KiB
C++

/**
* Copyright (C) 2004 Scott Wheeler <wheeler@kde.org>
* Copyright (C) 2007 Michael Pyne <mpyne@kde.org>
*/
/**
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version
* 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA
*/
#include "ktrm.h"
#include <config-juk.h>
#if HAVE_TUNEPIMP > 0
#include <kprotocolmanager.h>
#include <kurl.h>
#include <kdebug.h>
#include <kio/job.h>
#include <QCoreApplication>
#include <QtAlgorithms>
#include <QMutex>
#include <QRegExp>
#include <QFile>
#include <QEvent>
#include <QDomDocument>
#if HAVE_TUNEPIMP >= 5
#include <tunepimp-0.5/tp_c.h>
#else
#include <tunepimp/tp_c.h>
#endif
#ifdef Q_WS_X11
#include <fixx11h.h>
#endif
#include "ktrm.moc"
class KTRMLookup;
extern "C"
{
#if HAVE_TUNEPIMP >= 4
static void TRMNotifyCallback(tunepimp_t pimp, void *data, TPCallbackEnum type, int fileId, TPFileStatus status);
#else
static void TRMNotifyCallback(tunepimp_t pimp, void *data, TPCallbackEnum type, int fileId);
#endif
}
/**
* This represents the main TunePimp instance and handles incoming requests.
*/
class KTRMRequestHandler
{
public:
static KTRMRequestHandler *instance()
{
static QMutex mutex;
mutex.lock();
static KTRMRequestHandler handler;
mutex.unlock();
return &handler;
}
int startLookup(KTRMLookup *lookup)
{
int id;
if(!m_fileMap.contains(lookup->file())) {
#if HAVE_TUNEPIMP >= 4
id = tp_AddFile(m_pimp, QFile::encodeName(lookup->file()), 0);
#else
id = tp_AddFile(m_pimp, QFile::encodeName(lookup->file()));
#endif
m_fileMap.insert(lookup->file(), id);
}
else {
id = m_fileMap[lookup->file()];
tp_IdentifyAgain(m_pimp, id);
}
m_lookupMap[id] = lookup;
return id;
}
void endLookup(KTRMLookup *lookup)
{
tp_ReleaseTrack(m_pimp, tp_GetTrack(m_pimp, lookup->fileId()));
tp_Remove(m_pimp, lookup->fileId());
m_lookupMap.remove(lookup->fileId());
}
bool lookupMapContains(int fileId) const
{
m_lookupMapMutex.lock();
bool contains = m_lookupMap.contains(fileId);
m_lookupMapMutex.unlock();
return contains;
}
KTRMLookup *lookup(int fileId) const
{
m_lookupMapMutex.lock();
KTRMLookup *l = m_lookupMap[fileId];
m_lookupMapMutex.unlock();
return l;
}
void removeFromLookupMap(int fileId)
{
m_lookupMapMutex.lock();
m_lookupMap.remove(fileId);
m_lookupMapMutex.unlock();
}
const tunepimp_t &tunePimp() const
{
return m_pimp;
}
protected:
KTRMRequestHandler()
{
m_pimp = tp_New("KTRM", "0.1");
//tp_SetDebug(m_pimp, true);
#if HAVE_TUNEPIMP < 5
tp_SetTRMCollisionThreshold(m_pimp, 100);
tp_SetAutoFileLookup(m_pimp,true);
#endif
tp_SetAutoSaveThreshold(m_pimp, -1);
tp_SetMoveFiles(m_pimp, false);
tp_SetRenameFiles(m_pimp, false);
#if HAVE_TUNEPIMP >= 4
tp_SetFileNameEncoding(m_pimp, "UTF-8");
#else
tp_SetUseUTF8(m_pimp, true);
#endif
tp_SetNotifyCallback(m_pimp, TRMNotifyCallback, 0);
#if HAVE_TUNEPIMP < 5
// Re-read proxy config.
KProtocolManager::reparseConfiguration();
if(KProtocolManager::useProxy()) {
// split code copied from kcm_kio.
QString noProxiesFor = KProtocolManager::noProxyFor();
QStringList noProxies = noProxiesFor.split(QRegExp("[',''\t'' ']"), QString::SkipEmptyParts);
bool useProxy = true;
// Host that libtunepimp will contact.
QString tunepimpHost = "www.musicbrainz.org";
QString tunepimpHostWithPort = "www.musicbrainz.org:80";
// Check what hosts are allowed to proceed without being proxied,
// or if using reversed proxy, what hosts must be proxied.
foreach(const QString &host, noProxies) {
const QString normalizedHost(KUrl::fromAce(KUrl::toAce(host)));
if(normalizedHost == tunepimpHost ||
tunepimpHost.endsWith('.' + normalizedHost))
{
useProxy = false;
break;
}
// KDE's proxy mechanism also supports exempting a specific
// host/port combo, check that also.
if(normalizedHost == tunepimpHostWithPort ||
tunepimpHostWithPort.endsWith('.' + normalizedHost))
{
useProxy = false;
break;
}
}
// KDE supports a reverse proxy mechanism. Uh, yay.
if(KProtocolManager::useReverseProxy())
useProxy = !useProxy;
if(useProxy) {
KUrl proxy = KProtocolManager::proxyFor("http");
QString proxyHost = proxy.host();
kDebug() << "Using proxy server " << proxyHost << " for www.musicbrainz.org.\n";
tp_SetProxy(m_pimp, proxyHost.toAscii(), short(proxy.port()));
}
}
#else
tp_SetMusicDNSClientId(m_pimp, "0c6019606b1d8a54d0985e448f3603ca");
#endif
}
~KTRMRequestHandler()
{
tp_Delete(m_pimp);
}
private:
tunepimp_t m_pimp;
QMap<int, KTRMLookup *> m_lookupMap;
QMap<QString, int> m_fileMap;
mutable QMutex m_lookupMapMutex;
};
/**
* A custom event type used for signalling that a TRM lookup is finished.
*/
class KTRMEvent : public QEvent
{
public:
enum Status {
Recognized,
Unrecognized,
Collision,
PuidGenerated,
Error
};
KTRMEvent(int fileId, Status status) :
QEvent(static_cast<QEvent::Type>(id)),
m_fileId(fileId),
m_status(status)
{
}
int fileId() const
{
return m_fileId;
}
Status status() const
{
return m_status;
}
static const int id = User + 1984; // random, unique, event id
private:
int m_fileId;
Status m_status;
};
/**
* A helper class to intercept KTRMQueryEvents and call recognized() (from the GUI
* thread) for the lookup.
*/
class KTRMEventHandler : public QObject
{
public:
static void send(int fileId, KTRMEvent::Status status)
{
QCoreApplication::postEvent(instance(), new KTRMEvent(fileId, status));
}
protected:
KTRMEventHandler() : QObject() {}
static KTRMEventHandler *instance()
{
static QMutex mutex;
mutex.lock();
static KTRMEventHandler handler;
mutex.unlock();
return &handler;
}
virtual void customEvent(QEvent *event)
{
if(event->type() != KTRMEvent::id)
return;
KTRMEvent *e = static_cast<KTRMEvent *>(event);
static QMutex mutex;
mutex.lock();
if(!KTRMRequestHandler::instance()->lookupMapContains(e->fileId())) {
mutex.unlock();
return;
}
KTRMLookup *lookup = KTRMRequestHandler::instance()->lookup(e->fileId());
#if HAVE_TUNEPIMP >= 4
if ( e->status() != KTRMEvent::Unrecognized)
#endif
KTRMRequestHandler::instance()->removeFromLookupMap(e->fileId());
mutex.unlock();
switch(e->status()) {
case KTRMEvent::Recognized:
lookup->recognized();
break;
case KTRMEvent::Unrecognized:
lookup->unrecognized();
break;
case KTRMEvent::Collision:
lookup->collision();
break;
case KTRMEvent::Error:
lookup->error();
break;
case KTRMEvent::PuidGenerated:
break; // XXX: Does anything special need done here?
}
}
};
/**
* Callback function for TunePimp lookup events.
*/
#if HAVE_TUNEPIMP >= 4
static void TRMNotifyCallback(tunepimp_t /*pimp*/, void * /*data*/, TPCallbackEnum type, int fileId, TPFileStatus status)
#else
static void TRMNotifyCallback(tunepimp_t pimp, void *data, TPCallbackEnum type, int fileId)
#endif
{
if(type != tpFileChanged)
return;
#if HAVE_TUNEPIMP < 4
track_t track = tp_GetTrack(pimp, fileId);
TPFileStatus status = tr_GetStatus(track);
#endif
switch(status) {
case eRecognized:
KTRMEventHandler::send(fileId, KTRMEvent::Recognized);
break;
case eUnrecognized:
KTRMEventHandler::send(fileId, KTRMEvent::Unrecognized);
break;
#if HAVE_TUNEPIMP >= 5
case ePUIDLookup:
case ePUIDCollision:
case eFileLookup:
KTRMEventHandler::send(fileId, KTRMEvent::PuidGenerated);
break;
#else
case eTRMCollision:
#if HAVE_TUNEPIMP >= 4
case eUserSelection:
#endif
KTRMEventHandler::send(fileId, KTRMEvent::Collision);
break;
#endif
case eError:
KTRMEventHandler::send(fileId, KTRMEvent::Error);
break;
default:
break;
}
#if HAVE_TUNEPIMP < 4
tp_ReleaseTrack(pimp, track);
#endif
}
////////////////////////////////////////////////////////////////////////////////
// KTRMResult implementation
////////////////////////////////////////////////////////////////////////////////
class KTRMResult::KTRMResultPrivate
{
public:
KTRMResultPrivate() : track(0), year(0), relevance(0) {}
QString title;
QString artist;
QString album;
int track;
int year;
int relevance;
};
////////////////////////////////////////////////////////////////////////////////
// KTRMResult public methods
////////////////////////////////////////////////////////////////////////////////
KTRMResult::KTRMResult()
{
d = new KTRMResultPrivate;
}
KTRMResult::KTRMResult(const KTRMResult &result)
{
d = new KTRMResultPrivate(*result.d);
}
KTRMResult::~KTRMResult()
{
delete d;
}
QString KTRMResult::title() const
{
return d->title;
}
QString KTRMResult::artist() const
{
return d->artist;
}
QString KTRMResult::album() const
{
return d->album;
}
int KTRMResult::track() const
{
return d->track;
}
int KTRMResult::year() const
{
return d->year;
}
bool KTRMResult::operator<(const KTRMResult &r) const
{
return r.d->relevance < d->relevance;
}
bool KTRMResult::operator>(const KTRMResult &r) const
{
return r.d->relevance > d->relevance;
}
bool KTRMResult::isEmpty() const
{
return d->title.isEmpty() && d->artist.isEmpty() && d->album.isEmpty() &&
d->track == 0 && d->year == 0;
}
////////////////////////////////////////////////////////////////////////////////
// KTRMLookup implementation
////////////////////////////////////////////////////////////////////////////////
class KTRMLookup::KTRMLookupPrivate
{
public:
KTRMLookupPrivate() :
fileId(-1) {}
QString file;
KTRMResultList results;
int fileId;
bool autoDelete;
};
////////////////////////////////////////////////////////////////////////////////
// KTRMLookup public methods
////////////////////////////////////////////////////////////////////////////////
KTRMLookup::KTRMLookup(const QString &file, bool autoDelete)
{
d = new KTRMLookupPrivate;
d->file = file;
d->autoDelete = autoDelete;
d->fileId = KTRMRequestHandler::instance()->startLookup(this);
}
KTRMLookup::~KTRMLookup()
{
KTRMRequestHandler::instance()->endLookup(this);
delete d;
}
QString KTRMLookup::file() const
{
return d->file;
}
int KTRMLookup::fileId() const
{
return d->fileId;
}
void KTRMLookup::recognized()
{
kDebug() << d->file;
d->results.clear();
metadata_t *metaData = md_New();
track_t track = tp_GetTrack(KTRMRequestHandler::instance()->tunePimp(), d->fileId);
tr_Lock(track);
tr_GetServerMetadata(track, metaData);
KTRMResult result;
result.d->title = QString::fromUtf8(metaData->track);
result.d->artist = QString::fromUtf8(metaData->artist);
result.d->album = QString::fromUtf8(metaData->album);
result.d->track = metaData->trackNum;
result.d->year = metaData->releaseYear;
d->results.append(result);
md_Delete(metaData);
tr_Unlock(track);
finished();
}
void KTRMLookup::unrecognized()
{
kDebug() << d->file;
#if HAVE_TUNEPIMP >= 4
char trm[255];
bool finish = false;
trm[0] = 0;
track_t track = tp_GetTrack(KTRMRequestHandler::instance()->tunePimp(), d->fileId);
tr_Lock(track);
#if HAVE_TUNEPIMP >= 5
tr_GetPUID(track, trm, 255);
#else
tr_GetTRM(track, trm, 255);
#endif
if ( !trm[0] ) {
tr_SetStatus(track, ePending);
tp_Wake(KTRMRequestHandler::instance()->tunePimp(), track);
}
else
finish = true;
tr_Unlock(track);
tp_ReleaseTrack(KTRMRequestHandler::instance()->tunePimp(), track);
if ( !finish )
return;
#endif
d->results.clear();
finished();
}
void KTRMLookup::collision()
{
#if HAVE_TUNEPIMP < 5
kDebug() << d->file;
track_t track = tp_GetTrack(KTRMRequestHandler::instance()->tunePimp(), d->fileId);
if(track <= 0) {
kDebug() << "invalid track number";
return;
}
tr_Lock(track);
int resultCount = tr_GetNumResults(track);
if(resultCount > 0) {
TPResultType type;
result_t *results = new result_t[resultCount];
tr_GetResults(track, &type, results, &resultCount);
switch(type) {
case eNone:
kDebug() << "eNone";
break;
case eArtistList:
kDebug() << "eArtistList";
break;
case eAlbumList:
kDebug() << "eAlbumList";
break;
case eTrackList:
{
kDebug() << "eTrackList";
albumtrackresult_t **tracks = (albumtrackresult_t **) results;
d->results.clear();
for(int i = 0; i < resultCount; i++) {
KTRMResult result;
result.d->title = QString::fromUtf8(tracks[i]->name);
#if HAVE_TUNEPIMP >= 4
result.d->artist = QString::fromUtf8(tracks[i]->artist.name);
result.d->album = QString::fromUtf8(tracks[i]->album.name);
result.d->year = tracks[i]->album.releaseYear;
#else
result.d->artist = QString::fromUtf8(tracks[i]->artist->name);
result.d->album = QString::fromUtf8(tracks[i]->album->name);
result.d->year = tracks[i]->album->releaseYear;
#endif
result.d->track = tracks[i]->trackNum;
result.d->relevance = tracks[i]->relevance;
d->results.append(result);
}
break;
}
case eMatchedTrack:
kDebug() << "eMatchedTrack";
break;
}
delete [] results;
}
tr_Unlock(track);
finished();
#endif
}
void KTRMLookup::puidGenerated()
{
#if HAVE_TUNEPIMP >= 5
char puid[255] = {0};
track_t track = tp_GetTrack(KTRMRequestHandler::instance()->tunePimp(), d->fileId);
tr_Lock(track);
tr_GetPUID(track, puid, 255);
tr_Unlock(track);
tp_ReleaseTrack(KTRMRequestHandler::instance()->tunePimp(), track);
d->results.clear();
KIO::Job *job = KIO::storedGet( QString( "http://musicbrainz.org/ws/1/track/?type=xml&puid=%1" ).arg( puid ) , KIO::NoReload, KIO::HideProgressInfo );
connect( job, SIGNAL(result(KJob*)), SLOT(lookupResult(KJob*)) );
#endif
}
void KTRMLookup::lookupResult( KJob* job )
{
#if HAVE_TUNEPIMP >= 5
if ( job->error() != 0 ) {
finished();
return;
}
KIO::StoredTransferJob* const storedJob = static_cast<KIO::StoredTransferJob*>( job );
QString xml = QString::fromUtf8( storedJob->data().data(), storedJob->data().size() );
QDomDocument doc;
QDomElement e;
if( !doc.setContent( xml ) ) {
finished();
return;
}
e = doc.namedItem( "metadata" ).toElement().namedItem( "track-list" ).toElement();
QStringList strList = d->file.split('/');
QDomNode n = e.namedItem("track");
for( ; !n.isNull(); n = n.nextSibling() ) {
QDomElement track = n.toElement();
KTRMResult result;
result.d->title = track.namedItem( "title" ).toElement().text();
result.d->artist = track.namedItem( "artist" ).toElement().namedItem( "name" ).toElement().text();
QDomNode releaseNode = track.namedItem("release-list").toElement().namedItem("release");
for( ; !releaseNode.isNull(); releaseNode = releaseNode.nextSibling() ) {
KTRMResult tmpResult( result );
QDomElement release = releaseNode.toElement();
tmpResult.d->album = release.namedItem( "title" ).toElement().text();
QDomNode tracklistN = release.namedItem( "track-list" );
if ( !tracklistN.isNull() ) {
QDomElement tracklist = tracklistN.toElement();
if ( !tracklist.attribute( "offset" ).isEmpty() )
tmpResult.d->track = tracklist.attribute( "offset" ).toInt() + 1;
}
//tmpResult.d->year = ???;
tmpResult.d->relevance = static_cast<int>(
4 * stringSimilarity(strList,tmpResult.d->title) +
2 * stringSimilarity(strList,tmpResult.d->artist) +
1 * stringSimilarity(strList,tmpResult.d->album)
);
/*
if( !d->results.contains( tmpResult ) )
d->results.append( tmpResult );
*/
}
}
qSort(d->results);
finished();
#else
Q_UNUSED( job );
#endif
}
void KTRMLookup::error()
{
kDebug() << d->file;
d->results.clear();
finished();
}
KTRMResultList KTRMLookup::results() const
{
return d->results;
}
////////////////////////////////////////////////////////////////////////////////
// KTRMLookup protected methods
////////////////////////////////////////////////////////////////////////////////
void KTRMLookup::finished()
{
if(d->autoDelete)
delete this;
}
////////////////////////////////////////////////////////////////////////////////
// Helper Functions used for sorting MusicBrainz results
////////////////////////////////////////////////////////////////////////////////
double stringSimilarity(const QStringList &l, const QString &s)
{
double max = 0, current = 0;
foreach(const QString &string, l) {
if(max < (current = stringSimilarity(string, s)))
max = current;
}
return max;
}
double stringSimilarity(QString s1, QString s2)
{
QRegExp whiteSpace("[\\s\\t\\r\\n]");
s1.remove(whiteSpace);
s2.remove(whiteSpace);
double nCommon = 0;
int p1 = 0, p2 = 0, x1 = 0, x2 = 0;
int l1 = s1.length(), l2 = s2.length(), l3 = l1 + l2;
QChar c1 = 0, c2 = 0;
while(p1 < l1 && p2 < l2) {
c1 = s1.at(p1); c2 = s2.at(p2);
if(c1.toUpper() == c2.toUpper()) {
++nCommon;
++p1; ++p2;
}
else {
x1 = s1.indexOf(c2, p1, Qt::CaseInsensitive);
x2 = s2.indexOf(c1, p2, Qt::CaseInsensitive);
if( (x1 == x2 || -1 == x1) || (-1 != x2 && x1 > x2) )
++p2;
else
++p1;
}
}
return l3 ? nCommon * 2.0 / l3 : 1.0;
}
#endif // (HAVE_TUNEPIMP > 0)
// vim: set et sw=4 tw=0 sta: